ND.
All Articles
February 8, 20246 min read

State is the Enemy of Scale: Pragmatic Stateless Architecture

How pushing state exclusively to Postgres and Redis allowed us to scale horizontally with zero downtime.

ArchitecturePostgreSQLRedisKubernetes
State is the Enemy of Scale: Pragmatic Stateless Architecture

What "Stateless" Actually Means

A stateless service does not store any persistent data about a client session in local memory. Every request is self-contained — the service reads state from an external source (a database, a cache), performs work, and returns a response. It does not remember the previous request.

This sounds obvious, but it's violated constantly in practice. Common violations include:

  • In-memory session stores (req.session in Express backed by MemoryStore)
  • Local file caches or temp directories that accumulate state
  • Websocket connection maps stored per-instance without a shared pub/sub
  • Why Statefulness Kills Horizontal Scale

    Suppose you have 3 instances of a service and a user's session is stored in memory on Instance 1. If you route their next request to Instance 2, their session is gone. The two common "fixes" are sticky sessions or session replication — both are architectural debt that compounds over time.

    Sticky sessions defeat the purpose of horizontal scaling. If one instance dies, every user pinned to it loses their session.

    Session replication is expensive network overhead that grows quadratically with instance count.

    The Pure Stateless Pattern

    Push all state external. Full stop.

    // ❌ Stateful: breaks horizontal scaling
    const sessions = new Map<string, SessionData>();
    
    app.get('/dashboard', (req, res) => {
      const session = sessions.get(req.headers['session-id']);
      if (!session) return res.status(401).send();
      res.json(session.user);
    });
    
    // ✅ Stateless: any instance can handle any request
    app.get('/dashboard', async (req, res) => {
      const sessionId = req.headers['session-id'] as string;
      const session = await redis.get(`session:${sessionId}`);
      if (!session) return res.status(401).send();
      res.json(JSON.parse(session).user);
    });

    What We Put Where

    Data typeStorageWhy
    Auth sessionsRedis (TTL)Fast lookup, expiry is free
    User/account dataPostgreSQLACID guarantees, long-lived
    Computed aggregatesRedisExpensive to recompute per-request
    Messages, eventsPostgreSQL + KafkaDurable, replayable

    The Deployment Benefit

    With fully stateless services, a deployment is trivial. Kubernetes can terminate old pods and start new ones in any order. Traffic shifts to new pods immediately. Zero downtime, zero session loss, zero user impact. We went from deployments requiring 2am maintenance windows to fully automated continuous delivery with 0 complaints.