Sticky Sessions and Stateless Applications

5.0 out of 5 from 1 votes
By Oleksandr Andrushchenko — Published on
1 Likes
0 Dislikes
Sticky Sessions and Stateless Applications
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

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.

  1. Route subsequent requests from that client back to App 1.
  2. 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.

With cookie-based affinity, the load balancer associates a cookie with a selected backend.

A simplified flow looks like this:

  1. A client sends its first request without an affinity cookie.
  2. The load balancer selects App 2 using its normal routing algorithm.
  3. The response contains an affinity cookie.
  4. The client includes that cookie in later requests.
  5. 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.

Author

Enjoyed this article?

Support Oleksandr Andrushchenko

Buy me a coffee

This helps Oleksandr Andrushchenko continue creating useful content

Related articles

Comments (0)