Sticky Sessions and Stateless Applications
Load balancers normally distribute requests across multiple interchangeable application instances. This works best when any healthy instance can process any request. Problems appear when an application stores user-specific state locally and later requests must return to the same server.
Sticky sessions solve this by creating affinity between a client and a backend instance. Stateless applications take the opposite approach: application instances do not depend on local session state, allowing requests to move freely across the backend fleet. The choice directly affects scalability, failure recovery, deployments, and load distribution.
Table of Contents
- Why Session Affinity Exists
- Sticky Sessions
- Stateless Applications
- Sticky Sessions vs Stateless Applications
- Production Design Example
- Common Mistakes
- Production Checklist
- Conclusion
- Key Takeaway
- More Articles to Read
Why Session Affinity Exists
Consider an application running three instances behind a load balancer. A user logs in through App 1, which stores the session in its local memory.
Login request
|
v
Load Balancer
|
v
App 1
App 1 memory:
session_abc = {
user_id: 123,
authenticated: true
}
The next request may be routed to App 2:
GET /account
|
v
Load Balancer
|
v
App 2
App 2 memory:
session_abc = NOT FOUND
From App 2's perspective, the session does not exist. The user might appear logged out even though authentication succeeded seconds earlier.
There are two fundamental ways to solve this problem.
- Route subsequent requests from that client back to App 1.
- Move the required state somewhere accessible to every application instance.
The first approach creates session affinity. The second makes the application tier stateless with respect to the session.
This distinction becomes increasingly important as application fleets grow, autoscaling adds and removes instances, deployments continuously replace servers, and failures redistribute traffic.
Sticky Sessions
Sticky sessions, also called session persistence or session affinity, instruct a load balancer to keep routing a client to the same backend for some period.
Load Balancer
/ | \
/ | \
v v v
App 1 App 2 App 3
^ ^ ^
| | |
User A User B User C
User A requests --> App 1
User A requests --> App 1
User A requests --> App 1
The backend can therefore retain user-specific state in local memory or local storage because subsequent requests are expected to return to that instance.
Affinity is commonly implemented using a cookie or a value derived from the client's source address.
Cookie-Based Affinity
With cookie-based affinity, the load balancer associates a cookie with a selected backend.
A simplified flow looks like this:
- A client sends its first request without an affinity cookie.
- The load balancer selects App 2 using its normal routing algorithm.
- The response contains an affinity cookie.
- The client includes that cookie in later requests.
- The load balancer uses it to route the client back to App 2.
First request:
Client
|
| GET /account
v
Load Balancer
|
+------------------> App 2
|
<-- Set-Cookie: LB_ROUTE=app2
Later request:
Client
|
| Cookie: LB_ROUTE=app2
v
Load Balancer
|
+------------------> App 2
The cookie should normally contain an opaque routing value rather than exposing internal server details directly. Depending on the load balancer, the cookie can be generated by the infrastructure or supplied by the application.
Affinity cookies require the same security considerations as other cookies. HTTPS, appropriate Secure and HttpOnly behavior, suitable expiration, and controlled domain and path scope should be used where applicable.
Most importantly, routing metadata should not be treated as authorization. An affinity cookie determines where a request goes, not whether the request is trusted.
Source IP Affinity
Another strategy hashes the client's source IP address and uses the result to select a backend.
def select_backend(client_ip: str, backends: list[str]) -> str:
# Simplified illustration only.
# Production implementations need stable hashing behavior.
index = hash(client_ip) % len(backends)
return backends[index]
This requires no browser cookie, but source IP is often a weak representation of client identity.
Thousands of users can appear behind the same NAT gateway, corporate proxy, mobile carrier, or forward proxy:
User A ---\
User B ----\
User C -----> Corporate NAT -- 203.0.113.10 --> Load Balancer
User D ----/
User E ---/
If routing uses only the visible source IP, all of these clients can be mapped to the same backend and create a hotspot.
Client addresses can also change during a session, particularly on mobile networks. Proxies and multi-layer traffic architectures further complicate determining which address should participate in affinity.
Source-IP affinity is therefore appropriate only when the network topology and client behavior make the address a sufficiently stable and well-distributed routing key.
Sticky Session Trade-Offs
The main advantage of sticky sessions is that they can support applications that depend on local state without immediately redesigning the application.
They can be useful for legacy applications, expensive migrations, specialized stateful workloads, or temporary transitions toward a stateless architecture.
The cost is coupling between clients and individual application instances.
Suppose three backends initially receive similar traffic:
App 1: 1,000 sticky clients
App 2: 1,050 sticky clients
App 3: 950 sticky clients
A group of users assigned to App 2 then begins performing expensive operations:
App 1: CPU 35%
App 2: CPU 92%
App 3: CPU 31%
A normal load-balancing algorithm could redirect new requests toward Apps 1 and 3. Strong session affinity limits that flexibility because existing App 2 clients continue returning there.
Failures create another problem. If App 2 disappears, its clients must be reassigned:
Before failure:
User A --> App 2 --> local session exists
After failure:
User A --> App 1 --> local session missing
If the session existed only on App 2, affinity cannot preserve it after failure. Sticky sessions therefore provide routing persistence, not state durability.
Stateless Applications
A stateless application instance does not require locally stored client session state to process the next request. Any healthy instance can receive it.
Request 1 --> App 1
Request 2 --> App 3
Request 3 --> App 2
Request 4 --> App 1
The application may still depend on state. The important distinction is where that state lives.
Instead of tying state to one application process, shared state can live in databases, distributed caches, object storage, or other dedicated systems.
Externalizing State
A common architecture stores session information in a shared session store:
Load Balancer
/ | \
v v v
App 1 App 2 App 3
\ | /
\ | /
v v v
+-------------+
| Shared |
| Session |
| Store |
+-------------+
Every application instance can resolve the same session identifier:
from fastapi import Cookie, FastAPI, HTTPException
app = FastAPI()
def get_session_from_store(session_id: str) -> dict | None:
# Represents a shared session store such as Redis.
# Every application instance reads the same logical state.
...
@app.get("/account")
def account(session_id: str | None = Cookie(default=None)):
if not session_id:
raise HTTPException(status_code=401)
session = get_session_from_store(session_id)
if not session:
raise HTTPException(status_code=401)
return {"user_id": session["user_id"]}
App 1 can process the login request while App 3 processes the next request because both access the same session store.
Another option is carrying sufficient authentication information in a signed token. This can reduce session-store lookups, although token revocation, expiration, authorization changes, key rotation, and token size still require careful design.
Statelessness should not be interpreted as putting every piece of application state into a token. Large or frequently changing state usually belongs in an external data system.
Stateless Application Trade-Offs
Stateless application instances provide several important operational advantages:
- Flexible routing: any healthy instance can process a request.
- Simpler autoscaling: new instances do not need existing client assignments.
- Safer deployments: instances can be replaced without losing local sessions.
- Better failure recovery: traffic can move away from failed instances.
- Better load distribution: routing algorithms remain free to choose healthy capacity.
The trade-off is that state has not disappeared. It has moved into dedicated infrastructure.
A shared session store now requires its own:
- availability strategy;
- capacity planning;
- latency monitoring;
- expiration policy;
- failure handling;
- security controls.
If every API request synchronously reads a remote session store, that store becomes part of the critical request path. A session-store outage can affect every application instance simultaneously.
Stateless application servers reduce coupling between requests and compute instances, but they do not eliminate distributed-system dependencies.
Sticky Sessions vs Stateless Applications
The architectural difference becomes most visible during scaling and failures.
| Characteristic | Sticky Sessions | Stateless Applications |
|---|---|---|
| Client affinity | Usually required | Usually unnecessary |
| Request routing | Constrained by existing assignments | Any healthy instance |
| Horizontal scaling | More complicated | Straightforward |
| Instance failure | Can lose local session state | Requests move to another instance |
| Rolling deployments | Require affinity and draining considerations | Instances are easier to replace |
| Load distribution | Can become uneven | Routing remains flexible |
| Shared state infrastructure | Potentially reduced | Often required |
| Application-instance coupling | High | Low |
| Typical use | Legacy or intentionally stateful workloads | Modern horizontally scaled services |
For most horizontally scaled HTTP applications, stateless application instances are the preferred default. They allow the load-balancing layer to make routing decisions according to health and capacity instead of preserving historical client assignments.
Sticky sessions remain useful when affinity has a concrete technical benefit. They should be an intentional architectural decision rather than an automatic solution for application state management.
Routing algorithms such as Round Robin and Least Connections become more effective when requests are not constrained by affinity. More about those strategies can be found in Round Robin vs Least Connections vs Consistent Hashing.
Production Design Example
Consider an e-commerce API running six application instances across three availability zones. Users authenticate, maintain shopping carts, and upload product-related files.
A fragile architecture stores everything locally:
Load Balancer
Sticky Sessions
/ | \
v v v
App 1 App 2 App 3
App local state:
- authentication session
- shopping cart
- uploaded files
This architecture makes application instances difficult to replace. Losing an instance can mean losing sessions, carts, and files associated with that instance.
A stronger design separates compute from durable or shared state:
Clients
|
v
+---------------+
| Load Balancer |
+---------------+
/ | \
v v v
App 1 App 2 App 3
\ | /
\ | /
\ | /
+-----------+---+---+-----------+
| | |
v v v
Session Store Database Object Storage
short-lived carts and uploaded files
session data business data
The load balancer no longer needs session affinity. Requests can be distributed according to backend health and workload.
A request lifecycle might look like:
1. POST /login
Load Balancer --> App 2
App 2 --> Session Store
2. GET /cart
Load Balancer --> App 1
App 1 --> Session Store
App 1 --> Database
3. POST /checkout
Load Balancer --> App 3
App 3 --> Session Store
App 3 --> Database
App 2 can disappear after the login request without invalidating the architectural assumption that App 1 or App 3 can process later requests.
This also simplifies autoscaling:
Traffic increases
|
v
Add App 4, App 5, App 6
|
v
Pass readiness checks
|
v
Register with load balancer
|
v
Immediately eligible for requests
No client assignments need to be migrated to the new instances.
During scale-in or deployment, an instance is first removed from new traffic. Existing in-flight requests are drained, and then the instance can be terminated. No durable user state should disappear with it.
This lifecycle works naturally with highly available load-balancing infrastructure. For the broader failure-domain architecture, see Designing Highly Available Load Balancing Architectures.
There are still workloads where affinity is intentional. A real-time processing system may maintain expensive in-memory state associated with a specific key, or cache locality may be important enough to prefer repeated routing to the same backend.
In those cases, affinity should be designed together with explicit failure behavior. The system must answer what happens when the preferred backend disappears rather than assuming the mapping will always remain available.
Common Mistakes
| Mistake | Why It Causes Problems | Better Approach |
|---|---|---|
| Enabling sticky sessions by default | Client-to-instance coupling is introduced even when the application does not require it. | Use affinity only when a concrete state or locality requirement exists. |
| Storing critical sessions only in process memory | Instance failure destroys session state and can log out affected users. | Use durable or replicated shared session storage when sessions must survive instance failure. |
| Treating stickiness as durability | Affinity only routes back to an instance; it cannot recover state after that instance disappears. | Design state durability independently from routing affinity. |
| Using source IP as reliable user identity | NAT and proxies can place many users behind one address, while mobile clients can change addresses. | Use a stable application or load-balancer affinity identifier when affinity is required. |
| Storing uploaded files on application disks | Files become tied to individual instances and may disappear during scaling or replacement. | Store durable uploads in shared object or file storage. |
| Moving sessions to one unreplicated cache | The application tier becomes stateless while the session tier becomes a new single point of failure. | Design session storage according to the application's availability requirements. |
| Putting excessive state into authentication tokens | Tokens become large, stale, difficult to revoke, and expensive to transmit repeatedly. | Keep tokens focused on stable identity and authorization information; externalize larger mutable state. |
| Ignoring affinity expiration | Old routing assignments can persist longer than required and interfere with deployments or scaling. | Set affinity duration according to the actual application requirement. |
| Assuming stateless means dependency-free | Shared state stores can become latency bottlenecks or common failure points. | Capacity-plan, monitor, and make shared state infrastructure appropriately resilient. |
| Deploying sticky applications without draining | Removing an instance abruptly breaks active sessions and in-flight requests assigned to it. | Deregister targets and allow sufficient draining before termination. |
Production Checklist
- Identify local state: document sessions, caches, temporary files, uploads, and other data stored on application instances.
- Challenge affinity requirements: verify that sticky routing solves a real requirement rather than hiding avoidable local state.
- Externalize durable state: keep important business and user data outside replaceable application instances.
- Choose session storage intentionally: balance latency, durability, availability, expiration, and cost.
- Protect affinity cookies: apply appropriate transport, scope, expiration, and cookie security controls.
- Keep routing separate from authorization: never trust affinity metadata as proof of identity or access.
- Define affinity failure behavior: specify what happens when the preferred backend becomes unavailable.
- Test instance termination: remove application instances while active sessions and requests are present.
- Enable graceful draining: stop new routing before terminating instances.
- Monitor per-target traffic: detect imbalance caused by sticky client populations or expensive sessions.
- Test autoscaling: verify that new instances can immediately serve arbitrary requests where statelessness is expected.
- Plan shared-store failures: define application behavior when session or state infrastructure becomes slow or unavailable.
- Use expiration policies: remove abandoned session state and stale affinity mappings.
- Keep tokens reasonably small: avoid replacing server-side state with oversized client-side state.
- Prefer replaceable compute: application instances should be safe to restart, scale, deploy, and remove without losing durable state.
Conclusion
Sticky sessions preserve client-to-server affinity, making them useful when requests depend on state stored or cached on a particular backend. That convenience comes with tighter coupling, less flexible load distribution, and more complicated failure and deployment behavior.
Stateless application instances move important state outside individual compute nodes. This makes horizontal scaling, failover, deployments, and load balancing substantially easier, although shared state infrastructure must then be designed as a reliable production dependency.
Key Takeaway
Prefer stateless application instances unless affinity provides a concrete architectural benefit. Sticky sessions can preserve routing to a backend, but they cannot make local state durable. Keeping application compute replaceable allows load balancers to route according to health and capacity instead of being constrained by historical client assignments.
More Articles to Read
- Load Balancing Explained: Distributing Traffic at Scale
- Round Robin vs Least Connections vs Consistent Hashing
- Designing Highly Available Load Balancing Architectures
- Traffic Routing Strategies for Zero-Downtime Deployments
- Global Load Balancing and Multi-Region Traffic Routing
- Load Balancing Best Practices for Production Systems
Comments (0)