shortened.cv

A URL shortener with privacy-respecting click analytics: two cache layers in front of PostgreSQL, clicks recorded through Redis Streams without slowing the redirect, and published load and failure tests behind every performance claim.

5 min readLive

Topics: Go · PostgreSQL · Redis · Nuxt 4 · System Design · Performance

shortened.cv: short links, less nonsense

shortened.cv turns long URLs into links people can actually share. You can shorten a link without an account, sign in later to claim it, and see who clicked, from where, and on what device, without the service ever storing a raw IP address.

The product is small on purpose. The interesting part is underneath: a redirect path that answers from memory, a click pipeline that never makes a visitor wait for the database, and a set of load and failure tests published next to the code.

Web App · GitHub · Write-up · Benchmarks

Tech Stack: Go · Gin · PostgreSQL 17 · Redis Streams · sqlc · goose · limen · Nuxt 4 · Nuxt UI · TanStack Query · k6 · Prometheus · Docker · Coolify · Traefik


What it does

FeatureDetails
Shorten without signing upAnonymous links come back with a one-time claim token, saved in the browser. Sign in later and they move into your account.
AccountsEmail magic links or Google sign-in. No passwords to store.
Custom slugs and previewsSigned-in users pick their own slug. Add + to any short link to see where it goes before visiting.
AnalyticsClicks, unique visitors, bot visits, a daily chart, and the top sources, referrers, countries, devices, browsers, operating systems and UTM campaigns, over 7, 30 or 90 days.
SafetyAnonymous rate limits, optional Google Web Risk checks on new links, and an admin takedown that stops a link redirecting on every instance at once.

Making redirects fast

Every redirect checks two caches before it touches the database:

  • L1, in-process: a size-bounded LRU in the Go service answers hot links with no network hop.
  • L2, Redis: shared across instances, so a new instance doesn't start cold.
  • PostgreSQL: only on a miss in both. Concurrent misses for the same slug share one lookup, so a link that goes viral can't stampede the database, and lookups are capped at half the connection pool.

Redirects use 302, not 301. A browser caches a 301 forever, and every later click would skip the service and go uncounted.

Deleting or taking down a link writes a short-lived revocation to Redis and broadcasts it, so every instance forgets the slug immediately instead of waiting for its cache entry to expire.


Counting clicks without slowing anyone down

The visitor gets their redirect first. The click is recorded afterwards:

  1. The click is parsed (source, referrer host, device, browser, OS, country, UTM tags) and appended to a Redis stream within about 20 ms.
  2. Consumers write batches to PostgreSQL and acknowledge an entry only after the commit. If an instance dies mid-batch, another one claims the unfinished entries.
  3. Every click carries an event ID, and the insert skips IDs already stored, so a retried batch never counts a click twice.
  4. If Redis is down or the stream is over budget, clicks fall back to an in-memory writer. Redirects keep working either way.

Raw clicks live in daily partitions for 30 days; a maintenance job rolls closed days into daily summaries that are kept for good. Reports read the summaries for past days and raw clicks only for today. Unique visitors are a keyed hash of IP and user agent that rotates every day, so a visitor can't be followed across days.


Measured, not claimed

Every number below links to its raw results and the environment it ran in.

On one laptop (API limited to 2 CPUs and 512 MiB, plain HTTP, k6 on the same machine):

WorkloadDurationRedirect p99Result
3,200 redirects/s + 100 creates/s + 10 reports/s60 s7.9 msAll 192,000 clicks reconciled
5,000 redirects/s to one hot link30 s14.2 msCompleted
167 reports/s over 3 million stored clicks, alongside 1,000 redirects/s60 sreport p95 16.5 msCompleted

On the live 4 vCPU VPS, through HTTPS and Traefik, with k6 on the same host:

Rate held for 60 sErrorsp95Host CPU
1,000 redirects/s0%13.8 ms71%
2,000 redirects/s0%68.1 ms86%
3,000 redirects/sStopped—98%

At 2,000 redirects a second, the Go service used about half a vCPU and 115 MB of memory. TLS termination and the load generator took most of the machine. The test reused connections and hit one cached link, so real visitors cost more, and the held-minute figures are approximate because the first version of the script timed its warm-up per virtual user; the benchmarks README spells out what each number does and doesn't show.


Breaking it on purpose

Throughput numbers don't say what happens when a dependency fails, so I stopped them mid-traffic:

  • PostgreSQL down: cached links kept redirecting at 500 requests a second through the outage; uncached ones returned 503 until it came back.
  • Redis down: redirect p99 rose from about 2 ms to 51 ms, which exposed a retry that waited on Redis too often.
  • API killed mid-batch: the unfinished clicks were reclaimed by another consumer after restart.
  • A failed revocation: let a deleted link reload into a local cache. Local revocation tracking and retries closed the gap.

Running it

  • API: a single Go binary in Docker on a Coolify-managed VPS, behind Traefik with Let's Encrypt. PostgreSQL and Redis run beside it on the private Docker network.
  • Migrations: embedded in the binary and applied at startup under a Postgres advisory lock. A failed migration stops the new container while the old one keeps serving.
  • Web app: Nuxt 4 on Vercel at app.shortened.cv, calling the API with credentialed CORS.
  • Deploys: a push to main rebuilds whichever app changed.
  • Observability: Prometheus metrics and pprof on an internal port, readiness and liveness endpoints, and an access log that can drop to errors only.

The full story, from slug generation to the reporting queries, is in the write-up: Building shortened.cv.

A closer look

Your links: click counts, search, copy and per-link actions.
Per-link analytics: a launch spike, then where the clicks came from.
Fully responsive: shorten, manage and read stats on a phone.
Shorten a link without an account.
Light and dark themes, following the system by default.
Add + to any short link to see where it goes first.
Even the 404 is on brand.