Introduction: What are Cloudflare Workers and Why They Matter
Cloudflare Workers are serverless JavaScript functions that run at the edge of Cloudflare’s global network across 300+ locations worldwide. They allow developers to build applications that respond to HTTP requests without managing servers. Cloudflare Workers matter because they provide ultra-low latency, high reliability, and automatic scaling while reducing costs and complexity. By executing code closer to your users, Workers deliver faster user experiences through Cloudflare’s edge computing platform.
Core Concepts and Fundamentals
| Concept | Description |
|---|---|
| Edge Computing | Code execution close to users at Cloudflare’s data centers worldwide |
| Serverless | No server management or provisioning needed |
| V8 Isolates | Lightweight JavaScript execution environments with microsecond cold starts |
| Workers KV | Global, low-latency key-value data store for Workers |
| Durable Objects | Globally unique objects providing coordination and consistent storage |
| R2 | Object storage service compatible with S3 API |
| D1 | Serverless SQLite database for Workers |
| Queues | Managed message queue system for asynchronous processing |
| Workers Triggers | Event-based execution (HTTP, Cron, etc.) |
Getting Started with Cloudflare Workers
Prerequisites
- Cloudflare account
- Node.js installed
- Wrangler CLI installed (
npm install -g wrangler)
Basic Setup Process
Install Wrangler and authenticate
npm install -g wrangler wrangler loginCreate a new Worker project
wrangler init my-worker cd my-workerDevelop and test locally
wrangler devDeploy to Cloudflare
wrangler publish
Writing Your First Worker
Simple Response Worker
export default {
async fetch(request, env, ctx) {
return new Response("Hello World!");
}
};
Handling Different HTTP Methods
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
switch (request.method) {
case "GET":
return handleGet(url);
case "POST":
return handlePost(request);
default:
return new Response("Method not allowed", { status: 405 });
}
}
};
async function handleGet(url) {
// Handle GET logic
return new Response("GET request received");
}
async function handlePost(request) {
// Handle POST logic
const data = await request.json();
return new Response(JSON.stringify({ received: data }), {
headers: { "Content-Type": "application/json" }
});
}
Worker Configuration
wrangler.toml Configuration
name = "my-worker"
main = "src/index.js"
compatibility_date = "2023-10-30"
# Environment variables
[vars]
API_KEY = "your-api-key-for-dev"
# Production environment
[env.production]
vars = { API_KEY = "your-api-key-for-prod" }
# Route binding
[triggers]
routes = [
{ pattern = "example.com/api/*", zone_name = "example.com" }
]
# KV Namespace binding
[[kv_namespaces]]
binding = "MY_KV"
id = "xxxx"
# R2 Bucket binding
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-bucket"
# D1 Database binding
[[d1_databases]]
binding = "MY_DB"
database_name = "my-database"
database_id = "xxxx"
Environment Variables and Secrets
# Set environment variable
wrangler secret put API_KEY
# Access in code
export default {
async fetch(request, env, ctx) {
const apiKey = env.API_KEY;
// Use apiKey
}
};
Common Patterns and Examples
Modifying Requests and Responses
export default {
async fetch(request, env, ctx) {
// Clone the request to modify it
const modifiedRequest = new Request(request);
modifiedRequest.headers.set("X-Custom-Header", "custom-value");
// Forward to origin
const response = await fetch(modifiedRequest);
// Clone the response to modify it
const modifiedResponse = new Response(response.body, response);
modifiedResponse.headers.set("X-Served-By", "Cloudflare-Workers");
return modifiedResponse;
}
};
Caching Responses
export default {
async fetch(request, env, ctx) {
const cacheKey = new Request(request.url, request);
const cache = caches.default;
// Check if the request is in the cache
let response = await cache.match(cacheKey);
if (!response) {
// If not in cache, fetch from origin
response = await fetch(request);
// Clone the response for caching
const responseToCache = new Response(response.body, response);
// Set cache control headers
responseToCache.headers.set('Cache-Control', 'max-age=3600');
// Store response in cache
ctx.waitUntil(cache.put(cacheKey, responseToCache));
}
return response;
}
};
API Gateway / Proxy
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const apiBase = 'https://api.example.com';
const apiPath = url.pathname.replace('/api', '');
// Create a new request to the API
const apiRequest = new Request(`${apiBase}${apiPath}`, {
method: request.method,
headers: request.headers,
body: request.body,
});
// Add API key
apiRequest.headers.set('Authorization', `Bearer ${env.API_KEY}`);
try {
return await fetch(apiRequest);
} catch (err) {
return new Response(`API Error: ${err.message}`, { status: 500 });
}
}
};
Working with Data Services
Workers KV (Key-Value Store)
// Reading data
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const key = url.pathname.split('/').pop();
// Read from KV
const value = await env.MY_KV.get(key);
if (value === null) {
return new Response('Not found', { status: 404 });
}
return new Response(value);
}
};
// Writing data
export default {
async fetch(request, env, ctx) {
if (request.method === 'POST') {
const key = new URL(request.url).pathname.split('/').pop();
const value = await request.text();
// Write to KV (with optional expiration in seconds)
await env.MY_KV.put(key, value, { expirationTtl: 86400 });
return new Response('Stored successfully');
}
return new Response('Method not allowed', { status: 405 });
}
};
Durable Objects
// Durable Object class
export class Counter {
constructor(state) {
this.state = state;
}
async fetch(request) {
// Get current count
let count = await this.state.storage.get("count") || 0;
// Update count based on request
if (request.method === "POST") {
count++;
await this.state.storage.put("count", count);
}
return new Response(count.toString());
}
}
// Worker that uses the Durable Object
export default {
async fetch(request, env, ctx) {
// Create Durable Object ID based on a name
const id = env.COUNTER.idFromName("my-counter");
// Get Durable Object stub using the ID
const obj = env.COUNTER.get(id);
// Forward the request to the Durable Object
return await obj.fetch(request);
}
};
// In wrangler.toml:
// [[durable_objects]]
// bindings = [{ name = "COUNTER", class_name = "Counter" }]
R2 Storage
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const objectKey = url.pathname.replace('/storage/', '');
if (request.method === 'GET') {
// Get object from R2
const object = await env.MY_BUCKET.get(objectKey);
if (object === null) {
return new Response('Object Not Found', { status: 404 });
}
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('etag', object.httpEtag);
return new Response(object.body, {
headers
});
}
if (request.method === 'PUT') {
// Store object in R2
await env.MY_BUCKET.put(objectKey, request.body, {
httpMetadata: request.headers
});
return new Response('Object uploaded successfully');
}
return new Response('Method not allowed', { status: 405 });
}
};
D1 Database
export default {
async fetch(request, env, ctx) {
// Create table
if (request.url.includes('/setup')) {
await env.MY_DB.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
email TEXT
)
`);
return new Response('Table created');
}
// Insert data
if (request.method === 'POST') {
const { name, email } = await request.json();
const stmt = env.MY_DB.prepare(
'INSERT INTO users (name, email) VALUES (?, ?)'
);
const result = await stmt.bind(name, email).run();
return new Response(JSON.stringify({
success: true,
id: result.meta.last_row_id
}), {
headers: { 'Content-Type': 'application/json' }
});
}
// Query data
if (request.method === 'GET') {
const { results } = await env.MY_DB.prepare(
'SELECT * FROM users'
).all();
return new Response(JSON.stringify(results), {
headers: { 'Content-Type': 'application/json' }
});
}
return new Response('Method not allowed', { status: 405 });
}
};
Workers for Advanced Use Cases
HTML Rewriting
export default {
async fetch(request, env, ctx) {
const response = await fetch(request);
// Check if HTML
const contentType = response.headers.get('content-type') || '';
if (!contentType.includes('text/html')) {
return response;
}
// Create HTML rewriter
return new HTMLRewriter()
.on('title', {
element(element) {
element.setInnerContent('Modified Title');
}
})
.on('a', {
element(element) {
// Add rel="noopener" to all links
element.setAttribute('rel', 'noopener');
}
})
.on('div#hero', {
element(element) {
element.setAttribute('class', 'enhanced-hero');
},
text(text) {
// Replace text content
if (text.text.includes('Original Text')) {
text.replace('New Text');
}
}
})
.transform(response);
}
};
Scheduled Tasks (Cron Triggers)
// Worker executed on a schedule
export default {
// Standard request handler
async fetch(request, env, ctx) {
return new Response('Hello from scheduled worker');
},
// Scheduled handler
async scheduled(event, env, ctx) {
// This runs according to the cron schedule
console.log('Running scheduled task', event.cron);
// Perform scheduled operations
await performCleanup(env);
// Make API calls
const response = await fetch('https://api.example.com/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ timestamp: Date.now() })
});
console.log('Task completed with status:', response.status);
}
};
async function performCleanup(env) {
// Implementation of cleanup task
}
// In wrangler.toml:
// [triggers]
// crons = ["0 0 * * *"] # Daily at midnight UTC
Queues
// Producer Worker
export default {
async fetch(request, env, ctx) {
if (request.method === 'POST') {
const message = await request.json();
// Send message to queue
await env.MY_QUEUE.send(message);
return new Response('Message queued');
}
return new Response('Method not allowed', { status: 405 });
}
};
// Consumer Worker
export default {
async queue(batch, env, ctx) {
for (const message of batch.messages) {
try {
// Process message
console.log('Processing message:', message.body);
// Perform work with the message
await processMessage(message.body);
// If processing succeeded, the message will be deleted from the queue
} catch (error) {
// If an error is thrown, the message will be retried
console.error('Error processing message:', error);
throw error;
}
}
}
};
async function processMessage(data) {
// Message processing logic
}
// In wrangler.toml:
// [[queues.consumers]]
// queue = "my-queue"
// max_batch_size = 10
// max_batch_timeout = 5
// max_retries = 3
Security Best Practices
Authentication and Authorization
export default {
async fetch(request, env, ctx) {
// Check for authentication token
const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return new Response('Unauthorized', { status: 401 });
}
const token = authHeader.split(' ')[1];
// Validate token (example)
if (!isValidToken(token, env.API_SECRET)) {
return new Response('Forbidden', { status: 403 });
}
// Continue with authorized request
return handleAuthorizedRequest(request, env);
}
};
function isValidToken(token, secret) {
// Token validation logic
// In production, use proper JWT validation or other secure method
return token === 'valid-token'; // Example only
}
async function handleAuthorizedRequest(request, env) {
// Process authorized request
return new Response('Authorized content');
}
CORS Configuration
export default {
async fetch(request, env, ctx) {
// Handle CORS preflight requests
if (request.method === 'OPTIONS') {
return handleCors(request, true);
}
// Handle actual request
const response = await handleRequest(request, env);
// Add CORS headers to response
return handleCors(request, false, response);
}
};
function handleCors(request, isPreflight, response) {
const origin = request.headers.get('Origin');
const allowedOrigins = ['https://example.com', 'https://sub.example.com'];
// Check if origin is allowed
const corsHeaders = {
'Access-Control-Allow-Origin': allowedOrigins.includes(origin) ? origin : 'https://example.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400', // 24 hours
};
// Handle preflight request
if (isPreflight) {
return new Response(null, {
status: 204,
headers: corsHeaders,
});
}
// Add CORS headers to actual response
const newResponse = new Response(response.body, response);
Object.entries(corsHeaders).forEach(([key, value]) => {
newResponse.headers.set(key, value);
});
return newResponse;
}
async function handleRequest(request, env) {
// Actual request handling logic
return new Response('Response data');
}
Content Security Policies
export default {
async fetch(request, env, ctx) {
const response = await fetch(request);
// Add security headers
const secureResponse = new Response(response.body, response);
secureResponse.headers.set('Content-Security-Policy', "default-src 'self'; script-src 'self' https://trusted-scripts.com;");
secureResponse.headers.set('X-Content-Type-Options', 'nosniff');
secureResponse.headers.set('X-Frame-Options', 'DENY');
secureResponse.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
secureResponse.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
secureResponse.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
return secureResponse;
}
};
Common Challenges and Solutions
| Challenge | Solution |
|---|---|
| Cold start performance | Cloudflare Workers have microsecond cold starts due to V8 isolates; generally not an issue |
| CPU-intensive tasks | Split work into smaller chunks; use Queues for async processing; stay within CPU limits |
| Large responses | Stream large responses; use R2 for static content; implement chunked processing |
| Rate limiting | Implement token bucket algorithm with Workers KV or Durable Objects |
| Debugging | Use console.log(), wrangler tail for logs, and local dev environment with wrangler dev |
| Cross-origin requests | Implement proper CORS headers; use Workers to proxy API requests |
| Global state management | Use Durable Objects for global coordination; KV for shared read-mostly data |
| Database connections | Use D1 for SQL needs; design for serverless patterns (connection pooling doesn’t apply) |
Performance Optimization Tips
Worker Optimization
- Keep dependencies minimal
- Avoid large libraries that increase bundle size
- Use native Web APIs when possible
- Leverage streaming for large responses
- Use
waitUntil()for non-critical background tasks - Cache expensive computations or API responses
KV Optimization
- Use KV for read-heavy workloads
- Batch operations when possible
- Structure keys efficiently for list operations
- Be aware of KV consistency model (eventual consistency)
- Use cache API for ultra-low latency needs
Request/Response Handling
export default {
async fetch(request, env, ctx) {
// Example: Stream a large response
return streamLargeResponse(request, env);
}
};
async function streamLargeResponse(request, env) {
const { readable, writable } = new TransformStream();
ctx.waitUntil((async () => {
const writer = writable.getWriter();
try {
// Get data in chunks and write to stream
const data = await getDataInChunks(env);
for (const chunk of data) {
await writer.write(chunk);
}
} finally {
await writer.close();
}
})());
return new Response(readable, {
headers: { 'Content-Type': 'application/json' }
});
}
async function getDataInChunks(env) {
// Implementation to get data in chunks
// Could be from R2, external API, etc.
}
Deployment and CI/CD
GitHub Actions Integration
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Publish
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
command: publish
Testing Before Deployment
# Run tests
npm test
# Test in local development mode
wrangler dev
# Test in preview mode (deploys to workers.dev subdomain)
wrangler publish --env staging
Resources for Further Learning
Official Documentation
Community Resources
- Cloudflare Developer Discord
- Workers GitHub repository
- Cloudflare Community forums
Wrangler CLI Command Reference
# Create new project
wrangler init my-worker
# Start local development
wrangler dev
# Deploy to production
wrangler publish
# Deploy to specific environment
wrangler publish --env production
# View logs
wrangler tail
# Generate types for bindings
wrangler types
# Secret management
wrangler secret put MY_SECRET
wrangler secret delete MY_SECRET
# KV operations
wrangler kv:namespace create "MY_NAMESPACE"
wrangler kv:key put --namespace-id=xxx "key" "value"
wrangler kv:key get --namespace-id=xxx "key"
# R2 operations
wrangler r2 bucket create my-bucket
# D1 operations
wrangler d1 create my-database
wrangler d1 execute my-database --command="CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"
This cheatsheet provides a comprehensive overview of Cloudflare Workers, covering everything from basic concepts to advanced techniques, with practical code examples and best practices for both beginners and experienced developers.
