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):
- Architecture A (Stateful Memory): When a user logs in, the application server creates a session object in its local RAM (
server_memory["session_8901"] = { user_id: 42 }). When the user clicks "Add to Cart", the HTTP load balancer routes the request to Server Node 2. Because Node 2's RAM does not contain the session object created on Node 1, the user is abruptly logged out and their shopping cart vanishes. - Architecture B (Stateless Compute + Shared Store): When a user logs in, the application signs a cryptographic JWT token containing
{ user_id: 42 }or writes the session object to a shared Redis cluster. Every subsequent request carries the token or session key. Any healthy application node in the 100-node cluster handles any request instantly.
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.- Session Bound: Subsequent requests from the same client must route to the exact same server instance that initialized the session.
- Examples: Legacy Java Tomcat session memory, WebSockets connections bound to local server memory, embedded databases (SQLite).
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.- Node Independent: Any healthy instance in the application pool can process any request from any client at any time.
- Examples: RESTful API microservices, AWS Lambda functions, Kubernetes application worker pods.
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
- 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.
- 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.
- 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: BearerPattern 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 / Dimension | Stateful Architecture | Stateless + JWT | Stateless + Shared Redis |
|---|---|---|---|
| Horizontal Scaling | Hard (Requires Sticky Sessions) | Linear & Instant | Linear (Constrained by Redis QPS) |
| Node Failover Impact | Severe (Lost local sessions) | Zero Impact | Zero Impact |
| Session Invalidation | Immediate (Delete local RAM key) | Complex (Requires token blacklist) | Immediate (Delete Redis key) |
| Request Latency | Ultra-Fast (Local RAM lookup) | Ultra-Fast (In-memory crypto check) | Fast (Sub-ms network hop to Redis) |
| Network Bandwidth | Minimal (Small cookie header) | Higher (JWT token payload in header) | Minimal (Small session UUID cookie) |
| Operational Complexity | Low initially, High at scale | Low | Moderate (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:"user_id"
Email string json:"email"
Role string json:"role"
CreatedAt time.Time json:"created_at"
}
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 Scenario | Root Cause | System Symptom | Detection Signal | Mitigation Strategy |
|---|---|---|---|---|
| 1. Stateful Session Loss on Deploy | App 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 Imbalance | Load 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 Bloat | Storing 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 JWT | User 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
- Stateless compute scales linearly: Stateless services allow any node in an auto-scaling pool to handle any request, enabling seamless horizontal scaling.
- Avoid Sticky Sessions at scale: Sticky sessions cause load imbalances and break rolling deployments when nodes are terminated.
- Offload state to shared stores: Move session state out of application RAM into centralized Redis clusters or signed JWT tokens.
- JWTs enable zero-DB auth: Cryptographic JWT signatures allow app nodes to authenticate requests in RAM without database or cache lookups.
- Manage stateful stores carefully: Databases, message queues, and caches are stateful; isolate their complexity away from the stateless application tier.
Glossary of Terms
| Term | Definition |
|---|---|
| Stateful Service | A service that retains client session context in local memory or local disk across consecutive HTTP requests. |
| Stateless Service | A 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 Store | A 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 Deployment | A 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.
- Explain why local JVM heap session storage prevents the engineering team from deploying continuous zero-downtime updates.
- 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
- Scalability — Vertical, Horizontal, and Elastic Growth: Revisit horizontal scaling patterns and auto-scaling group mechanics.
- Load Balancing — Algorithms and Layers: Learn how load balancers distribute traffic across stateless server pools.
- Database Scaling Strategies — Replicas, Shards, and Multiplexing: Explore how to scale the stateful database tier.
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