DEV Community

Max LIAO
Max LIAO

Posted on

How I Built Sub-50ms QR Code Redirects with nextjs, performance, Cloudflare Workers

Most QR code services run redirects through their main application server. Here's why that's a terrible idea, and how I solved it with Cloudflare Workers for OwnQR.

The Problem

When someone scans a dynamic QR code, three things happen:

  1. The phone opens a URL (like oq.link/abc123)
  2. The server looks up where abc123 should redirect to
  3. The user gets sent to the destination

If you run this through a Next.js/Vercel app, you're looking at:

  • Cold starts: 500ms+ on serverless
  • Single region: User in Tokyo, server in Virginia = 200ms latency
  • Database query: Another 50-100ms

That's 750ms+ before the user sees anything. For a QR code scan (which people expect to be instant), that feels broken.

The Solution: Edge Redirects

I moved the redirect logic to a Cloudflare Worker. The entire lookup + redirect happens at the nearest edge node:

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const url = new URL(request.url)
    const match = url.pathname.match(/^\/r\/([a-zA-Z0-9]+)$/)
    if (!match) return new Response('Not Found', { status: 404 })

    const slug = match[1]
    const qrCode = await getDestination(slug, env)
    if (!qrCode) return new Response('QR Code not found', { status: 404 })

    // Log scan event in background (non-blocking)
    ctx.waitUntil(logScanEvent(qrCode.id, request, env, request.cf))

    // 302 redirect (not 301 -- allows URL changes)
    return Response.redirect(qrCode.destination_url, 302)
  },
}
Enter fullscreen mode Exit fullscreen mode

Why 302 Instead of 301?

Dynamic QR codes let users change their destination URL anytime. A 301 (permanent redirect) gets cached by browsers -- if a user changes their link, returning visitors would still see the old destination. 302 ensures every scan hits the latest URL.

Free Geo Data with request.cf

Cloudflare gives you city, country, latitude, and longitude on every request via the request.cf object -- no external geo API needed:

async function logScanEvent(qrId: string, request: Request, env: Env, cf: any) {
  const ua = request.headers.get('User-Agent') || ''

  await fetch(`${env.SUPABASE_URL}/rest/v1/scan_events`, {
    method: 'POST',
    headers: {
      apikey: env.SUPABASE_ANON_KEY,
      Authorization: `Bearer ${env.SUPABASE_ANON_KEY}`,
      'Content-Type': 'application/json',
      Prefer: 'return=minimal',
    },
    body: JSON.stringify({
      qr_id: qrId,
      device_type: getDeviceType(ua),
      os_type: getOS(ua),
      ip_city: cf?.city || null,
      ip_country: cf?.country || null,
    }),
  })
}
Enter fullscreen mode Exit fullscreen mode

Non-Blocking Analytics with ctx.waitUntil()

The redirect fires immediately. Analytics logging happens in the background via ctx.waitUntil(). The user doesn't wait for the database insert.

Results

Metric Before (Next.js) After (Workers)
Average redirect ~750ms ~40ms
Cold starts Yes None
Edge locations 1 region 300+ cities
Infra cost Included in Vercel ~$5/month

Architecture

User scans QR -> Cloudflare Worker (edge) -> 302 redirect
                        | (async, non-blocking)
                  Log to Supabase
                  (device, city, country, timestamp)
Enter fullscreen mode Exit fullscreen mode

The main app (Next.js on Vercel) handles QR code creation, dashboard, payments, and design customization. The Worker only handles redirects -- the one thing that needs to be globally fast.

Open Source

I've open-sourced the redirect worker: cloudflare-qr-redirect on GitHub

Zero npm dependencies -- just Cloudflare Workers built-in APIs + Supabase REST.

The Product

This architecture powers OwnQR -- a QR code generator where you pay $15 once instead of monthly subscriptions. If you're tired of QR code services charging $7-15/month for a redirect, give it a try.


Have questions about the Cloudflare Workers setup? Drop a comment below.

Top comments (0)