Serverless at the edge: architecture we wish we'd used sooner
When we launched Workers in 2019, most teams treated it as "serverless, but closer to the user." That framing undersells what's different. After a few hundred billion requests, here's the architecture advice we keep giving.
Workers are not a smaller Lambda
The obvious difference is runtime: a Worker starts in single-digit milliseconds because it runs on the same node that already terminated the TLS connection. The less obvious difference is the compute model. Workers get a fresh isolate per request, a strict execution budget, and no persistent local process state. Code that assumes a long-lived process will fight the platform.
What transfers cleanly from a Lambda mindset: stateless request handlers, environment-based configuration, and treating storage as an external dependency. What doesn't: connection pooling to your own database, long-lived caches in memory, and anything that expects warm shared state.
Stateless handlers, stateful storage
The pattern that scales on the edge is simple: the handler stays pure, and every bit of state lives in a storage primitive designed for the edge. That means Edge KV for keys, queues for work, and Workers Durable Storage for strong-consistency state.
// the shape we recommend — thin handler, state behind bindings
export default {
async fetch(request, env) {
const cart = await env.STORE.get(`cart:${userId(request)}`);
if (!cart) return new Response(null, { status: 404 });
return new Response(cart, {
headers: { "Cache-Control": "private, no-store" },
});
},
};
Put the cache in front, not behind
The biggest win we see repeatedly: teams move caching from inside the application to the edge. A Worker with Cache-Control: public and a cache key per device type answers most of the traffic that used to reach compute at all:
gitflare cache keys add example.com --include device.type # → /products answered from cache for 90%+ of traffic, # Worker only runs for cache misses
This is why we describe the edge as "three layers": routing and TLS, caching, and compute. Each layer absorbs a share of traffic; compute should be the last one touched.
Queues for anything slow
Workers have a wall-clock budget per request. Anything that can't finish inside it belongs in a queue. Since v1.34, Workers Queues are first-class: at-least-once delivery, configurable retries, and dead-letter namespaces.
export default {
async queue(batch, env) {
for (const msg of batch.messages) {
try {
await deliverWebhook(msg.body);
msg.ack();
} catch (err) {
msg.retry({ delayMs: 5000 });
}
}
},
};
If a webhook receiver is down, messages retry in the background without ever blocking a request path. That's the queue's whole job: decouple latency.
What we'd do differently now
Our early adopters — ourselves included — made two recurring mistakes. First, over-fragmenting cache keys, which drove hit ratios down. Second, treating storage as an afterthought, which forced rewrites later. Both are avoidable by deciding up front which requests are cache-first and which are compute-first.
If you're starting out, the quickstart scaffolds a well-shaped project. For deeper reading, our API reference documents the storage and queue bindings in full.