System design interview
Design a login and session service
Slow password hashing, rotating refresh tokens, revocation.
Press Play to follow a request through the design, or step through it.
- Client and edge
- Service
- Data store
- Cache and queue
- The app posts an email and a password over HTTPS.
- Before any costly work, the gateway checks the attempt counters for this account and this IP address.
- Under the limit, the request goes on. Over it, the reply is a delay or a challenge, and no hash is computed.
- The gateway passes the request to the Auth service.
- The service loads the account's salted hash, runs the slow hash on the password, and compares the two in constant time.
- On a match it saves a session row for this device, holding only a hash of a new random refresh token.
- It returns a signed access token valid for 15 minutes, and the refresh token.
- A browser gets the refresh token in an HttpOnly, Secure cookie. A phone app puts it in secure storage.
- Client
- API gateway: verifies access tokens
- Attempt counters: per account and IP
- Auth service: login, refresh, logout
- App API
- User DB: salted password hashes
- Session store: one row per device
- Email queue
On this page
In 2 minutes
I would split the system into a gateway and an Auth service, with three stores: user accounts, sessions and attempt counters. Passwords are stored as a slow, salted hash such as Argon2id, tuned to about 100 ms, and compared in constant time. Because each attempt costs us CPU, the gateway counts attempts per account and per IP in a fast store before any hashing, using delays and challenges instead of hard lockouts, with an optional second factor. After login the client holds a signed access token that lasts 15 minutes and a random refresh token that lasts 30 days.
The gateway verifies access tokens by signature alone, so 50,000 API calls a second never touch the session store. Only refreshes and logins do, about 2,300 a second in our estimate. Refresh tokens rotate on each use, and a reused token revokes the whole session family. A session row per device powers the devices list and single-device logout.
Log out everywhere revokes every row and raises a token version in one transaction, so no refresh works. Old access tokens expire within 15 minutes, and sensitive actions check the session row directly. In a browser the refresh token sits in an HttpOnly, Secure, SameSite cookie.
Step 1 of 6: Clarify the scope
Interviewer asks
Design the sign-up, login and session system for an app on web and mobile. People sign up with an email and a password, stay signed in on several devices, and can sign out of one device or of all of them. A stolen password or token should do as little damage as possible.
Questions to ask first
Ask these before you draw anything. Under each one is why its answer changes the design.
- Is it email and password only, or also social login and one-time codes?
Social login hands the password problem to another provider. Passwords are the hardest case, so we design for them first.
- Do web and mobile share the same accounts and sessions?
A browser can keep a cookie that page scripts cannot read. A phone app keeps a token in the system's secure storage. One backend must serve both.
- How long should someone stay signed in without typing a password?
Weeks of convenience means a long-lived credential, which is worth more to a thief. This sets the life of the refresh token.
- Is a second factor required, or optional?
A second factor adds a step to login and needs a recovery path for people who lose their phone.
- After someone logs out, how fast must their old tokens stop working?
Instant revocation needs a lookup on every request. A delay of a few minutes lets us check tokens with no lookup at all.
Requirements
Functional
- Sign up with an email and a password, and verify the email address.
- Log in on a new device and stay signed in for up to 30 days.
- List the devices that are signed in, and log out one device or all of them.
- Change or reset a password, which ends every other session.
- Offer an optional second factor, such as a code from an authenticator app.
Non-functional
- Login: p99 under 600 ms, including a password hash that is slow on purpose.
- Checking a session on an ordinary request adds under 5 ms at p99 and needs no database read.
- A logged-out or stolen session stops working at once for refresh, and within 15 minutes for API calls.
- Passwords are never stored in a recoverable form, and an account allows at most 5 wrong guesses in 15 minutes before it slows down.
- Handles about 2,200 refreshes and 120 logins a second at peak, plus up to 1,000 attack attempts a second.
Step 2 of 6: Size it
Normal logins are rare and refreshes are the steady load. Password hashing is the one expensive step that an attacker can force. Throttle before you hash, and keep the 50,000 API calls a second away from the session store by using short-lived signed tokens.
Assumed for the exercise, not facts about a real system:
- 5 million people use the app on a typical day, and each starts a fresh login about once in 5 days, because sessions last for weeks. That is 1 million logins a day. Peak traffic is 10 times the average.
- At the busiest time 2 million devices are open and in use. Each one trades in its 15-minute (900 s) access token for a new one every 15 minutes.
- The API takes 50,000 requests a second at peak. One password hash costs about 100 ms of one CPU core, on purpose. A credential-stuffing attack can reach 1,000 login attempts a second.
- 20 million accounts, with 2 session rows each on average, at about 200 bytes a row.
- Logins per second
- 1M a day ÷ 86,400 s ≈ 11.6
- ≈ 12/s on average, about 120/s at a 10x peak
- Refreshes per second
- 2M devices ÷ 900 s ≈ 2,222
- ≈ 2,200/s, the steady load on the auth service
- Cores busy hashing under attack
- 1,000 attempts a second × 0.1 s of CPU
- 100 cores, so throttle before hashing
- Session rows
- 20M accounts × 2 rows × 200 bytes
- 8 GB, small enough for one replicated store
- Session store requests
- Refreshes plus logins: 2,222 + 120; API calls are checked by signature
- ≈ 2,300/s, not the 50,000/s of the API
Step 3 of 6: Sketch the path of one request
Step through the diagram at the top of the page to watch a request move through these parts.
The Client, a browser or a phone app, talks to an API gateway. For login routes the gateway first asks the Attempt counters, a fast in-memory store, whether this account or this IP address has guessed too often. Only then does it pass the request to the Auth service.
The Auth service looks up the account in the User DB, which holds a salted, slow hash of each password and never the password itself. On a match it creates a row in the Session store for this device and returns two things: a signed access token that lasts 15 minutes, and a random refresh token that lasts up to 30 days. The store keeps only a hash of the refresh token.
For ordinary API calls the gateway checks the access token's signature and expiry by itself, so no store is read. When the access token runs out, the client trades its refresh token for a new pair. Logout marks session rows as revoked, so the next refresh fails. Verification and security emails go through an Email queue, so a slow mail provider never slows a login.
Other paths through the system
Call the API
- The app calls the API and sends its access token.
- The gateway checks the signature and expiry with the public key, with no store lookup, then passes on the user ID.
- The App API serves the request for that user.
- The response returns. After 15 minutes the same call gets a 401, and the app refreshes its tokens.
Log out everywhere
- Alice taps 'log out everywhere' on her laptop.
- The gateway forwards the call to the Auth service.
- In one transaction, the service marks every session row of Alice's as revoked and raises her token version by one.
- The store confirms. From now on every refresh by any of her devices is refused.
- A 'you were signed out everywhere' notice is queued as an email, so it can never slow the call.
- The service tells the gateway it is done.
- The laptop deletes its tokens. Her other devices fail at their next refresh, and their access tokens end within 15 minutes.
Step 4 of 6: Go deep on the hard part
Storing passwords and stopping guessing
Interviewer asks
How do you store passwords, and how do you stop someone from guessing them?
Never store a password, and never store a fast hash of it.
Read moreShow less: Storing passwords and stopping guessing
If the user table leaks, an attacker with a graphics card can test billions of fast hashes a second. Store the output of a slow, salted password hash: Argon2id, scrypt or bcrypt. They are built to cost real time, and in some cases real memory, on purpose. Tune the cost so one hash takes about 100 ms on your login servers, and raise it as hardware gets faster.
A salt is a random value, unique to each account, stored beside the hash and fed into it. Two people with the same password then get different hashes, and an attacker cannot use one precomputed table against everyone. The stored string also records the algorithm and its cost settings, so you can raise the cost later: on the next successful login, hash again with the new settings.
Compare the result with a constant-time comparison, so the response time does not reveal how much of the hash matched. When the email has no account, do the same work by hashing against a dummy value, and give one message for both cases, such as 'Email or password is wrong'. Otherwise the login form becomes a way to find out who has an account.
Throttling. The slow hash costs the attacker for each guess, but it also costs you 100 ms of CPU for each attempt. In our estimate an attack of 1,000 attempts a second would keep 100 cores busy. So count attempts before hashing, in the attempt counters:
- Per account: after 5 failures in 15 minutes, add a growing delay or ask for a challenge. This stops guessing at one account.
- Per IP address and per device: a looser limit, since many people share one address. This slows an attack that tries one common password on many accounts, known as password spraying.
- Check new passwords against lists of leaked ones at sign-up, and rate-limit sign-up and password reset too.
A hard lockout after a few failures is simple, but it lets anyone lock a victim out by typing wrong passwords on purpose. Prefer delays and challenges, and let a login from a device the person has used before skip the delay. An optional second factor, such as a time-based code from an authenticator app, makes a stolen password alone useless. Plan the recovery path early, with backup codes, because people lose phones.
Opaque session IDs or signed tokens
Interviewer asks
What does the client hold after login, and how is it checked on each request?
There are two common shapes.
Read moreShow less: Opaque session IDs or signed tokens
An opaque session ID is a random string. The server looks it up in a fast store on every request. It is simple, and revoking it is instant, but every API call costs a store read. At 50,000 requests a second that is a heavy, always-on dependency.
A signed access token carries the user ID, an expiry time and a few claims, with a signature made by the Auth service's private key. Any gateway can verify it with the public key and no lookup. The price is that you cannot take it back before it expires, so keep it short, 15 minutes, and pair it with a long-lived refresh token.
The refresh token is a long random string that only the client holds. The server stores its hash, with the user, the device and an expiry of 30 days. When the access token runs out, the client sends the refresh token and gets a new pair. This is the only step that reads the session store, about 2,300 requests a second in our estimate and not 50,000.
Rotation and reuse detection. Each refresh token works once. On a refresh, the server marks the old token as used and issues a new one in the same session family, in a single conditional write. If a used token turns up again, two parties hold the same token: the owner and a thief. The server cannot tell which one is calling, so it revokes the whole family and the person logs in again. A grace window of a few seconds for the old token tolerates a repeated request from a flaky network.
Where tokens live. On the web, put the refresh token in a cookie with three flags. HttpOnly hides it from page scripts, so an injected script cannot read it. Secure sends it only over HTTPS. SameSite=Lax or Strict stops other sites from making the browser send it, which blunts forged requests. Limit the cookie's path to the refresh endpoint, so it does not travel with every call, and keep the access token in memory rather than in local storage. On mobile, use the system's secure storage.
Give each signing key an ID in the token header. Then you can rotate keys on a schedule and keep publishing the old public key until the last token signed with it has expired.
Revocation across many devices
Interviewer asks
How does 'log out everywhere' really work when access tokens are checked with no lookup?
Make the session store the list of what each person is signed into: one row per device, with a family ID, a device name, the last-used time, a rough location and a revoked flag.
Read moreShow less: Revocation across many devices
The devices screen reads these rows, and 'log out' on one device sets the flag on one row. Key the table by user ID so both reads stay on one shard.
Log out everywhere sets the flag on all of the person's rows and raises a token version on the account by one, in one transaction. A password change or reset does the same. Every refresh checks that its session row is not revoked and that the version matches the account, so no old refresh token survives.
That leaves the access-token window. A token issued 2 minutes ago stays valid for 13 more. Choose what is acceptable:
- Accept up to 15 minutes for ordinary reads. It costs nothing and is the usual choice.
- For sensitive actions, such as changing an email address or moving money, check the session row or token version on the server before acting. It adds one read to a rare request.
- For an emergency, such as an account takeover, keep a small denylist of user or session IDs revoked in the last 15 minutes. Each gateway holds it in memory, fed by an event stream. Entries can expire after 15 minutes, because by then the tokens have expired anyway, so the list stays tiny.
A shorter access token shrinks the window but raises the refresh load. At 5 minutes the load triples, to about 6,700 a second (2M ÷ 300 s). At 1 hour the window is too wide for most products.
Tell people what happened. Send an email on a login from a new device and on log out everywhere, and show the last-active time on each device so they can spot one they do not know. Delete session rows that have been unused for 30 days, with a TTL, so the table stays small.
Step 5 of 6: Name the trade-offs
What the client holds
A 15-minute signed access token plus a rotating refresh token over An opaque session ID looked up on every request
API calls need no store read, and the store only sees refreshes. The cost is that an access token cannot be revoked before it expires.
Password storage
A slow, salted hash tuned to about 100 ms over A fast hash such as SHA-256
A stolen table takes an attacker far longer to crack. The cost is 100 ms of CPU for every attempt, so we throttle before hashing.
Stopping guessing
Growing delays and challenges for each account over A hard lockout after a few failures
Attackers cannot lock real people out on purpose. The cost is that a patient guesser can still try slowly, so we add per-IP limits and an optional second factor.
Access-token lifetime
15 minutes over 5 minutes, or 1 hour
It keeps refresh traffic near 2,200 a second and limits how long a stolen token is useful. Shorter triples the refresh load, and longer widens the window.
Step 6 of 6: Say what breaks next
Questions an interviewer asks once the design is on the board. Answer each one out loud first.
- How should password reset work?
Email a single-use link with a long random token, stored only as a hash, that expires in about 30 minutes. Using it sets the new password, raises the token version so every old session ends, and sends a notice. Give the same reply whether or not the email has an account.
- What if the refresh token is stolen from a browser?
The HttpOnly cookie makes it hard for a script to read it. If a thief does copy and use it, rotation catches the theft the first time the real owner refreshes, because the old token is shown a second time. The family is then revoked and the owner logs in again.
- How would you add social login?
Treat the provider as another way to prove who the person is. After the provider confirms the identity, the Auth service finds or creates the account and issues the same access and refresh tokens, so everything after login is unchanged. Link accounts only by an email address that the provider says is verified.
- How do you handle a credential-stuffing attack?
Attackers replay passwords leaked elsewhere, so each account sees only one or two tries and per-account limits never trigger. Add per-IP and per-device limits, check new passwords against leaked lists, challenge risky logins, and offer a second factor. Watch the overall failure rate, because a sudden jump is the signal.