Deploying to Cloudflare Workers: A Developer's Guide
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.