Edge Computing with Cloudflare Workers: Code at the CDN Layer

I have been running production Cloudflare Workers for about six months now, and the experience has fundamentally changed how I think about backend architecture. The core idea is simple: instead of running your code in a single data center (or a handful of regions), it runs at the CDN layer, across 300+ locations worldwide. When a user in Tokyo makes a request, their code executes in Tokyo. When a user in Sao Paulo makes a request, it executes in Sao Paulo. No routing to us-east-1, no cross-ocean latency.
V8 Isolates: The Secret Sauce
The technology that makes this possible is V8 isolates. Traditional serverless platforms like AWS Lambda spin up containers for each function invocation. A cold start on Lambda can take anywhere from 100ms to several seconds depending on runtime and package size. Cloudflare Workers do not use containers. They use V8 isolates, which are the same lightweight execution environments that Chrome uses to isolate tabs from each other.
A V8 isolate starts in under 5 milliseconds. There is no operating system to boot, no runtime to initialize, no container to provision. The isolate shares the V8 engine with other isolates on the same machine, but each one has its own memory space and cannot access the others. The security model is similar to how your browser prevents one tab from reading another tab's data.
The practical result is near-zero cold start times. Your Worker is always warm because spinning up a new isolate is essentially free. This is a completely different experience from Lambda, where cold starts can dominate your P99 latency.
Writing Your First Worker
The Wrangler CLI is the primary tool for developing, testing, and deploying Workers. Setup is straightforward:
npm create cloudflare@latest my-worker
cd my-worker
A Worker is essentially a fetch event handler. It intercepts HTTP requests and returns responses. The API is based on the standard Web Workers API and the Fetch API, so if you know how to use Request and Response objects in the browser, you already know the fundamentals.
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname === '/api/hello') {
return new Response(JSON.stringify({ message: 'Hello from the edge' }), {
headers: { 'Content-Type': 'application/json' },
});
}
if (url.pathname === '/api/time') {
return new Response(JSON.stringify({
time: new Date().toISOString(),
location: request.cf?.colo, // The data center serving this request
}), {
headers: { 'Content-Type': 'application/json' },
});
}
return new Response('Not Found', { status: 404 });
},
};
The request.cf object is Cloudflare-specific and contains information about the request, including which data center (colo) is handling it, the client's country, city, timezone, and ASN. This metadata is incredibly useful for geolocation-based logic without needing a third-party GeoIP service.
KV: Global Key-Value Storage
Workers KV is a globally distributed key-value store. You write data, and it replicates across all of Cloudflare's edge locations. Reads are fast (single-digit milliseconds) because the data is served from the nearest edge location. Writes are eventually consistent, typically propagating globally within 60 seconds.
// wrangler.toml
[[kv_namespaces]]
binding = "CACHE"
id = "abc123"
// Worker code
export default {
async fetch(request, env) {
const url = new URL(request.url);
const cacheKey = 'page:' + url.pathname;
// Try cache first
const cached = await env.CACHE.get(cacheKey);
if (cached) {
return new Response(cached, {
headers: { 'Content-Type': 'text/html', 'X-Cache': 'HIT' },
});
}
// Fetch from origin
const response = await fetch('https://origin.example.com' + url.pathname);
const html = await response.text();
// Cache for 1 hour
await env.CACHE.put(cacheKey, html, { expirationTtl: 3600 });
return new Response(html, {
headers: { 'Content-Type': 'text/html', 'X-Cache': 'MISS' },
});
},
};
KV is perfect for caching, configuration, feature flags, and any read-heavy workload. The eventual consistency model means it is not suitable for data that needs strong consistency (like a counter that multiple users are incrementing simultaneously). For that, you need Durable Objects.
Durable Objects: Stateful Edge Computing
Durable Objects solve the hardest problem in edge computing: state. A Durable Object is a single instance that lives in one location. All requests to the same Durable Object are routed to that single instance, giving you strong consistency guarantees. Think of it as a tiny single-threaded server that handles requests sequentially.
export class RateLimiter {
constructor(state, env) {
this.state = state;
this.requests = [];
}
async fetch(request) {
const now = Date.now();
// Remove requests older than 60 seconds
this.requests = this.requests.filter(t => now - t < 60000);
if (this.requests.length >= 100) {
return new Response('Rate limit exceeded', { status: 429 });
}
this.requests.push(now);
return new Response('OK');
}
}
Durable Objects have persistent storage via a transactional key-value API. They are perfect for rate limiters, counters, WebSocket coordination, collaborative editing, and any workload where you need consistency for a specific entity. The trade-off is that requests route to a single location (where the Durable Object lives) rather than executing at the nearest edge, so you lose some of the latency benefits for that specific request.
R2: Object Storage Without Egress Fees
R2 is Cloudflare's object storage service, compatible with the S3 API. The headline feature is zero egress fees. AWS charges you for every byte that leaves S3. R2 does not. For applications that serve a lot of static assets, user uploads, or media files, the savings can be substantial.
export default {
async fetch(request, env) {
const url = new URL(request.url);
const key = url.pathname.slice(1); // Remove leading slash
if (request.method === 'GET') {
const object = await env.BUCKET.get(key);
if (!object) return new Response('Not Found', { status: 404 });
return new Response(object.body, {
headers: { 'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream' },
});
}
if (request.method === 'PUT') {
await env.BUCKET.put(key, request.body, {
httpMetadata: { contentType: request.headers.get('Content-Type') || 'application/octet-stream' },
});
return new Response('Created', { status: 201 });
}
return new Response('Method Not Allowed', { status: 405 });
},
};
A Practical Example: A/B Testing at the Edge
One of my favorite use cases for Workers is A/B testing. Instead of loading a JavaScript library on the client and dealing with flash-of-original-content, you make the variant decision at the edge before the page even loads.
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Only apply A/B testing to the homepage
if (url.pathname !== '/') {
return fetch(request);
}
// Check for existing assignment
const cookie = request.headers.get('Cookie') || '';
let variant = cookie.match(/ab_variant=(A|B)/)?.[1];
// Assign new users to a variant
if (!variant) {
variant = Math.random() < 0.5 ? 'A' : 'B';
}
// Rewrite to the variant page
url.pathname = variant === 'A' ? '/home-a' : '/home-b';
const response = await fetch(url.toString(), request);
// Clone response to modify headers
const newResponse = new Response(response.body, response);
newResponse.headers.set('Set-Cookie', 'ab_variant=' + variant + '; Path=/; Max-Age=2592000');
return newResponse;
},
};
The variant assignment happens before the origin server even receives the request. The user gets the correct variant page on the first request with zero client-side JavaScript overhead. The assignment is sticky via a cookie. This pattern works for feature flags, canary deployments, and geographic content routing too.
Latency Comparison
To illustrate the latency difference, consider a user in Sydney, Australia making an API request. With a traditional Lambda function in us-east-1, the request travels roughly 16,000 km each way. Round-trip latency is around 200 to 300ms just for the network hop, before any computation. With a Cloudflare Worker, the request is handled in Sydney. Network latency is under 5ms. The compute itself is fast because there is no cold start.
For a simple API proxy or edge logic, you are looking at total response times under 20ms compared to 300ms or more. That is a dramatic difference, and users notice it. Pages feel instant when API calls resolve in 20ms instead of 300ms.
When NOT to Use Edge Computing
Edge computing is not a silver bullet, and I want to be honest about the limitations.
- CPU limits: Workers have a 10ms CPU time limit on the free plan, 30ms on paid. This rules out heavy computation, image processing, and complex data transformations.
- No native database access: You cannot open a TCP connection to PostgreSQL or MySQL from a Worker. You need an HTTP-based database proxy (like Planetscale's HTTP API or Neon's serverless driver) or use Workers' own storage primitives.
- Limited runtime: The Workers runtime is not Node.js. Many npm packages that depend on Node.js APIs (fs, net, crypto internals) will not work. The runtime supports Web APIs, not Node APIs.
- Eventually consistent storage: KV is eventually consistent. If your use case requires strong consistency on reads after writes, KV alone will not work.
The sweet spot for Workers is request routing, API proxying, authentication checks, A/B testing, caching, header manipulation, redirects, and lightweight API endpoints. For heavy computation or complex database operations, a traditional server or serverless function in a specific region is still the right choice. The best architectures combine both: edge Workers for the fast path and regional servers for heavy lifting.