26.10. Stateless Applications
Table of Contents
Understanding Stateless Applications
In backend development, the idea of a "stateless application" shows up everywhere. It affects how you design APIs, sessions, caching, and how you scale your system. This chapter focuses on what statelessness means, why it matters, and how to design for it in practice.
What “State” Means in Backend Systems
When we say "state" in backend systems, we usually mean data that changes over time and affects how the system behaves.
Examples of state:
- Who the current user is
- Items in a user’s shopping cart
- Results of the last search
- Progress of a background job
- A counter of how many requests a user made today
Some of this state is short-lived (for example, which page of results you are on), some is long-lived (for example, user profile data).
The key question for statelessness is: Where is this state stored and who remembers it between requests?
Stateless vs Stateful Applications
Stateless application
A stateless application does not remember anything between requests. Every request is handled as if the server had never seen that client before.
- The server does not keep track of:
- Who you are.
- What you did earlier.
- What your "session" is.
- All information needed to handle the request must be:
- In the request itself, or
- In shared external storage like a database or cache.
Stateful application
A stateful application keeps some in-memory context about the client between requests.
Examples:
- A server stores the currently logged-in user in memory based on a session ID.
- A server keeps a multi-step wizard’s progress only in memory.
- A game server keeps ongoing game state in RAM tied to a specific client connection.
These servers "remember" things without needing all of it to be sent again by the client.
Important rule:
A stateless server must be able to handle any valid request without relying on any in-memory data that was created by previous requests from the same client.
Why Stateless Applications Matter for Scalability
Stateless applications fit very well with horizontal scaling and load balancing.
Easier load balancing
In a stateless design:
- Request 1 from a user can go to Server A.
- Request 2 can go to Server B.
- Request 3 can go to Server C.
Since no server keeps private memory about that user, any server can serve any request.
In a stateful design, you often need "sticky sessions" where the same user must always be routed to the same server, because that server holds the user’s in-memory session. This makes scaling and failover more complicated.
Easier to add or remove servers
With statelessness:
- You can add more app server instances during high traffic.
- You can remove instances during low traffic.
You do not have to copy over in-memory session data to the new servers. All necessary state is outside the servers, for example in:
- Databases
- Caches
- Message queues
- Tokens carried by clients
Better fault tolerance
If one stateless server crashes midway:
- Future requests can go to other servers.
- No important in-memory session state is lost, because essential state was never kept only inside that instance.
In a stateful server:
- If the server holding your session crashes, you might be logged out or lose progress, because the state was in that server’s memory only.
Statelessness and REST APIs
REST (Representational State Transfer) encourages stateless communication between client and server.
In a RESTful system:
- Each HTTP request must contain enough information for the server to:
- Authenticate the user.
- Authorize the operation.
- Understand what resource is being acted on.
- Perform the requested action.
Example:
A stateful design might:
- POST
/login
Server storesuser_id = 123in an in-memory session associated with a session cookie. - GET
/orders
Server readsuser_idfrom memory and returns orders for user 123.
A stateless design might:
- POST
/login
Server returns a JWT token encodinguser_id = 123. - GET
/orderswithAuthorization: Bearer <token>
Server decodes the token on every request, without needing any in-memory session.
All the necessary info (the token) is sent with each request.
Where to Keep State in a “Stateless” System
Even in a “stateless application,” the system as a whole is not state-free. You still need to persist data. The idea is to move state away from individual app server instances.
Common storage places:
| Type of state | Typical storage |
|---|---|
| User accounts | Database (for example, PostgreSQL) |
| Shopping carts | Database or cache (for example, Redis) |
| Authentication | JWTs, token tables in DB, or Redis |
| Rate limits | Redis counters or similar |
| Background job progress | Job queue database or Redis |
| Long-term business data | Relational or NoSQL databases |
The app server simply:
- Reads state from these external systems.
- Processes the request.
- Writes any changes back.
The server itself does not "own" state. It just uses it.
Practical Example: Stateless Session Handling
Imagine a simple login system.
Stateful session example
- Client sends POST
/loginwith username and password. - Server:
- Verifies password.
- Generates a random session ID like
abc123. - Stores
{ session_id: "abc123", user_id: 42 }in memory or an in-process data structure. - Sends back a cookie:
Set-Cookie: session_id=abc123. - Client later requests GET
/profilewithCookie: session_id=abc123. - Server:
- Looks up session in memory by
abc123. - Finds
user_id = 42. - Returns user 42’s profile.
This server is stateful, because the in-memory session is required for future requests.
Stateless token example
- Client sends POST
/loginwith username and password. - Server:
- Verifies password.
- Generates a JWT with payload
{ "sub": 42, "exp": <timestamp> }, signed with a secret key. - Returns:
{ "access_token": "<jwt-here>" }. - Client later requests GET
/profilewith headerAuthorization: Bearer <jwt-here>. - Server:
- Verifies the JWT signature and expiration using its secret key.
- Reads
"sub": 42. - Returns user 42’s profile.
No in-memory session is required. Any server instance can decode the token and process the request.
Common Patterns to Achieve Statelessness
1. Use tokens, not in-memory sessions
Instead of storing sessions in memory:
- Use JWTs or opaque tokens.
- Optionally validate against a shared store, such as:
- Redis
- Database table
Example pattern:
- Access token (short-lived) sent in
Authorizationheader. - Refresh token (longer-lived) stored securely and used to get new access tokens.
The server can remain stateless if it only needs to verify tokens, not maintain per-user memory.
2. Store user-specific state in a database or cache
Do not keep things like cart contents in RAM per user.
Instead:
- Have a
cartstable:
carts
-----
user_id
item_id
quantity- Or a Redis key like
cart:<user_id>that contains serialized items.
In both cases, any server can load and update that cart.
3. Make requests self-contained
Each request should include:
- Authentication information, for example, token.
- All necessary parameters, for example, filters, pagination page, resource IDs.
Avoid designs where:
- A previous request "sets" some internal state.
- The next request relies on this hidden server state.
Bad pattern:
- POST
/searchwith body{ "query": "laptop" }.
Server storeslast_search = "laptop"in memory. - GET
/search/next-page.
Server useslast_searchfrom memory.
Better stateless pattern:
- GET
/search?query=laptop&page=2.
Now the second request contains all needed information.
4. Use caches that are external to the app instance
You can cache data and still be stateless, as long as:
- The cache is external (Redis, Memcached, etc.).
- Any server instance can access it.
Avoid per-instance in-memory caches that hold user-specific or request-specific state that other instances cannot see.
When Full Statelessness Is Hard or Undesirable
Not every system can be perfectly stateless.
Examples:
- Multiplayer games: The server holds game state in memory for performance and real-time updates.
- Long-lived WebSocket connections: Some state lives in memory while the connection is open.
- In-memory caches: For some data, per-node caching might be worth it even if it introduces complexity.
In these cases, you try to:
- Limit stateful parts to where they are truly needed.
- Keep protocols and APIs as stateless as possible for the rest of the system.
- Document the stateful behavior clearly.
Example: Migrating a Stateful API to Stateless
Imagine you have a simple note-taking API with this flow:
- Client logs in with POST
/loginand gets a session cookie. - Server stores the user in a global
sessionsdict, keyed by session ID. - GET
/notesreads the user from that dict. - POST
/notescreates notes for that user.
You want to scale to multiple app servers, but your in-memory sessions dict is tied to one instance.
Step 1: Move sessions to a shared store (intermediate)
Use Redis instead of in-memory dict:
sessions[session_id] = user_idin Redis.
Now any instance can read the session from Redis. The application is still logically stateful from the protocol point of view, but the state is no longer tied to one server.
Step 2: Switch to tokens (more stateless)
Replace session cookies with access tokens:
- Login returns JWTs.
- Each request includes
Authorization: Bearer <token>.
Now you can:
- Remove Redis sessions completely, if you trust token expiration and do not need server-side token invalidation.
- Or still keep a token blacklist or token metadata in Redis for logout/revocation.
From the API client’s perspective, the protocol is now stateless. Every request can be processed alone.
Trade-offs of Stateless Designs
Choosing statelessness has both benefits and costs.
Benefits
- Simple horizontal scaling.
- Simpler failover and deployment.
- Better compatibility with containers and orchestrators, for example, Kubernetes.
- Easier to add new instances or restart instances without affecting user sessions.
Costs and considerations
- More data needs to be sent with each request, for example, tokens or IDs.
- You may need more reads and writes to external storage, for example, DB or Redis.
- Some features, for example, immediate token revocation, are trickier with purely stateless tokens.
Often, systems end up mostly stateless, with a few carefully isolated stateful components.
Summary
- A stateless application does not rely on per-client in-memory context between requests.
- Statelessness is ideal for load balancing, scaling, and fault tolerance, because any request can be handled by any instance.
- State still exists, but it is stored in shared external systems, for example, databases, caches, or encoded in tokens.
- REST APIs are naturally suited to stateless designs where each request is self-contained.
- In practice, aim to keep your HTTP APIs and core business logic stateless, and push unavoidable statefulness into well-defined, controlled parts of the system.
Views: 8
KAHIBARO