Skip to content

Cloudflare Workers: When Edge Functions Actually Earn Their Keep

Workers are fast and cheap. They’re also the wrong answer for half the things people use them for. Here’s when they fit.

John Cravey with EleviFounder4 min read

Cloudflare Workers run TypeScript on Cloudflare’s edge network — 300+ global locations, sub-10ms cold starts, generous free tier. They’re a real tool. They’re also the thing people reach for whenever “serverless” comes up in a meeting, often when a Next.js middleware function or a Supabase Edge Function would do the job cleaner. Here’s the framework we use for deciding when Workers are right.

What a Worker actually is

A TypeScript/JavaScript script deployed to Cloudflare. It runs in V8 (not Node — no `fs`, no `child_process`, no `node:` modules). It handles HTTP requests, fetches other URLs, and has bindings to Cloudflare-native primitives (KV, R2, Durable Objects, D1). Deployment is `wrangler deploy`.

Three patterns we run for FH clients

Pattern 1: edge-level URL rewrites for a legacy site we don’t control

One FH client has a WordPress site we’re migrating piece-by-piece to Next.js. During the transition, certain URLs serve from the new Next app and others from WordPress. A Worker sits in front, inspects the path, and routes to the correct backend. The Worker is the only piece of infrastructure that knows about both backends.

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const url = new URL(req.url);
    const path = url.pathname;
    if (path.startsWith("/blog/") || path === "/about") {
      return fetch("https://new.example.com" + path, req);
    }
    return fetch("https://wp.example.com" + path, req);
  },
};

Pattern 2: A/B test variant selection that persists across the CDN cache

Next.js middleware can assign A/B variants, but if your origin caches HTML at the CDN, the cached version might serve the wrong variant. A Worker that runs before the cache layer sets a variant cookie, then routes to a variant-specific URL (`/`, `/variant-b`). Each variant URL caches independently.

Pattern 3: cron-scheduled jobs that don’t fit in our Next app

Workers can run on a cron schedule via Cron Triggers. We use one Worker to refresh sitemaps across all FH client sites every night at 3am UTC. The Worker hits each site’s `/api/refresh-sitemap` endpoint with an auth header. Cheap, simple, runs forever.

// wrangler.toml
// [triggers]
// crons = ["0 3 * * *"]

export default {
  async scheduled(event: ScheduledEvent, env: Env) {
    const sites = ["fh", "bhr", "cab", "tivey", "james-marina"];
    await Promise.all(
      sites.map((s) =>
        fetch(`https://${s}.example.com/api/refresh-sitemap`, {
          method: "POST",
          headers: { authorization: `Bearer ${env.CRON_SECRET}` },
        })
      )
    );
  },
};

Three patterns we DON’T use Workers for

  • Form submissions for a Next.js app. The server action is right there. Don’t add a separate Worker.
  • Database queries against Postgres. Workers don’t hold persistent connections; every request opens a new one through PgBouncer. Latency is fine but the architecture is wrong — put the query in your origin app.
  • Heavy business logic. The 50ms CPU budget per Worker invocation is generous but not infinite. Compute-heavy work belongs in a regular server or a queue worker.

Workers vs Pages Functions

Cloudflare Pages includes a “Functions” feature that’s basically Workers attached to a Pages site. Same runtime, same APIs, just deployed alongside the static site. Use Pages Functions when the function is tightly coupled to a Pages-hosted app; use standalone Workers when the function fronts something else or runs independently.

Bindings: where Workers get powerful

Workers bind to other Cloudflare services as first-class objects. Need to read from R2? `env.MY_BUCKET.get(key)`. Need to write to KV? `env.MY_KV.put(key, value)`. Need to query D1? `env.MY_DB.prepare("SELECT …").first()`. The bindings are declared in `wrangler.toml` and injected at runtime. No connection strings, no credentials in code.

Local development

`wrangler dev` runs the Worker locally with Miniflare, simulating the Cloudflare runtime including bindings. Works well; the gap between local and production is small. We run Worker tests with `vitest` + `miniflare` for the unit tests, then deploy to a `*.workers.dev` staging URL for integration testing.

Cost and limits

  • Free tier: 100k requests per day, 10ms CPU per request, 128MB memory.
  • Paid tier: $5/month for 10M requests, 30M extra requests at $0.30 per million. 50ms CPU per request, can be raised.
  • KV reads: 10M free per day on the paid plan.
  • R2 storage: $0.015/GB-month, no egress fees.
  • D1: 5M reads + 100k writes per day on free tier.

For most SMB use cases, the free tier covers everything. Paid kicks in at meaningful scale.

Deployment and CI

`wrangler deploy` ships from local. For team setups, use GitHub Actions to deploy on every push to main. Cloudflare provides the auth token format. The Worker is live within seconds of the action passing.

Debugging

`console.log()` shows up in the Cloudflare Workers dashboard logs. For real observability, use `wrangler tail` to stream logs locally, or pipe logs to Workers Analytics Engine for query-able retention. For errors, integrate Sentry’s Cloudflare Workers SDK — works cleanly.

When NOT to start with Workers

If you’re building a Next.js app and you haven’t yet hit a limitation of Next’s built-in primitives (server actions, route handlers, middleware), don’t add Workers. They add a vendor surface, a separate deployment, and a separate set of conventions. Use the right tool for the actual problem. Most “we should use Workers for X” conversations end with “…or we could just write it as a server action.”

How this lands across FH client work

Across the FH client book, we run exactly four Workers: the WordPress-routing Worker for one client, the A/B test Worker for another, the cross-site sitemap-refresh cron, and a small image-resizing Worker that fronts an R2 bucket for one retail client. Everything else runs in the per-site Next app where it belongs. The bias is toward fewer moving parts, not more. If a Worker isn’t the unambiguous right answer, book a consultation and we’ll help you decide — usually the answer is to keep the logic in your Next app.

Written by
John Cravey
Founder

Founder of Frontend Horizon. Writes most of the long-form work on the FH blog.

Newer post
Google Ads for Small Businesses: The Complete Guide to Running Campaigns That Don’t Waste Your Budget
Older post
Container Queries Everywhere: The CSS Feature That Killed the Breakpoint Mindset
Keep reading

More from the blog

Cloudflare·6 min

Cloudflare DNS and CDN: The Base Configuration for Every FH Client Site

Every FH site sits behind Cloudflare. Here’s the exact configuration and why each setting is where it is.

Cloudflare·5 min

Cloudflare Pages vs Workers vs R2: Picking the Right Cloudflare Product

Cloudflare has 30+ products. Three of them cover 80% of what most SMB sites need.

Cloudflare·5 min

Cloudflare WAF and Bot Management for SMB Sites: The Rules That Actually Work

WAF isn’t set-and-forget. Here’s the configuration that catches the real attacks without breaking legitimate traffic.