When to Use Cloudflare Workers — Zero-Backend Server Logic and a CORS Proxy

TL;DR: This site — the blog and the tools — runs on zero backend servers. No EC2, no always-on box, no deploy pipeline. Pure static frontend. And yet it measures accurate traffic and pulls data from third-party APIs that block CORS. Building frontend-only, you hit exactly two walls — a full server is overkill, but the browser alone can't — and I wedge a Cloudflare Worker into both. This is when a Worker is the right tool, with code.

So what is a Cloudflare Worker

In one line: a function that runs at the edge, before the request reaches your server. You don't keep a server running (it executes only on request), deploys are one command, and the free tier alone covers 100k requests a day. Need data? Attach D1 (Cloudflare's SQLite).

It fits precisely here:

The middle ground where a full backend is overkill, but the frontend alone can't do it.

When you want a bit of logic in front of the request — no provisioning, no always-on box, no deploy infra.

Below are the two cases I hit most often building frontend-only.

Wall ① Frontend-only can't measure traffic accurately

Some things a static frontend just can't do — accurate visit analytics is the classic one. Browser-side analytics (GA4, GoatCounter) lose a big chunk to ad blockers, privacy browsers, and bots. You need to measure server-side to be accurate — but standing up a backend just for stats is the tail wagging the dog.

That's the Worker's spot. Intercept the request at the edge and log it to D1, and you get server-side analytics with no backend server. That's exactly what runs on this site.

Visitor browser Cloudflare edge Worker · example.com/* ① return origin response as-is ② human HTML views only ctx.waitUntil( async log ) origin static hosting D1 (SQLite) hits table · 1 row INSERT request ① proxy ② async log
▲ The visitor gets the origin response first (①); the hit is logged afterward, out of band (②). That ordering is why analytics adds no latency.

The key is responding first and logging afterward, asynchronously (ctx.waitUntil) — the visitor doesn't feel a millisecond of it. There's a lot of detail here — fail-open so a logging bug can't take the site down, cookieless daily hashing so you count visitors without storing personal data, bot filtering by network, a cron rollup, and locking the dashboard so nobody else can open it.

I broke the full build — setup, the actual Worker code, and the dashboard — into its own post: Build Self-Hosted Web Analytics on Cloudflare Workers + D1. If the analytics angle is what brought you here, that's the one with all the code.

The lesson from case ① wasn't a trick — it was asking whether I actually needed the fancy tool. Cloudflare has Workflows (durable execution); I considered it for the rollup and didn't use it, because it changes how something runs, not how much it aggregates. "There's a shiny tool, let's use it" is the wrong reflex; "does this problem need it?" is the right one.

Wall ② The browser blocks a third-party API with CORS

Fetching an external API or resource from the frontend, you hit this wall:

Access to fetch at 'https://api.foo.com/...' from origin 'https://mysite.com'
has been blocked by CORS policy

CORS means the response is only readable from another origin's browser if the server adds a permission header (Access-Control-Allow-Origin). The catch: whether that header gets added is the other server's call. If they don't add it, the browser gives you no way through.

If it's your API — one header

If that API is your own Worker, it's trivial: add one header to the response. This site's stats dashboard calls its stats API from another origin, and the Worker opens it up like this:

// your stats API response
headers: {
  'content-type': 'application/json; charset=utf-8',
  'access-control-allow-origin': '*',   // ← this one line is the CORS grant
}

If it's someone else's API — stand up a proxy Worker

The real problem is an API you can't change. Here you put a Worker in the middle as a proxy: browser → (your Worker) → their API, and the Worker adds CORS headers to the response on the way back. Server-to-server calls have no CORS, so it goes through.

This is where most tutorials teach something dangerous. Make it an "open proxy" that fetches any URL, and you've just built a free relay for the whole internet plus an SSRF target. So I lock it down — allowlist of hosts + GET only + restrict who can call it + strip cookies:

const ALLOW_HOSTS   = ['api.foo.com'];             // only hosts you'll proxy for
const ALLOW_ORIGINS = ['https://mysite.com'];      // only your site may call this proxy

export default {
  async fetch(request) {
    const origin = pickOrigin(request);
    if (request.method === 'OPTIONS')                       // preflight
      return new Response(null, { status: 204, headers: corsHeaders(origin) });
    if (request.method !== 'GET' && request.method !== 'HEAD')
      return json({ error: 'method not allowed' }, 405, origin);

    const t = new URL(new URL(request.url).searchParams.get('url'));
    if (t.protocol !== 'https:')          return json({ error: 'https only' }, 400, origin);
    if (!ALLOW_HOSTS.includes(t.hostname)) return json({ error: 'host not allowed' }, 403, origin);

    const upstream = await fetch(t.toString(), {            // no cookies/auth forwarded
      headers: { accept: request.headers.get('accept') || '*/*' },
      cf: { cacheTtl: 300, cacheEverything: true },         // 5-min edge cache
    });
    const headers = new Headers(upstream.headers);
    Object.entries(corsHeaders(origin)).forEach(([k, v]) => headers.set(k, v));
    headers.delete('set-cookie');                            // block cookie leakage
    return new Response(upstream.body, { status: upstream.status, headers });
  },
};

From the frontend you call it like this:

// what used to be blocked by CORS
fetch('https://api.foo.com/data.json')
// now goes through your proxy
fetch('https://cors.mysite.com/?url=' + encodeURIComponent('https://api.foo.com/data.json'))
A CORS proxy is as dangerous as it is convenient. Never deploy an open proxy without an allowlist. Name the hosts you'll proxy, accept GET only, and don't pass through auth headers or cookies — those four are the minimum safety line.

Summary — when a Worker, when a real backend

The instinct that runs through both cases:

When you have… Reach for
Persistent state, DB transactions, complex domain logic A real backend server
Light server-side logic in front of the request (logging, aggregation, auth checks, redirects) A Worker
A third-party API that won't send CORS A Worker proxy (allowlist)
Something the frontend can do on its own Just the frontend

A Worker isn't a silver bullet. Heavy state or long transactions still want a real backend. But for that awkward middle — overkill for a full server, impossible for the frontend alone — one stat, one CORS wall — nothing beats it.

If you run a static site and a moment comes where you think "do I really have to stand up a server for this?" — remember Workers. It's less code than you'd expect.

Share𝕏f

Comments