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 AIFounder4 min readUpdated Jul 6, 2026

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.

Free estimate · 2 minutes

Read the playbook. Now see it for your business.

The posts are the mechanism. The estimate below sketches the version we'd actually ship for your business, at your scale. About a minute, no opt-in.

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.

Answers

Frequently asked questions

What is a Worker, concretely?

Code that runs at the edge before or instead of a request reaching your origin, in a restricted runtime with fast start-up. The model suits small, latency-sensitive decisions and suits general application logic badly, which is the distinction most adoption decisions get wrong.

Which patterns genuinely earn a Worker?

Rewriting or routing before the origin sees the request, serving or transforming something without waking the application, and enforcing rules that must hold across multiple origins. All three share the property that doing them in the application would be too late or in the wrong place.

Which patterns should not be a Worker?

Ordinary application logic, anything needing your session or database context, and anything that changes whenever the app changes. That last one is decisive: two deploy pipelines for one feature is a workflow people abandon within a month.

Workers or Pages Functions?

Pages Functions when the logic belongs to one site and deploys with it. Workers when it is independent, spans origins, or needs its own lifecycle. Choosing on capability rather than ownership is what produces edge code nobody can find later.

What makes bindings important?

They give a Worker direct access to storage and services without a network hop or credentials in the source. Without them, edge code fetching over the public internet gives up the latency advantage that justified running at the edge in the first place.

How do I develop and debug Workers?

Locally, with the runtime emulated, because the deploy-and-check loop is slow enough to waste a day. Logging matters more here than in an application: there is no attached debugger in production and the failures tend to be environmental rather than logical.

What are the runtime limits?

Bounded CPU time per request, a restricted API surface, and size limits on the deployed script. Those constraints are what make the model fast, and they are also what makes porting existing Node code into a Worker unreliable rather than merely fiddly.

What does it cost?

Very little at small volume and it scales with request count, which is exactly the profile that surprises people when a Worker is placed in front of every request rather than on a specific route. Scope matters more to the bill than the code does.

How should Workers be deployed?

Through the same pipeline as everything else, versioned and reviewed. Edge code deployed by hand from a laptop is the classic source of a rule nobody can explain, still running, months after the person who wrote it moved on.

When should a team not start with Workers?

When they have not yet established that the origin is too slow or too late for the job. Most latency problems are caching problems, and a Worker introduced before caching has been fixed adds a component without addressing the cause.

Can a Worker break my site?

Yes, completely, because it sits in the request path for whatever it matches. A bug there is not a broken feature, it is a broken site, which is why scope and rollback matter more for edge code than for anything running behind it.

What is the honest summary?

Excellent for a narrow class of before-the-origin work, unnecessary for most application logic, and easy to over-adopt because the first use case is genuinely satisfying. Add one when a specific problem names it, and keep the matcher as tight as it will go.

Question we did not answer? Ask us directly and we will answer it here.

John Cravey, Founder
Written by
John Cravey
Founder

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

Newer post
Bundle Size Budgets: How to Stop JS Bloat Before It Ships
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·6 min

Cloudflare Pages vs Workers vs R2: Which One to Use in 2026

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.