We printed 18,000 flyers for a DACH product launch in March. One short link on the back, three regional landing pages we wanted to send people to: /de for German visitors, /fr for the small French slice, /en for everyone else. The marketing lead asked the obvious question: do we print three flyers or one?
You print one. The link does the routing.
A "smart link" is a single short URL whose destination is computed at the redirect, not at link-creation time. There is one slug. There are several possible destinations. The decision happens in the same handler that would otherwise issue a plain 302 — no separate service to call, no JS shim on a landing page, no extra hop. This post is about what that actually looks like under the hood, the six dimensions Elido routes on, and the cases where you should reach for a different tool instead.
Three things a smart link is not#
People reach for smart links from three different prior experiences, and the trade-offs are different in each.
Plain redirect. One slug, one destination, zero logic. The redirect handler does a cache lookup and writes a 302. You can't beat it on latency; you also can't make it conditional. That's the floor — anything fancier costs something.
Smart link at the edge. One slug, several possible destinations, a tiny rule evaluation step inserted between the cache lookup and the response. Because the rule lives in the same process as the cache lookup, the cost is sub-millisecond (0.3ms p50 / 1ms p95 in Elido's case). The visitor sees one HTTP round trip. The browser cache isn't poisoned, because 302 responses aren't cacheable by default per RFC 7234 §4.2.2 — a fact that matters here, because per-request routing only makes sense if every request is allowed to choose its own destination.
JavaScript A/B router on a landing page. A neutral HTML page renders, JS examines navigator.userAgent or a geo-IP service, then window.location = '/foo'. This is the worst option of the three. The visitor sees an HTML render, then a redirect, then the actual page — at least one extra round trip, often two if the geo lookup is third-party. SEO indexing is muddled because crawlers see the neutral page. Cookie-blocking browsers and privacy extensions break the JS half. Apple's Intelligent Tracking Prevention 2.3 release notes call out exactly this pattern: client-side tracking links via document referrer get throttled, and the mitigation requires server-side participation. If you're routing in JS today, you're already paying the bill.
The right place to put a routing decision is the same hop that's already issuing the redirect. That's what edge smart links do.
Why it lives at the edge — the latency budget#
The Elido redirect tier has a hard latency budget: p50 5ms, p95 15ms on a cache hit, excluding TLS handshake. That number isn't aspirational — anything that pushes us over gets ripped out. Synchronous SQL on the hot path, regex compilation per request, blocking I/O on the click event: all gone, all moved to cold-path workers.
The two reasons that budget exists:
- Mobile networks add their own tax. Apple's "Reducing Network Latency" guide walks through how cellular network delays compound across redirect chains. Each extra hop adds RTT that the visitor's network already inflated. The fewer hops we add, the less their network punishes them.
- Edge proximity is the real lever. Cloudflare's primer on edge-side routing frames it the same way: the cheapest decision is the one made in the same process as the response writer, in the POP closest to the visitor. We're not unique in doing edge routing; what's unique is bundling it into the URL shortener instead of asking you to deploy a separate Workers / Lambda@Edge function.
If we punted rule evaluation to a downstream service — say, a hypothetical "rules-api" reachable over HTTP — we'd add a same-region round trip on every request. In the EU that's around 5ms minimum (Frankfurt → Frankfurt over a private network), and in US-Singapore traffic the tail goes ugly very fast. The 15ms p95 doesn't survive the round trip. So smart link rules are inline, in the edge binary, evaluating against compiled matchers that were built when the link was loaded into the cache. The whole rules engine is around 400 lines of Go.
That tight coupling is also why we can do real-time rule edits: rule changes propagate via a Redis pub/sub channel (link:invalidate) that every edge POP subscribes to. The L1 LRU evicts within a second of the publish, the next request repopulates from L2, and the new rule is live. More on this below.
The six routing dimensions#
Elido smart links match on six things. Each maps to a specific input the edge has access to per request.
Country. Two-letter ISO 3166-1 alpha-2, derived from the visitor's IP via geoip. Useful when you have regional storefronts and the per-country uplift in conversion is worth the routing complexity. The classic gotcha here is travelers — a German on holiday in Spain hits the Spanish destination if you route on country alone. If the language preference matters more than the geographic location, route on languages instead. We discuss the full geoip flow in the analytics privacy post — the IP is truncated before storage so the GDPR side stays clean.
Device. mobile, tablet, desktop, parsed from the User-Agent string at request time. The use case marketers reach for this for: app-install banners that go to the App Store on iOS, Play Store on Android, and a marketing page on desktop. The thing to watch for: User-Agent strings on iPad have been a moving target since iPadOS started presenting the desktop Safari UA by default, and our tablet detection accommodates that, but it's not 100% on every browser version. If the difference between tablet and desktop traffic matters to you in dollars, instrument the destination and verify.
OS. ios, android, macos, windows, linux. Same User-Agent source as device, narrower partition. The deep-link case: route iOS visitors to a Universal Link that the app intercepts and falls back to the App Store; route Android to the Play Store with referrer data preserved. This is what we built the Apple App Site Association integration for.
Language. Primary language tag from the visitor's Accept-Language header. ISO 639-1 codes like de, fr, pt. The trap: Accept-Language is the browser's preference, which often disagrees with the IP geo. A French expat in Berlin gets country: DE, languages: ["fr", "en"] — if you want them on /fr, route on language; if you want them on the German storefront because you're A/B testing localised pricing, route on country. Sequence the rules accordingly.
Time of day and day of week. HH:MM window in any IANA timezone, plus a days_of_week bitmap. Time-windowed deals — a "happy hour" landing page that goes live at 17:00 Europe/Berlin Mon–Fri and falls back to the regular page outside that window — are the natural fit. The time_start / time_end window supports wraparound (22:00 → 02:00), which sounds obvious but caught us when we ported the rule engine from the prototype that didn't handle it. The full schema is in the smart links guide.
Referrer host. The hostname portion of the Referer header, normalised. Useful for partner-aware destinations: visitors arriving from partner.example get a co-branded landing page; everyone else gets the default. Less useful than it used to be — modern browsers strip Referer aggressively when the referring page sets Referrer-Policy: no-referrer or when the navigation crosses HTTPS contexts in a way the policy doesn't allow. Treat referrer rules as a soft signal, never as authentication.
That's the lot. Six dimensions cover the marketing routing decisions we've seen in three years of customer conversations. The deliberate omissions are user identity (we don't know it on the redirect), arbitrary HTTP headers (the cost-to-payoff isn't there for the few teams who've asked), and randomised splits (use variant rotation instead, which is a separate feature).
First-match semantics; fallback always required#
Rules are an array. The edge walks them in order. The first rule whose match block is fully satisfied wins, and its destination_url is the redirect target. The link's top-level destination_url is the unconditional fallback. We refuse to create a smart link without one — a smart link never produces a 404, by design.
The minimum viable shape:
{
"destination_url": "https://acme.example/en",
"targeting_rules": [
{
"match": { "countries": ["DE", "AT", "CH"] },
"destination_url": "https://acme.example/de"
},
{
"match": { "languages": ["fr"] },
"destination_url": "https://acme.example/fr"
}
]
}
DACH visitors hit /de because rule 1 matches first. A French expat in Berlin has country=DE, so they also hit /de — rule 1 matches before rule 2 gets a chance. If you want the French expat on /fr, swap the rules so the language rule is checked first. The order in the dashboard is the order we evaluate.
Two things this implies that are worth saying out loud:
- Wider rules go last. A rule with no
matchconditions matches everything; if it's first, no rule below it ever fires. The dashboard validates against this and warns you, but the API doesn't, so script-built rules need a sanity check. - Mutual exclusion is on you. If two rules both match a single visitor, the first one wins silently. There's no error, no flag, no metric. We've considered emitting a warning at link-load time when two rules are detectable as overlapping, and that's on the roadmap for the next minor release. For now: read your rules top-to-bottom and trust order.
The cost: cache invalidation propagation#
Every routing decision has a propagation window. Smart link rules edited in the dashboard propagate through L1 caches at all three Elido POPs in roughly 1 second on the happy path. Roughly, because:
- The L1 LRU at each POP holds rule-bearing links with a 60-second TTL (the cache architecture is documented here). The TTL is the upper bound — even without an invalidate publish, a stale entry is gone within a minute.
- The invalidate publish is via Redis pub/sub. The Frankfurt and Ashburn POPs share a Redis cluster; Singapore has its own. Cross-region propagation is essentially Redis replication latency plus our subscriber's pub/sub processing, which has been under 1 second p99 in our metrics for the past quarter.
- A POP that's lost its Redis subscription falls back to the 60-second TTL. We alert on subscription loss; the on-call has 5 minutes of buffered clicks before the WAL kicks in.
Translation: for marketing flows where 60 seconds of stale routing is fine, you don't have to think about this. For flows where staleness matters — a legal disclaimer rotation, a billing cohort split where the wrong destination charges the wrong currency — the play is status=disabled first, then re-enable after a minute, then publish the new rule. We've added a GET /v1/links/{id}/status endpoint so a CI pipeline can poll for the propagation to finish before flipping a switch downstream.
When not to use a smart link#
Three cases where the right tool is not a smart link.
Server-side rendering of the destination is better. If the variant has to be injected into the HTML response — say, price localisation that depends on the visitor's authenticated state, or a landing page that pulls a cohort-specific hero from your CMS — that's a job for the destination's own server, not the redirect. The redirect chooses where to send the visitor; the destination chooses what to render. Routing logic that lives at the edge can't see your auth session, and shoehorning it would require either leaking the session into the redirect path (which we won't do) or proxying through the edge (which we don't do because of the latency budget). Render variants on origin.
Statistically rigorous A/B testing. Smart links route per request, not per visitor. If a visitor lands twice in five minutes from the same device, they might see two different destinations under a randomised rule, which is the right behaviour for "send 50% of mobile traffic to A and 50% to B" but the wrong behaviour for "measure whether variant A converts better than variant B over a 4-week window". For the latter, you need a stable variant cookie and an experimentation tool that does the statistics properly. PostHog, GrowthBook, and LaunchDarkly all do this. We don't, and we're not going to — the tooling is a different job. Use variant rotation with round_robin for low-stakes sampling and reach for an experiment platform when you need to defend the result.
Identity-aware routing. Smart links are deliberately stateless. They evaluate against country | device | OS | language | time | referrer and nothing else. If you need to route based on a logged-in user's tier, their feature flags, or anything that requires looking up "who is this person", the redirect path is the wrong layer. Resolve identity on origin and serve the variant from there. Or, if you really need a redirect-time decision, mint per-user short links via the API — every authenticated user gets their own slug, the slug's destination is correct for that user at creation time, and you never have to do identity resolution on the hot path.
What's next#
If you want to try the rule shape on your own data, the docs guide walks through the JSON schema and the dashboard editor. The rule builder lives under any link's edit page in the dashboard — Links → ⋯ → Targeting.
Two improvements landing in the next minor release: a fallback hierarchy for languages (so pt-BR cleanly degrades to pt, then to en, without writing three rules), and a static analysis pass at link-save time that flags overlapping rules so the dashboard can warn before the rule goes live. Both are implementation work, no breaking schema changes. If you've got a rule shape we don't support and you think we should, the feedback channel is on the bottom of the smart links feature page.