Stateless Services and Sessions
A stateless server keeps nothing about a user between requests, so any server can handle any request. Learn where session data goes instead: a shared store, a database or a signed token.
In a nutshell
- A stateless service keeps no per-user data in its own memory or disk between requests.
- Then any instance can serve any request: scaling out, deploys and crashes become easy.
- Move state to a shared store (Redis, a database) or into a signed token the client sends back.
- Sticky sessions keep stateful servers working behind a load balancer, but they spread load unevenly and lose sessions on restarts.
Where the trouble starts
A login form stores the user's session in the server's memory. With one server, fine. Add a second server behind a load balancer and the next request lands on server B, which has never heard of this user: logged out. Restart server A for a deploy: everyone on it is logged out too.
The fix is to make the application servers stateless: they keep nothing about a user between requests. Anything that must survive is stored somewhere every server can reach.
Three places to keep sessions
A shared session store
All steps Any server can handle any request because the session lives in the shared store, not in one server's memory.
- The browser sends only a random session ID in a cookie.
- The load balancer can send the request to whichever server is least busy.
- That server loads the session from the shared store. Any server could have done the same.
Diagram as text
- The browser sends only a random session ID in a cookie.
- The load balancer can send the request to whichever server is least busy.
- That server loads the session from the shared store. Any server could have done the same.
| Approach | How it works | Good | Watch out |
|---|---|---|---|
| Shared store | Cookie holds a random ID; data lives in Redis or Memcached | Fast; easy to log users out | The store must be highly available |
| Database | Sessions in a table | Durable; no new system | A read on every request; clean up old rows |
| Signed token | The data itself (user ID, expiry) travels in a signed cookie or token, for example a JWT | No lookup at all; works across services | Hard to revoke before it expires; keep it small |
| Sticky sessions | Load balancer pins each user to one server | No code changes | Uneven load; sessions lost when that server goes away |
Signed tokens
A signed token puts the session data in the client's hands, with a signature that only your servers can produce. Any server can check the signature with the shared secret and trust the data without a lookup. The client can read the token, so never put secrets in it; it simply cannot change it.
A tiny signed session token (HMAC-SHA256): user, expiry, signature
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
class SessionToken {
private final byte[] secret;
SessionToken(String secret) {
this.secret = secret.getBytes(StandardCharsets.UTF_8);
}
String sign(String userId, long expires) {
String payload = userId + "." + expires;
return payload + "." + mac(payload);
}
// Returns the user ID, or null if the token is forged or expired.
String verify(String token, long now) {
int last = token.lastIndexOf('.');
int first = token.lastIndexOf('.', last - 1);
if (first < 0) {
return null;
}
String payload = token.substring(0, last);
byte[] given = token.substring(last + 1).getBytes(StandardCharsets.UTF_8);
byte[] expected = mac(payload).getBytes(StandardCharsets.UTF_8);
if (!MessageDigest.isEqual(given, expected)) {
return null;
}
long expires = Long.parseLong(token.substring(first + 1, last));
return now < expires ? token.substring(0, first) : null;
}
private String mac(String payload) {
try {
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(secret, "HmacSHA256"));
return HexFormat.of().formatHex(hmac.doFinal(payload.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}import hashlib
import hmac
class SessionToken:
def __init__(self, secret):
self.secret = secret.encode()
def sign(self, user_id, expires):
payload = f"{user_id}.{expires}"
return f"{payload}.{self._mac(payload)}"
def verify(self, token, now):
"""Returns the user ID, or None if the token is forged or expired."""
parts = token.rsplit(".", 2)
if len(parts) != 3:
return None
user_id, expires, signature = parts
if not hmac.compare_digest(signature, self._mac(f"{user_id}.{expires}")):
return None
return user_id if now < int(expires) else None
def _mac(self, payload):
return hmac.new(self.secret, payload.encode(), hashlib.sha256).hexdigest()Beyond sessions
Sessions are the usual culprit, but other state hides in servers too:
- Uploaded files saved to local disk: store them in object storage instead.
- In-memory caches: fine as a speed-up, as long as losing them only costs time, never correctness.
- Scheduled jobs running on "server 1": use a scheduler or a lock so exactly one instance runs each job.
- WebSocket connections: these are stateful by nature; keep them in a dedicated layer and pass messages through a pub/sub system.
Common mistakes
- Keeping sessions or uploads in one server's memory or disk, then adding a second server.
- Relying on sticky sessions as the long-term fix.
- Putting secrets or large data inside client-side tokens.
- Long-lived tokens with no way to revoke them.
Interview questions
Try answering out loud before you open each one.
How do you scale the web tier horizontally?
Make the servers stateless: sessions in Redis or in signed tokens, files in object storage, jobs coordinated through a scheduler. Then put them behind a load balancer and add or remove instances freely, for example with autoscaling on CPU or request rate.
Server-side sessions or JWTs?
Server-side sessions (random ID in a cookie, data in Redis) are easy to revoke and keep data private, at the cost of a lookup per request. Signed tokens avoid the lookup and work well between services, but cannot be revoked before expiry without extra machinery. Many systems use short-lived tokens plus revocable refresh tokens.
Check yourself
Related topics
Further reading
- The Twelve-Factor App: Processes 12factor.net
- Session Management Cheat Sheet OWASP
- RFC 7519: JSON Web Token (JWT) IETF
Your notes
Saved in this browser as you type.
Finished this topic?
Mark it read to track your progress, then carry on with the next one.