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.