What Cloudflare Workers Actually Are
Cloudflare Workers are JavaScript functions that run at Cloudflare's edge network — meaning your code executes at a data centre physically close to your user, not on a server you manage. A request from Lagos hits a Lagos data centre. A request from London hits a London data centre. Response times under 50ms globally.
The free tier is genuinely useful:
| Plan | Requests | CPU Time | Price |
|---|---|---|---|
| Free | 100,000/day | 10ms/request | $0 |
| Paid | 10,000,000/month | 50ms/request | $5/month |
| Enterprise | Unlimited | Custom | Custom |
For most side projects and early-stage products, the free tier never runs out.

When to Use Workers vs Next.js API Routes
| Use Case | Best Choice | Why |
|---|---|---|
| API that needs global low latency | Workers | Edge execution worldwide |
| Full-stack app with React frontend | Next.js on Cloudflare Pages | Co-located with your UI |
| High-volume webhook receiver | Workers | Handles scale natively |
| Complex server logic with long runtime | Traditional server | Workers have CPU time limits |
| Image or asset transformation | Workers + R2 | Native Cloudflare integration |
| Simple REST API | Workers | Simplest, fastest deployment |
Step 1: Install Wrangler and Create Your First Worker
npm install -g wrangler
wrangler login
wrangler init my-api
cd my-api
This creates a project with:
my-api/
├── src/
│ └── index.js
├── wrangler.toml
└── package.json
Your First Worker
// src/index.js
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url)
const path = url.pathname
// Simple router
if (path === '/api/hello' && request.method === 'GET') {
return Response.json({ message: 'Hello from the edge!', region: request.cf?.country })
}
if (path === '/api/echo' && request.method === 'POST') {
const body = await request.json()
return Response.json({ received: body, timestamp: Date.now() })
}
return new Response('Not Found', { status: 404 })
}
},
Test it locally:
wrangler dev
# Open http://localhost:8787/api/hello
Step 2: Add a Database With Cloudflare D1
D1 is Cloudflare's edge SQLite database. It runs at the edge alongside your Worker.
Create the Database
wrangler d1 create my-database
Copy the database ID output and add it to wrangler.toml:
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id-here"
Create Your Schema
# Create a migration file
wrangler d1 execute my-database --local --command "
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id),
title TEXT NOT NULL,
content TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
"
Use D1 in Your Worker
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url)
const path = url.pathname
// GET /api/users — list all users
if (path === '/api/users' && request.method === 'GET') {
const { results } = await env.DB.prepare(
'SELECT id, email, name, created_at FROM users ORDER BY created_at DESC LIMIT 50'
).all()
return Response.json({ users: results })
}
// POST /api/users — create a user
if (path === '/api/users' && request.method === 'POST') {
const { email, name } = await request.json()
if (!email) {
return Response.json({ error: 'Email required' }, { status: 400 })
}
try {
const result = await env.DB.prepare(
'INSERT INTO users (email, name) VALUES (?, ?) RETURNING *'
).bind(email, name || null).first()
return Response.json({ user: result }, { status: 201 })
} catch (e) {
if (e.message.includes('UNIQUE constraint')) {
return Response.json({ error: 'Email already exists' }, { status: 409 })
}
throw e
}
}
return new Response('Not Found', { status: 404 })
}
},
Step 3: Add KV Storage for Caching
Cloudflare KV (Key-Value) is a globally replicated store. Perfect for caching expensive computations, storing session data, or rate limiting.
Add KV to wrangler.toml
[[kv_namespaces]]
binding = "CACHE"
id = "your-kv-namespace-id"
Create the namespace:
wrangler kv:namespace create CACHE
Cache Expensive API Calls
async function getCachedOrFetch(env, key, fetchFn, ttlSeconds = 300) {
// Try cache first
const cached = await env.CACHE.get(key, { type: 'json' })
if (cached) return cached
// Cache miss — fetch and store
const fresh = await fetchFn()
await env.CACHE.put(key, JSON.stringify(fresh), { expirationTtl: ttlSeconds })
return fresh
},
// Usage in your handler
const data = await getCachedOrFetch(
env,
'exchange-rates-usd',
() => fetch('https://api.exchangerate-api.com/v4/latest/USD').then(r => r.json()),
3600 // Cache for 1 hour
)
Step 4: Add Authentication
Workers don't have sessions, but they work perfectly with JWT tokens:
async function verifyToken(token, secret) {
// Simple JWT verification using WebCrypto API (available in Workers)
const parts = token.split('.')
if (parts.length !== 3) return null
try {
const payload = JSON.parse(atob(parts[1]))
if (payload.exp && payload.exp < Date.now() / 1000) return null
return payload
} catch {
return null
}
},
function requireAuth(handler) {
return async (request, env, ctx) => {
const authHeader = request.headers.get('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const token = authHeader.slice(7)
const payload = await verifyToken(token, env.JWT_SECRET)
if (!payload) {
return Response.json({ error: 'Invalid token' }, { status: 401 })
}
// Attach user to request context
request.user = payload
return handler(request, env, ctx)
}
},
// Protect a route
const protectedHandler = requireAuth(async (request, env) => {
return Response.json({ user: request.user, message: 'Authenticated!' })
})
Step 5: Deploy to Production
# Deploy to Cloudflare's edge network (300+ locations)
wrangler deploy
Your API is now live at https://my-api.your-username.workers.dev.
Add a Custom Domain
- In Cloudflare dashboard, go to Workers & Pages → your worker
- Click Triggers → Add Custom Domain
- Enter
api.yourdomain.com - Done — Cloudflare handles SSL automatically
Performance Comparison: Workers vs Traditional Servers
| Metric | Traditional Server (1 location) | Cloudflare Workers (300+ locations) |
|---|---|---|
| Latency from Lagos | 200–400ms to EU/US server | 15–40ms (Lagos edge) |
| Latency from London | 20–50ms | 10–20ms |
| Cold start | 100–500ms | ~0ms (always warm) |
| Scale to 1M requests | Needs server scaling | Automatic |
| Cost at 1M requests/month | $20–$100 server | $5 Workers Paid |
Common Gotchas
| Issue | Cause | Fix |
|---|---|---|
| `Cannot use Node.js modules` | Workers use Web APIs not Node.js | Use Web-compatible libraries |
| `CPU time limit exceeded` | Heavy computation | Move to a background job or use Durable Objects |
| `D1 returning empty results` | Local vs production DB mismatch | Run migrations on production: `wrangler d1 execute --remote` |
| `Environment variable undefined` | Secrets not set in Wrangler | Use `wrangler secret put MY_KEY` |

