xo xofactory
Blog
urlxo

A short link's hot path: Redis, a 302, and a queue

How urlxo redirects without waiting for analytics, which characters it refuses to put in a code, and what it does not store about you.

A short link has one job that has to be fast and one job that has to be accurate, and they pull in opposite directions. The redirect must happen in a few milliseconds. The click must be counted with its country, device and referrer. urlxo does the first one synchronously and the second one somewhere else.

The redirect

A request for urlxo.link/k7Qm does this:

  1. Look up link:md5(k7Qm) in Redis. The value is the destination URL plus a few flags (paused, password-protected, expiry, click limit).
  2. If the flags allow it, return a 302 to the destination.
  3. Push a job onto the queue with the code, the timestamp, the User-Agent, the Referer, and the country Cloudflare attached to the request.

No database query, no analytics write, nothing the visitor waits for except the Redis round trip. If the key is not in Redis, the link is loaded from MariaDB once and cached; after that it stays hot until it is edited.

The queue worker does the rest: parses the user agent into a device and browser, drops the click if the user agent is a known bot or a link preview fetcher (Slack, WhatsApp, Telegram and friends all fetch a link the moment it is pasted), and writes a row. The daily chart on the stats page is a sum over those rows, cached for a minute.

The alphabet

Generated codes use a 56-character alphabet: digits and letters without 0, O, 1, l and I. Someone reading a code off a slide or over the phone should not have to guess. Custom aliases can use anything URL-safe, but the generator never produces an ambiguous one.

Codes are case-sensitive, so k7Qm and K7QM are different links. That doubles the space but it also means the database column has to compare bytes, not letters. MariaDB’s default collation is case-insensitive, which would make the two codes collide on insert. The column uses utf8mb4_bin, and there is a test that creates both and expects both to exist, because this is the kind of thing that gets “fixed” by a well-meaning migration a year later.

What is not stored

No visitor IP addresses. Country comes from Cloudflare’s header and is stored as a two-letter code; the address itself is never written anywhere. Unique visitors are counted with a hash of the IP and the user agent together with a salt that rotates every day, so the same person is one unique visitor today and a different one tomorrow, and nobody can reverse the hash into an address next week.

The creator of an anonymous link is stored as a hashed IP, so the hourly limit on anonymous links can be enforced and so an abused link can be traced to a batch. Logged-in users are stored as users.

The boring parts

Passwords on links are hashed with bcrypt like any password. Expiry and click limits are checked at redirect time from the cached flags, so an expired link is dead the moment it expires and not after a cron run. Pausing a link rewrites the cached value; resuming it does the same. The QR code is an SVG generated once and cached, because the one thing worse than a slow redirect is a slow QR code on a printed poster.