Scale & edge
How redirects stay under 10 ms at millions of requests — Cloudflare edge worker, KV link configs, queues, Redis streams, batched ingest, rollups and partitions.
Updated 2026-09-02
Principles#
- The redirect path never writes to the database. Clicks are queued and written in batches.
- Link configuration is cached close to the request: edge KV → in-process LRU → Redis → Postgres, with negative caching for unknown slugs and single-flight refresh.
- Enrichment and fraud scoring happen off the request path.
visitor ──▶ Cloudflare Worker (edge/) KV link configs · request.cf geo · rate-limit binding
│ 302 in < 10 ms, no origin call
└─▶ Queue wc-clicks ──▶ consumer batches ≤ 100 ──▶ POST /api/ingest/clicks (HMAC)
│
control plane (Next.js) ── LRU + Redis cache ── Redis Streams wc:clicks ──▶ worker ──▶ Postgres
link configs · negative cache · single-flight batched createMany + scoring
Postgres ── BRIN on createdAt · GiST cidr pool · StatDaily rollups · monthly partitionsEdge worker#
edge/ (Cloudflare Workers, wrangler.jsonc) serves /c/:slug and /l/:slug from 300+ locations using the same routing engine as the control plane (src/lib/routing/engine.ts). Geo, ASN and bot score come from request.cf. A cron every 5 minutes warms KV from GET /api/edge/links/hot. Clicks go to a Queue whose consumer POSTs HMAC-signed batches to /api/ingest/clicks.
cd edge && npm i
wrangler kv namespace create LINKS
wrangler queues create wc-clicks && wrangler queues create wc-clicks-dlq
wrangler secret put EDGE_INGEST_SECRET && wrangler secret put CONTROL_PLANE_TOKEN
wrangler deployWithout Cloudflare#
The same code runs in-process: the ingest buffer flushes every 250 ms or 500 rows to Postgres (src/lib/infra/ingest.ts). With REDIS_URL set, clicks go to a Redis Stream and pnpm worker consumes them, which decouples web replicas from database latency. Behaviour is identical; only headroom differs.
Database#
prisma/sql/scale.sql (pnpm db:scale) adds what Prisma cannot express: BRIN indexes on time columns, the GiST index on IpRange.cidr, monthly partitions for Click, and helper functions. pnpm rollup maintains StatDaily for the overview; retention purge drops old partitions.
Rate limiting#
Three layers: Workers rate-limit binding at the edge (per IP / /64), the Next.js proxy (per IP per path), and Redis token buckets per API key for write endpoints.
Capacity guidance#
| Setup | Sustained redirects | Notes |
|---|---|---|
| Single Next.js instance + Postgres | ~1 500 / s | Memory batching |
| Next.js × N + Redis + worker | ~10 000 / s | Streams absorb bursts |
| Cloudflare edge + queues | Millions / day per region, bursts of 50 000 / s | Origin only sees batches |