Ecommet
Topic 25 of 75

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.

Beginner 6 min read 4 quiz questions

Jump to the quiz

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

A shared session store User → Load balancer: request Load balancer → Server C: any server Server C → Session store: get 91c Server A → Session store Server B → Session store Usercookie: sid=91cLoad balancerServer AServer BServer CSession storeRedisrequestany serverget 91c

All steps Any server can handle any request because the session lives in the shared store, not in one server's memory.

Diagram as text
  1. The browser sends only a random session ID in a cookie.
  2. The load balancer can send the request to whichever server is least busy.
  3. That server loads the session from the shared store. Any server could have done the same.
ApproachHow it worksGoodWatch out
Shared storeCookie holds a random ID; data lives in Redis or MemcachedFast; easy to log users outThe store must be highly available
DatabaseSessions in a tableDurable; no new systemA read on every request; clean up old rows
Signed tokenThe data itself (user ID, expiry) travels in a signed cookie or token, for example a JWTNo lookup at all; works across servicesHard to revoke before it expires; keep it small
Sticky sessionsLoad balancer pins each user to one serverNo code changesUneven 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

Java
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);
        }
    }
}
Python
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

1. What does "stateless server" mean?
Show the answer

Answer: It keeps no per-user data between requests. The state still exists, but in shared systems, not in the server.

2. What is a downside of sticky sessions?
Show the answer

Answer: Load can be uneven and sessions are lost when a server goes away. Pinning users to servers ties their sessions to those servers.

3. Why can a user not edit the user ID inside a signed token?
Show the answer

Answer: Changing it breaks the signature, which only the server can produce. The signature covers the data. Note that signing does not hide the data; the user can read it.

4. Where should user uploads go in a stateless design?
Show the answer

Answer: Object storage shared by all servers. Shared object storage lets any server read the file later.

Further reading

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.

Next topic