Designing a login/session system that scales across many services — stateless JWT vs server-side sessions, refresh token rotation, single sign-on across services, and defending against credential-stuffing at high request volume.
Published September 23, 2026
Design an authentication system serving many downstream services (not just one monolith) — issuing and validating credentials for millions of users, at high request volume, without the auth system itself becoming a bottleneck or single point of failure for every other service.
Functional: register/login with credentials; issue a token/session proving identity; validate that token on every subsequent request; support logout/token revocation; support password reset. Non-functional: token validation must be fast (it happens on nearly every request across every service); the system must resist credential-stuffing and brute-force attacks; no single point of failure for the whole platform's auth.
Server-side session:
Login → server creates a session record in a shared store (Redis) → returns a session ID
Every request → service looks up the session ID in the shared store to validate
— validation requires a network call to the session store on EVERY request
JWT (stateless):
Login → server issues a signed token containing the user's identity + claims
Every request → service verifies the SIGNATURE locally, no network call needed
— validation is a local cryptographic check, not a lookup
This is the central architectural decision, and it's a real tradeoff, not a default "JWT is always better": JWTs eliminate the network round-trip to a shared session store on every request (meaningful at scale — every service validating identity on every incoming request adds up), but a JWT can't be easily revoked before its expiry (the signature is still valid even if you want to force-invalidate it) — server-side sessions can be revoked instantly (delete the session record) but require centralized store availability for every validation. Many systems use a hybrid: short-lived JWTs (limiting the revocation-window problem by simply expiring quickly) plus a refresh mechanism.
Login → issue:
Access token (JWT, short-lived, e.g. 15 min) — used for actual API requests
Refresh token (long-lived, e.g. 30 days, stored server-side, revocable) — used ONLY to get a new access token
Access token expires → client sends refresh token to /auth/refresh
→ server validates refresh token IS STILL VALID (checked against server-side store — this is
the revocation point), issues a NEW access token AND a NEW refresh token (rotation),
invalidates the OLD refresh token
This combines both models' strengths: the frequent, high-volume operation (validating an access token on every API request) stays fast and stateless (JWT signature check); the infrequent operation (refreshing) pays the cost of a server-side, revocable check — and only there. Refresh token rotation (issuing a new refresh token on every use, invalidating the old one) means a leaked refresh token that gets used by an attacker is DETECTABLE: if the legitimate user's next refresh attempt uses the now-invalidated old token, the system can flag this as a compromise signal and revoke the entire token family.
[Auth Service] (the single source of truth for identity, issues tokens)
│
├── Service A validates the SAME JWT (checks signature with Auth Service's public key)
├── Service B validates the SAME JWT
└── Service C validates the SAME JWT
A JWT signed by a central Auth Service, verified independently by every downstream service using the Auth Service's PUBLIC key (asymmetric signing — the Auth Service holds the private key, every other service only needs the public key to verify, never the ability to issue tokens themselves) is what makes single sign-on across many services practical: log in once, and every service can independently verify the resulting token without needing to call the Auth Service back for every single request.
A login endpoint is a uniquely attractive target for automated credential-stuffing (trying huge lists of leaked username/password pairs) — this connects directly to Payment — Security's fraud-signal thinking and API Rate Limiting Gateway's per-endpoint rate limiting: login specifically needs a STRICTER rate limit than most endpoints (per-IP AND per-account), often combined with progressive backoff (increasing delay after repeated failures) and, past a threshold, a CAPTCHA challenge or temporary account lock — a generic, uniform rate limit across all endpoints under-protects this specific, high-value target.
Q: If a JWT can't be revoked before expiry, how do you handle an urgent case like a stolen access token? A: Keeping access tokens SHORT-lived (minutes, not hours) bounds the exposure window inherently; for a genuinely urgent case, some systems maintain a small, fast-to-check revocation blocklist (checked in addition to signature verification) specifically for emergency revocation, accepting the small added lookup cost as a rare-case exception rather than the default path.
Q: Why rotate refresh tokens instead of just using the same long-lived refresh token repeatedly? A: Rotation is what turns refresh-token theft into a DETECTABLE event (the legitimate user's next use of the now-stale token reveals the compromise) rather than a silent, ongoing one — a non-rotating refresh token that's stolen remains valid and undetected for its entire (often long) lifetime.
Q: How does asymmetric signing (public/private key) specifically enable horizontal scaling of token validation? A: Because verification only needs the PUBLIC key, every service instance across every downstream service can independently verify tokens without ever holding the sensitive private key or needing network access to the Auth Service at validation time — this decouples validation throughput entirely from the Auth Service's own capacity, letting validation scale purely by scaling the downstream services themselves.
Q: Is login rate limiting per-IP sufficient, given attackers can use many IPs (distributed credential stuffing)? A: Not alone — per-account rate limiting (regardless of source IP) is the necessary complement, since a distributed attack spreads requests across many IPs specifically to evade per-IP limits, but every attempt still targets the same account and can be rate-limited on that dimension instead.