system-design · intermediate

Stateful vs. Stateless Architecture — Managing Session State

The Central Question

Consider an e-commerce platform running on the CoreLab platform (corelab.com):


Where session state lives dictates whether a service can scale horizontally or crash under load.

This lesson answers one central question: How do stateful and stateless architectures handle client session context, what are the trade-offs of sticky sessions versus shared state stores (Redis/JWT), and how do engineers design stateless compute tiers for linear horizontal scaling?


Core Definitions: Stateful vs. Stateless Services

System components fall into two fundamental architectural models based on how they handle session context across consecutive requests:

flowchart TB
  subgraph Stateful Architecture: Memory Dependent
    Client1[Client A: Request 1] -->|State Saved in Local RAM| Node1[Server Node 1]
    Client1 -.->|Request 2 Routed to Node 2| Node2[Server Node 2]
    Node2 --x|Session Missing!| Fail[HTTP 401 Unauthorized]
  end

subgraph Stateless Architecture: Shared State Store
Client2[Client B: Request 1 (JWT Header)] --> Node3[Server Node 3]
Client2 -->|Request 2 (JWT Header)| Node4[Server Node 4]
Node3 & Node4 -->|Validate & Query Shared State| Redis[(Shared Redis / Database)]
end

Figure 1: Architectural comparison between local memory state dependencies and shared state store designs.

1. Stateful Service

A service is **stateful** if it retains client session context, temporary files, or execution state in its local node memory or local disk between HTTP requests.

2. Stateless Service

A service is **stateless** if it stores zero client session data locally. Every incoming request is completely self-contained, bearing all necessary authorization credentials and context.

The Stateful Trap: Sticky Sessions and Failed Rolling Deploys

To make a stateful application work behind a load balancer, teams implement Sticky Sessions (Session Affinity):

sequenceDiagram
    autonumber
    actor Client as User Browser
    participant LB as Load Balancer (Cookie Sticky)
    participant NodeA as App Server Node A (Stateful)
    participant NodeB as App Server Node B (Stateful)
    
    Client->>LB: 1. POST /login
    LB->>NodeA: Route to Node A
    NodeA->>NodeA: Save Session in Local RAM (NodeA_Memory)
    NodeA-->>LB: Set Cookie: SERVERID=NodeA
    LB-->>Client: HTTP 200 OK + Cookie
    
    Client->>LB: 2. GET /cart (Cookie: SERVERID=NodeA)
    LB->>NodeA: Pin Route strictly to Node A!
    NodeA-->>Client: HTTP 200 OK (Cart Found)
    
    Note over NodeA: Node A crashes or is terminated for rolling deployment!
    
    Client->>LB: 3. GET /cart (Cookie: SERVERID=NodeA)
    LB->>NodeB: Node A Dead! Re-route to Node B
    NodeB-->>Client: HTTP 401 Unauthorized (Cart Vanished!)

Figure 2: Sequence diagram illustrating how sticky sessions fail during rolling deployments or instance crashes.

Why Sticky Sessions Break Down at Scale

  1. Uneven Load Imbalance: High-traffic power users get pinned to Node A, driving Node A's CPU to 100% while Node B sits 95% idle.
  2. Broken Rolling Deployments: During a continuous deployment, terminating Node A to upgrade its container drops all local sessions pinned to Node A, logging out thousands of active users.
  3. In-effective Auto-Scaling: Adding 10 new nodes to an auto-scaling pool does not relieve load on existing nodes because existing users remain pinned to old nodes by sticky cookies.

The Stateless Solution: Offloading State to Shared Stores

Stateless services achieve scale by pushing state out of the compute tier into two primary target locations:

flowchart TD
  StateLoc[Where Does Session State Live?] --> ClientSide[1. Client-Side State: Signed JWTs]
  StateLoc --> SharedStore[2. Shared External Store: Redis Cluster]
  
  ClientSide --> CDesc["Cryptographically signed token carried in HTTP Authorization header.<br/>Validated 100% in RAM by any app node (Zero DB Seeks!)."]
  SharedStore --> SDesc["Centralized in-memory key-value cache (sub-ms lookups).<br/>Any app node queries Redis cluster over network."]

Figure 3: Taxonomies of stateless session offloading targets.

Pattern 1: Client-Side State via JSON Web Tokens (JWT)

The server signs a cryptographic JWT payload containing user claims (`user_id`, `roles`, `exp`) and returns it to the client. On every request, the client presents the token in the `Authorization: Bearer ` header. Any app node verifies the HMAC/RSA signature in memory ($< 0.01\text{ms}$) without database lookups.

Pattern 2: Centralized Shared State Store (Redis Cluster)

The application server generates a random session UUID (`sess_9021`) and writes session data to a high-availability **Redis Cluster**. The client stores only the `sess_9021` UUID cookie. When a request hits any app node, the node fetches session data from Redis in sub-milliseconds.

Hybrid Stateful Topologies: WebSockets and Local Caches

While REST APIs are ideally 100% stateless, certain real-time workloads (such as WebSockets, multiplayer gaming, or live financial trading streams) require maintaining open long-lived TCP socket connections on specific server instances. In these **Hybrid Stateful Topologies**, application engineers isolate stateful TCP connection nodes from the stateless business logic tier. A lightweight WebSocket gateway holds the open client socket connections, while routing incoming messages via background pub/sub channels (such as Redis Pub/Sub or Apache Kafka) to a stateless pool of worker microservices. This decouples socket connection management from business processing, preserving horizontal auto-scaling across the core API application tier.

Architectural Trade-off Matrix

Vector / DimensionStateful ArchitectureStateless + JWTStateless + Shared Redis
Horizontal ScalingHard (Requires Sticky Sessions)Linear & InstantLinear (Constrained by Redis QPS)
Node Failover ImpactSevere (Lost local sessions)Zero ImpactZero Impact
Session InvalidationImmediate (Delete local RAM key)Complex (Requires token blacklist)Immediate (Delete Redis key)
Request LatencyUltra-Fast (Local RAM lookup)Ultra-Fast (In-memory crypto check)Fast (Sub-ms network hop to Redis)
Network BandwidthMinimal (Small cookie header)Higher (JWT token payload in header)Minimal (Small session UUID cookie)
Operational ComplexityLow initially, High at scaleLowModerate (Requires Redis cluster management)

Complete Worked Example: Go Stateless API Server with Redis Session Store

Let me show you a complete, production-ready Go HTTP service for the CoreLab platform (corelab.com) that implements stateless session checking against a shared Redis cluster.

package main

import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"

"github.com/redis/go-redis/v9"
)

type SessionData struct {
UserID string json:&quot;user_id&quot;
Email string json:&quot;email&quot;
Role string json:&quot;role&quot;
CreatedAt time.Time json:&quot;created_at&quot;
}

type StatelessSessionManager struct {
redisClient *redis.Client
}

func NewSessionManager(redisAddr string) *StatelessSessionManager {
rdb := redis.NewClient(&redis.Options{
Addr: redisAddr,
Password: "", // No password for dev
DB: 0,
})
return &StatelessSessionManager{redisClient: rdb}
}

// ServeHTTP handles requests statelessly across any node in the pool
func (m StatelessSessionManager) AuthenticateMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r
http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()

sessionID := r.Header.Get("X-Session-ID")
if sessionID == "" {
http.Error(w, "Missing X-Session-ID Header", http.StatusUnauthorized)
return
}

// Fetch session state from SHARED Redis Store (Any node can query this!)
val, err := m.redisClient.Get(ctx, "session:"+sessionID).Result()
if err == redis.Nil {
http.Error(w, "Session Expired or Invalid", http.StatusUnauthorized)
return
} else if err != nil {
http.Error(w, "Shared Session Store Error", http.StatusInternalServerError)
return
}

var session SessionData
json.Unmarshal([]byte(val), &session)

// Inject stateless session into context for downstream business logic
reqWithCtx := r.WithContext(context.WithValue(r.Context(), "user_session", session))
next.ServeHTTP(w, reqWithCtx)
})
}


Failure Modes and Engineering Mitigations

Failure ScenarioRoot CauseSystem SymptomDetection SignalMitigation Strategy
1. Stateful Session Loss on DeployApp server holds sessions in local RAM; container is terminated during rolling update.Thousands of active users logged out simultaneously during deployment.Spikes in HTTP 401 Unauthorized and support complaints post-deploy.Migrate session storage to a shared Redis cluster or signed JWT tokens.
2. Sticky Session Traffic ImbalanceLoad balancer pins users by IP/cookie; 3 power users saturate Node A's CPU.Node A hits 100% CPU while Nodes B, C, D sit 90% idle.High CPU load variance across target group instances.Eliminate sticky session affinity; adopt stateless app nodes.
3. Un-bounded JWT Payload BloatStoring massive user profile objects inside the JWT token string.HTTP headers exceed 16 KB; Nginx proxies reject requests with HTTP 431.Spikes in HTTP 431 Request Header Fields Too Large errors.Keep JWT payloads minimal (user_id, role, exp); store large objects in Redis/DB.
4. In-ability to Revoke Stolen JWTUser signs out or account is compromised, but signed JWT remains valid until expiration.Compromised token continues executing API actions until exp timestamp.Security audit flags delayed account revocation response times.Implement a Redis JWT revocation blacklist or use short-lived JWTs (15 mins) with refresh tokens.

What You Should Remember

  1. Stateless compute scales linearly: Stateless services allow any node in an auto-scaling pool to handle any request, enabling seamless horizontal scaling.
  2. Avoid Sticky Sessions at scale: Sticky sessions cause load imbalances and break rolling deployments when nodes are terminated.
  3. Offload state to shared stores: Move session state out of application RAM into centralized Redis clusters or signed JWT tokens.
  4. JWTs enable zero-DB auth: Cryptographic JWT signatures allow app nodes to authenticate requests in RAM without database or cache lookups.
  5. Manage stateful stores carefully: Databases, message queues, and caches are stateful; isolate their complexity away from the stateless application tier.

Glossary of Terms

TermDefinition
Stateful ServiceA service that retains client session context in local memory or local disk across consecutive HTTP requests.
Stateless ServiceA service that stores zero client session context locally, handling every request self-containedly.
Sticky Sessions (Session Affinity)A load balancer routing technique that pins requests from a specific client to the same backend server node.
Shared State StoreA centralized, high-availability data store (e.g. Redis Cluster) used by stateless app nodes to share session state.
JSON Web Token (JWT)An open standard (RFC 7519) defining a compact, cryptographically signed token for sharing claims statelessly.
Rolling DeploymentA deployment strategy where server instances are updated incrementally without bringing down the entire service pool.

Practice Scenario and Self-Assessment

Architecture Scenario

You are modernizing a legacy monolithic banking application (`corelab.com`):
  • Currently, 4 Tomcat servers store user shopping carts and active sessions in local JVM heap RAM.
  • When an administrator initiates a software update on Node 1, 25% of active users are disconnected and lose their shopping cart items.
**Questions**:
  1. Explain why local JVM heap session storage prevents the engineering team from deploying continuous zero-downtime updates.
  2. Design a stateless migration strategy using Redis and JWTs to enable zero-downtime rolling deployments.

Interactive Self-Assessment

New auto-scaled instances lack the local memory session state of existing users, causing auth failures unless sticky sessions pin users to old nodes.

Local RAM lookups are too slow compared to network database calls.

Operating system kernels forbid allocating RAM to auto-scaled container nodes.

Network load balancers are physically incapable of routing HTTP headers.

Application nodes verify the token signature 100% in RAM, eliminating database reads on every API call.

JWT tokens automatically encrypt all payload data so it cannot be read in transit.

JWT tokens never expire and cannot be stolen by malicious actors.

JWT tokens convert relational SQL databases into document stores automatically.


What to Learn Next

Track: Software Design and Architecture

Previous: Splitting a Monolith Safely (Strangler Fig)

Next: Sync vs Async Communication — Wait or Don’t Wait

By Shubham Jain

All articles · Study paths

Shubham Jain · Learning Lab