Workers runtime and deployment

Cloudflare Workers run inside workerd — Cloudflare’s open-source JavaScript/WebAssembly runtime built on V8 isolates. Unlike containerized serverless (Lambda, Cloud Run), every Worker invocation runs in a V8 isolate inside the same process at the POP — there’s no container/microVM cold start, just an isolate-creation cost of typically 5-50ms. The runtime is web-standards-first: fetch, Request, Response, URL, Streams, WebCrypto, with optional Node.js compatibility via a nodejs_compat flag. Deployment is via Wrangler — a single command (wrangler deploy) pushes the worker globally to all 330+ POPs within ~30 seconds.

See also

1. The V8-isolate model

Per developers.cloudflare.com/workers/. A V8 isolate is a sandboxed JS execution context inside the V8 engine. Cloudflare’s runtime (workerd) holds thousands of isolates per process and reuses them across requests:

  • First request to a Worker in a region — isolate is created from the deployed code bundle (cold start, ~5-50ms).
  • Subsequent requests — isolate is reused (~0ms isolate cost, just request handling).
  • Idle isolates — kept warm for minutes, evicted on memory pressure.

Compared to Lambda’s microVM + Node.js model:

  • Cloudflare cold start: 5-50ms typical.
  • Lambda cold start: 200ms-2s (Node), 1-3s (Java), 500ms-1s (Python).
  • Lambda warm start: ~5-20ms.
  • Workers warm start: ~0-3ms (no container, just isolate).

2. Limits

Per developers.cloudflare.com/workers/platform/limits/:

LimitFreePaid ($5/mo+)
CPU time per invocation10 ms30 sec (default) — up to 5 min
Memory128 MB128 MB
Subrequests per invocation501000
Script size (compressed)3 MB10 MB
Wall-clock time30 sec (incl. waiting on I/O)15 min (with event.waitUntil)
Number of Workers per account100500
Environment variables per Worker64128
Secrets per Worker64128

CPU time vs wall-clock: CPU time is when the JS thread is executing. Wall-clock includes time spent awaiting fetch() and other I/O. A Worker doing await fetch(...) for 2s uses ~5ms of CPU and 2s of wall-clock. CPU is what’s billed.

The 10ms CPU on Free is a hard limit — Workers exceeding it return EXCEEDED_CPU and the user sees an error. For real workloads, the Paid plan’s 30-second-default-to-5-minute is what you need.

3. Pricing

Per developers.cloudflare.com/workers/platform/pricing/:

Free plan ($0/mo)

  • 100,000 requests / day
  • 10 ms CPU per invocation
  • 100k KV reads / day, 1k KV writes / day
  • 100k Hyperdrive queries / day
  • Limited D1, DO (SQLite only), Queues, R2
  • Requests: 10M / month included, then $0.30 / million
  • CPU: 30M ms / month included, then $0.02 per additional million ms
  • Up to 5-minute CPU per invocation (default 30s)

Service-specific pricing

ServiceIncluded (Paid)Overage
KV reads10M / mo$0.50 / M
KV writes / list / delete1M / mo$5 / M
D1 rows read25B / mo$0.001 / M
D1 rows written50M / mo$1 / M
Durable Objects requests1M / mo$0.15 / M
DO duration400k GB-sec / mo$12.50 / M
R2 storageFirst 10 GB free$0.015 / GB-month
R2 Class A ops (writes)1M / mo$4.50 / M
R2 Class B ops (reads)10M / mo$0.36 / M
R2 egress$0 (zero egress)

The zero egress on R2 is the killer feature vs S3 — for content-heavy workloads, R2 saves the per-GB-out fee that S3 charges ($0.09/GB for first 10TB).

4. Wrangler CLI

Per developers.cloudflare.com/workers/wrangler/. The CLI for everything Workers-related.

Install

npm i -g wrangler
# Or use locally:
npx wrangler --version

Key commands

# Project lifecycle
wrangler init my-worker             # scaffold new Worker
wrangler dev                        # local dev server (port 8787)
wrangler dev --remote               # run locally but with real production bindings
wrangler deploy                     # deploy to Cloudflare
wrangler delete                     # tear down
 
# Bindings
wrangler secret put MY_SECRET      # set encrypted secret
wrangler kv namespace create CACHE
wrangler r2 bucket create my-bucket
wrangler d1 create my-db
wrangler queues create my-queue
 
# Observability
wrangler tail                       # live tail logs from production
wrangler tail --status error        # filter to errors
wrangler tail --format json | jq
 
# D1
wrangler d1 execute my-db --command "SELECT * FROM users LIMIT 10"
wrangler d1 migrations create my-db init
wrangler d1 migrations apply my-db
 
# Misc
wrangler whoami
wrangler login
wrangler types                      # generate TS types for bindings

5. wrangler.toml / wrangler.jsonc configuration

Per developers.cloudflare.com/workers/wrangler/configuration/.

wrangler.toml (legacy but still default)

name = "my-worker"
main = "src/index.ts"
compatibility_date = "2026-05-01"
compatibility_flags = ["nodejs_compat"]
 
# Default Environment ("production")
[vars]
ENVIRONMENT = "production"
 
# Bindings
[[kv_namespaces]]
binding = "CACHE"
id = "abc123def456"
 
[[r2_buckets]]
binding = "ASSETS"
bucket_name = "my-app-assets"
 
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "uuid-here"
 
[[durable_objects.bindings]]
name = "ROOMS"
class_name = "Room"
 
[ai]
binding = "AI"
 
# Environments
[env.staging]
name = "my-worker-staging"
vars = { ENVIRONMENT = "staging" }
{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-05-01",
  "compatibility_flags": ["nodejs_compat"],
  
  "vars": {
    "ENVIRONMENT": "production"
  },
  
  "kv_namespaces": [
    { "binding": "CACHE", "id": "abc123def456" }
  ],
  
  "r2_buckets": [
    { "binding": "ASSETS", "bucket_name": "my-app-assets" }
  ],
  
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "my-db",
      "database_id": "uuid-here"
    }
  ],
  
  "durable_objects": {
    "bindings": [
      { "name": "ROOMS", "class_name": "Room" }
    ]
  },
  
  "ai": { "binding": "AI" },
  
  "env": {
    "staging": {
      "name": "my-worker-staging",
      "vars": { "ENVIRONMENT": "staging" }
    }
  }
}

6. compatibility_date and compatibility_flags

Per developers.cloudflare.com/workers/configuration/compatibility-dates/. Cloudflare ships changes to the runtime that are usually backward-compatible — but for breaking changes, they’re gated behind a compatibility_date. Your Worker uses the runtime behavior as of the date you set.

compatibility_date = "2026-05-01"   # use runtime behavior as of this date

Some features are opt-in via compatibility_flags:

compatibility_flags = [
  "nodejs_compat",                 # Node.js APIs (process, Buffer, etc.)
  "global_navigator",              # navigator.userAgent
  "transformstream_enable_standard_constructor",
  "streams_enable_constructors",
  "url_standard",                  # WHATWG URL spec compliance
  "no_handle_cross_request_promise_resolution",
]

When in doubt, copy the flags from wrangler init’s scaffolded config — they reflect current best practices.

7. Module syntax (ES modules — required)

Per developers.cloudflare.com/workers/configuration/modules/. Modern Workers use ES modules:

// src/index.ts
export interface Env {
  CACHE: KVNamespace;
  ASSETS: R2Bucket;
  DB: D1Database;
  AI: Ai;
  MY_SECRET: string;
}
 
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    
    if (url.pathname === "/cached") {
      const value = await env.CACHE.get("key");
      return Response.json({ value });
    }
    
    return new Response("Hello!");
  },
  
  // Other handlers — same Worker can have multiple
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    // Cron trigger
    console.log("Cron fired at", new Date(event.scheduledTime));
  },
  
  async queue(batch: MessageBatch, env: Env, ctx: ExecutionContext) {
    // Queue consumer
    for (const msg of batch.messages) {
      await processMessage(msg.body);
      msg.ack();
    }
  },
  
  async email(message: EmailMessage, env: Env, ctx: ExecutionContext) {
    // Email Worker
    await message.forward("alerts@example.com");
  },
};

The default export is an object containing handlers. fetch is the most common; scheduled, queue, and email are optional.

8. Local development

Per developers.cloudflare.com/workers/local-development/. wrangler dev runs the Worker locally using workerd (the real production runtime):

wrangler dev                    # local — uses local KV, local D1, local R2
wrangler dev --remote           # local code, but real production bindings
wrangler dev --port 3000
wrangler dev --inspect          # enable Chrome DevTools debugging

--local (default) gives fast iteration but isolated state. --remote is for testing against real prod data.

Miniflare — the framework wrapper around workerd that powers wrangler dev. Available as a standalone library too for use in tests:

import { Miniflare } from "miniflare";
 
const mf = new Miniflare({
  modules: true,
  scriptPath: "./src/index.ts",
  kvNamespaces: ["CACHE"],
  d1Databases: ["DB"],
});
 
const response = await mf.dispatchFetch("http://localhost/");

9. Smart Placement

Per developers.cloudflare.com/workers/configuration/smart-placement/. By default, Workers run at the POP closest to the user. For backend-heavy Workers that mostly call the same upstream database (e.g. a Postgres in AWS us-east-1), this means most requests pay ~80-200ms of latency between Worker (anywhere globally) and DB.

Smart Placement: Cloudflare analyzes your Worker’s traffic and relocates the Worker close to the most-called upstream. The user → Worker hop becomes slightly slower (e.g. user in Asia hits a Worker in Virginia), but the Worker → DB hop becomes near-zero, and the round-trip is faster overall.

Enable:

[placement]
mode = "smart"

Best for: backend-heavy Workers with one dominant DB region. Worst for: read-only Workers serving static-ish data globally.

10. Deployment

wrangler deploy                        # deploys current code as latest
wrangler deploy --env staging          # deploys to staging environment
wrangler rollback                      # roll back to previous version
wrangler versions list                 # list deployed versions
wrangler versions deploy <ver>         # gradual rollout to a version

Deployments propagate globally to all 330+ POPs within ~30 seconds. There is no concept of “deployment URL” like Vercel — your Worker’s URL is fixed (<worker>.<account>.workers.dev or your custom domain).

Gradual rollouts (re:Invent / 2024)

wrangler versions deploy 2-of-2 --percentage 10    # 10% of traffic to new version
wrangler versions deploy 2-of-2 --percentage 50    # 50%
wrangler versions deploy 2-of-2 --percentage 100   # full cutover

11. Workers vs Pages vs Pages Functions

The history is messy. Briefly:

  • Workers (2017+) — standalone JS/Wasm at the edge. Serverless functions, APIs, agents.
  • Pages (2021+) — static site hosting with git integration. Cloudflare’s Vercel competitor.
  • Pages Functions (2022+) — Workers attached to a Pages project, file-system-routed (functions/api/foo.ts).
  • Workers Static Assets (2024+) — Workers can now also serve static assets. Cloudflare is converging Workers + Pages.

As of 2026-05: new projects should use Workers with Static Assets for full-stack apps. Pages still exists for pure-static or framework-driven sites (Next.js, Astro, SvelteKit on Pages), but is the older path. Cloudflare’s stated direction is Workers as the unified platform.

# Workers with Static Assets
name = "my-app"
main = "./src/worker.ts"
compatibility_date = "2026-05-01"
 
[assets]
directory = "./dist"
binding = "ASSETS"

12. Multi-region considerations

Workers run at every POP — there’s no “pick a region” decision. The Worker’s code runs wherever the user is. The variables here are:

  • Where your data is — KV is globally replicated (eventually consistent reads, ~60s write propagation). R2 has a single home region per bucket but reads are CDN-cached globally. D1 has a primary region + read replicas. Durable Objects live in one region per ID.
  • Smart Placement — relocates Worker close to upstream DB.
  • Latency from POPs to upstreams — for non-Cloudflare upstreams (your own Postgres in AWS), measure and decide on smart placement.

13. Common patterns

Hello, world

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    return new Response("Hello, Cloudflare!");
  },
};

JSON API with bindings

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    
    if (url.pathname === "/api/user" && request.method === "GET") {
      const id = url.searchParams.get("id");
      const user = await env.DB.prepare("SELECT * FROM users WHERE id = ?")
        .bind(id)
        .first();
      return Response.json(user);
    }
    
    return new Response("Not found", { status: 404 });
  },
};

Background work after response (waitUntil)

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    // Fire and forget — response returned immediately, work continues
    ctx.waitUntil(env.CACHE.put("last-request", Date.now().toString()));
    
    return new Response("OK");
  },
};

14. Plans and account-level features

  • Free — 100k requests/day, 10ms CPU. Great for learning, hobby projects, prototypes.
  • Paid (Workers Paid) — $5/month minimum + usage. The real starting point.
  • Enterprise — custom pricing, advanced features (Argo Smart Routing, Magic Transit, etc.).

Pro tip: a single Paid account can host hundreds of Workers — the $5/mo gates the account, not per-Worker. Use one account for all your projects to avoid multiplying the floor.

Further reading