Session Timeout: Why Your Session Needs to Die (and When)
Idle timeout, absolute timeout, frontend timers, and Redis with TTL. Understand the complete lifecycle of a session and how to protect users who forget to log out.
Mental model: Think of a session like a hotel keycard. It works for your stay (absolute timeout), stops working if you don't use it for a while (idle timeout), and the front desk can kill it remotely if your laptop gets stolen (server-side invalidation). A keycard that works forever isn't convenient, it's a liability.
You grab a coffee, leave your laptop open on the coworking desk, and come back 40 minutes later to find your admin dashboard still logged in. Anyone passing by had full access. Session timeout is what prevents this.
Sounds simple. But correct implementation involves backend, database, and frontend working together, and most devs configure only one.
Two Types of Timeout You Need to Know
Idle timeout: expires after X minutes without activity. User stops interacting, session dies. Example: 30 minutes.
Absolute timeout: expires after X hours regardless of activity. Even if the user is hammering the keyboard all day, the session has a hard ceiling. Example: 8 hours.
Why does absolute timeout exist? Idle alone allows eternal sessions for continuously active users. And if the token is compromised, an attacker can keep it alive forever just by pinging an endpoint every 29 minutes.
Idle timeout is the motion sensor that turns off the lights in an empty room. Absolute timeout is the janitor who locks the building at midnight, no matter who's still inside.
Quick comparison
| Type | Trigger | Typical value | Protects against |
|---|---|---|---|
| Idle | No user activity | 15-30 min | Unattended sessions (coffee break scenario) |
| Absolute | Time since login | 8-12 h | Long-lived stolen tokens, forgotten logouts |
| Sliding | Extends idle on each request | Idle window, capped by absolute | Balances UX and security (most common default) |
Sliding isn't really a third thing, it's idle timeout with rolling: true. But devs talk about it as its own concept, so it's worth naming.
A Short War Story
The 2022 GitHub/Heroku/Travis CI OAuth incident is the textbook case. Stolen OAuth tokens issued to Heroku and Travis CI integrations were used to clone private repos from dozens of organizations. The tokens didn't rotate, didn't have aggressive absolute expiry, and server-side revocation was slow. Once the keys were out, they kept working until humans noticed and pulled the plug.
The fixes GitHub rolled out afterward weren't exotic: shorter token lifetimes, better revocation tooling, and audit trails for token usage. Boring security wins, again.
Backend: The Session Owner
The backend creates, validates, and destroys. With Express:
rolling: true is critical: it re-sends the cookie on every request, resetting the idle timer. Without it, the timeout counts from login.
For absolute timeout, you need middleware:
Notice the split: maxAge handles idle (refreshed by rolling), the middleware handles absolute (checked against createdAt, never refreshed). Two different clocks, one session.
Database: Redis Is the Best Choice
Why store sessions in a database?
- Invalidate sessions from any server (in clusters)
- Implement "log out from all devices"
- Get visibility into active sessions
Redis beats relational databases here because its native TTL deletes expired sessions automatically, no cron jobs:
If you use SQL, don't forget periodic cleanup. Without it, the sessions table grows forever:
Redis TTL is a self-cleaning oven. Postgres sessions are a sink full of dishes someone has to wash. Pick your chore.
Frontend: Reacting to Expiration
The frontend doesn't control the session, it reacts. Three things it must do:
1. Intercept the 401
2. Warning Timer (before expiration)
The frontend timer must be shorter than the backend's. If the backend expires in 30, the frontend warns at 25. That five-minute buffer is what gives the user time to click "Continue" before the session actually dies.
3. Extension Modal
When the user confirms, fire a request that renews the session. If they ignore it, automatic logout after the remaining time.
The Complete Flow
Login -> Backend creates session (Redis, sets cookie)
-> Frontend starts timer
Navigation -> Each request renews cookie (rolling)
-> Redis updates TTL
-> Frontend resets timer
Inactive 25 min -> Frontend shows modal
-> "Yes" -> request renews session
-> Ignores -> automatic logout
Session expires on backend (30 min)
-> Redis deletes (TTL)
-> Next request returns 401
-> Frontend intercepts -> redirects to /login
Common Anti-Patterns
Stuff I've seen ship to production. Don't be that team.
- Trusting the frontend timer. Frontend logs the user out at 30 min, but the backend cookie lives for 24 hours. Anyone with the cookie keeps going. The frontend is a courtesy, not a control.
- Forgetting
rolling: true. Users get kicked out 30 min after login while actively typing. Support tickets pile up, someone bumpsmaxAgeto 8 hours, and now you have no idle timeout at all. - No absolute timeout with JWTs. "It's just an access token, it's short-lived." Sure, but the refresh token lives 30 days and never rotates. Congrats, you built a permanent session with extra steps.
- Destroying the session but not the cookie.
req.session.destroy()on the server, but the client still holds the cookie. Depending on your store, the next request can resurrect state. Always callres.clearCookie('connect.sid')on logout.
FAQ
Can I just use a really long JWT and skip all this? You can, but you lose server-side invalidation. If the token leaks, you can't kill it. You can only wait it out or rotate the signing key, which logs everyone out simultaneously. Stateful sessions trade a Redis lookup for control.
What's a safe default for idle timeout? 15-30 min for regular apps, 5-10 min for admin panels and anything touching money or PII. If in doubt, err shorter and add the warning modal.
Should the refresh token have an absolute timeout too? Yes. A refresh token without an absolute cap is a permanent login. 7-30 days is typical, and rotate it on every use (refresh token rotation). If a rotated token gets reused, that's your signal to revoke the entire family.
How do I implement "log out from all devices"?
Store a tokenVersion (or sessionGeneration) integer on the user record. Embed it in every session or JWT payload. On logout-all, increment it. Any session carrying the old version fails validation on the next request. One UPDATE, every device gone.
Does httpOnly really matter if I have a strong CSP?
Yes. CSP is defense in depth, not a replacement. One XSS bug, one misconfigured unsafe-inline, and your token walks out the door. httpOnly costs nothing.
Key Takeaways
- Configure both idle timeout and absolute timeout
- Use
rolling: truefor real idle timeout behavior - Redis with TTL is the best option for storing sessions
- The frontend should warn before the backend expires
httpOnly: trueon the cookie is critical to mitigate XSS- For JWT, access token (15 min) + refresh token (7 days, rotated) replaces traditional session timeout