← Articles

Deploying to Cloudflare Workers: A Developer's Guide

By Mark · 29 June 20260 views

Deploying to Cloudflare Workers: A Developer's Guide

Cloudflare Workers is a serverless platform that runs JavaScript, TypeScript, and WebAssembly at Cloudflare's global edge network — over 300 data centers in 100+ countries. Unlike traditional serverless platforms that spin up containers per invocation, Workers use the V8 isolate model, resulting in cold starts measured in microseconds rather than seconds. This guide walks through deploying a production-grade Cloudflare Worker from setup to CI/CD.

Why Cloudflare Workers?

  • Zero cold starts — V8 isolates start in under 1ms; your code runs immediately
  • Global by default — requests are served from the nearest PoP without any configuration
  • Generous free tier — 100,000 requests per day, unlimited in some plans
  • Integrated storage — KV (key-value), D1 (SQLite), R2 (object storage), and Durable Objects are all native
  • Low operational overhead — no servers to maintain, no auto-scaling configuration

Setting Up the Project

Install the Wrangler CLI:

npm install -g wrangler
wrangler login

Create a new Worker project:

npm create cloudflare@latest my-worker
cd my-worker

The generator creates a wrangler.toml (configuration) and src/index.ts (your Worker code).

The Worker Handler

Cloudflare Workers export a fetch handler that receives a Request and returns a Response:

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === '/health') {
      return new Response('OK', { status: 200 });
    }

    if (url.pathname === '/api/hello') {
      return Response.json({ message: 'Hello from the edge!' });
    }

    return new Response('Not Found', { status: 404 });
  },
};

The Env type holds your bindings (KV namespaces, secrets, D1 databases). The ExecutionContext provides ctx.waitUntil() for background tasks that should complete after the response is sent.

Routing

For non-trivial apps, use the itty-router library instead of manual pathname matching:

import { Router } from 'itty-router';

const router = Router();

router.get('/api/users/:id', async ({ params }, env: Env) => {
  const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?')
    .bind(params.id)
    .first();
  if (!user) return new Response('Not found', { status: 404 });
  return Response.json(user);
});

router.post('/api/users', async (request: Request, env: Env) => {
  const body = await request.json();
  // Insert into D1
  await env.DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
    .bind(body.name, body.email)
    .run();
  return Response.json({ success: true }, { status: 201 });
});

router.all('*', () => new Response('Not Found', { status: 404 }));

export default {
  fetch: router.handle,
};

Wrangler Configuration

# wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-11-01"

[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"

[vars]
APP_ENV = "production"

[[kv_namespaces]]
binding = "CACHE"
id = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

Secrets (API keys, tokens) are set via Wrangler and stored encrypted:

wrangler secret put API_SECRET

Secret values are available in env.API_SECRET at runtime.

Working with KV

KV is an eventually consistent key-value store — reads are globally distributed and fast, but writes propagate with up to 60 seconds of latency:

// Write
await env.CACHE.put('session:abc123', JSON.stringify(sessionData), {
  expirationTtl: 3600, // 1 hour
});

// Read
const raw = await env.CACHE.get('session:abc123');
const session = raw ? JSON.parse(raw) : null;

Working with D1 (SQLite)

D1 is Cloudflare's distributed SQLite database, optimized for read-heavy workloads:

// Query
const { results } = await env.DB
  .prepare('SELECT * FROM articles WHERE status = ? ORDER BY created_at DESC LIMIT ?')
  .bind('published', 20)
  .all();

// Batch writes
await env.DB.batch([
  env.DB.prepare('UPDATE articles SET views = views + 1 WHERE id = ?').bind(articleId),
  env.DB.prepare('INSERT INTO view_log (article_id, viewed_at) VALUES (?, ?)').bind(articleId, new Date().toISOString()),
]);

Local Development

Wrangler provides a local development server with simulated KV and D1:

wrangler dev

This runs your Worker locally at http://localhost:8787 with hot reloading. KV and D1 use local SQLite files by default.

Deploying

# Deploy to production
wrangler deploy

# Deploy to a specific environment
wrangler deploy --env staging

Wrangler handles bundling with esbuild and uploading the script to Cloudflare's network.

CI/CD with GitHub Actions

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test
      - name: Deploy to Cloudflare
        run: npx wrangler deploy
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

Limitations to Know

  • No Node.js built-in modules (use Web Platform APIs instead)
  • CPU time limit: 10ms per request on the free plan, 30s on paid
  • Memory limit: 128MB per isolate
  • Wasm modules must be imported as ES modules
  • No persistent in-memory state between requests (use KV or Durable Objects)

Conclusion

Cloudflare Workers is one of the most compelling serverless platforms available in 2026. The combination of zero cold starts, global distribution, integrated storage, and a generous free tier makes it particularly attractive for API backends, edge middleware, and JAMstack functions. Start with wrangler dev to iterate quickly locally, then deploy with a single command. The entire deployment pipeline — from local to global — is refreshingly simple.

Sign in to like, dislike, or report.

Comments

No comments yet. Be the first!

Sign in to leave a comment.