June 8, 2026
· 10 min readThe Ticking Clock: Why Short-Lived JWTs Are Your Best Defense
Stateless JWTs can't be revoked on demand — once issued, they live until they expire. This deep dive covers why short lifetimes (5–15 min) are the strongest control you have, how the access/refresh token split actually works, refresh token rotation with reuse detection, and the sender-constrained token guidance from RFC 9700 (Jan 2025).

TL;DR
- A standard JWT is stateless and self-contained — the server doesn't store it, so it cannot be revoked on demand. It stays valid until its
expclaim is reached. - The fix isn't clever revocation — it's time. Keep access tokens to 5–15 minutes so the blast radius of a stolen token is measured in minutes, not hours.
- Pair a short-lived stateless access token with a long-lived stateful refresh token. Revoke by killing the refresh token in your DB; the access token expires on its own.
- Add refresh token rotation with reuse detection — RFC 9700 (Jan 2025) treats this as standard practice for public clients, not an optional extra.
- For high-value APIs, layer on sender-constrained tokens (DPoP / mTLS) so a stolen token is useless without the client's private key.
Why this matters
JWTs gave us fast, horizontally scalable, stateless authentication. No session lookup on every request, no shared session store to bottleneck your fleet. That's the whole appeal.
But statelessness has a sharp edge. The same property that makes JWTs scale — the server not tracking them — is the property that makes them impossible to invalidate once issued. Most developers learn this the hard way, usually the first time a "logout" button doesn't actually log anyone out.
This post walks through why that happens, and why keeping your token validity as small as possible is the single most effective control you have.
How a JWT actually works
A JWT is a self-contained cryptographic credential — think of it as a backstage pass that carries everything needed to verify identity. Three base64url parts separated by dots: header.payload.signature.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← header (alg, typ)
.eyJzdWIiOiIxMjM0IiwiZXhwIjoxNzE3MDI2NjAwfQ ← payload (claims)
.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk ← signatureThe payload holds claims like:
{
"sub": "1234567890",
"name": "John Doe",
"role": "admin",
"iat": 1717023000,
"exp": 1717026600
}iat(issued at) — Unix timestamp when the token was minted.exp(expiration) — Unix timestamp after which the token is invalid.
The server verifies the signature with its key, checks exp, and trusts the claims. No database call. That's the speed — and the problem.
The invalidation dilemma
In classic session-based auth, the server keeps a record of every active session. Compromise, logout, or a permission change? Delete the row. Access is revoked instantly.
JWTs flip this. Because the backend doesn't store or track the token, a JWT cannot be invalidated by itself. Once issued, it's valid until exp.
Here's the terrifying part: the user clicks logout, but if an attacker already grabbed the JWT, they keep getting 200 OK until the token expires on its own. A 24-hour token means a 24-hour breach.
⚠️ Warning: "Logout" on a stateless JWT only deletes the token from your client. It does nothing to a copy an attacker already holds.
The danger of stale data
Revocation isn't the only casualty. A JWT is a point-in-time snapshot of identity and permissions, frozen at issuance.
Picture an admin who gets demoted or removed from an org. If they were handed a 24-hour token claiming "role": "admin", that claim keeps asserting admin privileges for the rest of the day — long after you revoked the role in your database.
| Scenario | Session-based auth | Long-lived JWT (24h) | Short-lived JWT (15m) |
|---|---|---|---|
| User logs out | Instant | Valid until exp ❌ |
≤15 min residual ✅ |
| Role demoted | Instant | Up to 24h stale ❌ | ≤15 min stale ✅ |
| Account flagged | Instant | Up to 24h access ❌ | ≤15 min access ✅ |
| Server overhead | DB lookup / request ❌ | None ✅ | None ✅ |
For any operation touching dynamic or sensitive permissions, a multi-hour authorization lag is simply unacceptable.
Why short lifespans are the mitigation
Because stateless tokens can't be cheaply revoked, the consensus across OWASP, RFC 9700, and major auth vendors is blunt: keep the access token's validity as short as possible — typically 5 to 15 minutes. An access token should be short-lived, and a good duration depends on the application and may be 5 to 15 minutes, while the refresh token should be valid for a longer duration.
🔒 Limiting the exposure window. If an attacker steals a token via XSS or network interception, the clock is already running. The "blast radius" is bounded by time — they get a few minutes before the token is dead weight.
⚡ Forcing permission refresh. A token that dies quickly forces the client to fetch a new one routinely. That refresh is where account changes — password resets, role demotions, de-provisioning — take effect almost immediately.
The numbers are real, not just folklore. A 2025 OWASP report attributed over 40% of authentication breaches in public APIs to misconfigured token handling, and one engineering team reported that 5–15 minute access tokens paired with strict refresh rotation cut unauthorized access incidents by 35% while maintaining session continuity.
💡 Tip: Tier your lifetimes by sensitivity. Short-lived tokens of 5–15 minutes suit high-security apps like banking; 1–24 hours fits general web apps; longer windows are reserved for refresh tokens or low-risk scenarios.
The hybrid approach: access + refresh tokens
The obvious objection: if tokens die every 15 minutes, won't users be logging in constantly?
No — because you issue two tokens at login:
- Short-lived stateless access token (5–15 min) — the JWT sent to your APIs for fast, stateless authorization.
- Long-lived stateful refresh token (days/weeks) — stored securely, used only to silently mint new access tokens in the background.
When you need to cut someone off — logout, or an account flagged for suspicious activity — you invalidate the stateful refresh token in your database. You kept the access token short, so the live one dies in minutes and the attacker is locked out without you ever needing a JWT denylist.
<?php
// Laravel-style sketch of the refresh exchange
public function refresh(Request $request)
{
$refresh = RefreshToken::where('token_hash', hash('sha256', $request->refresh_token))
->where('revoked', false)
->where('expires_at', '>', now())
->first();
if (! $refresh) {
return response()->json(['error' => 'invalid_grant'], 401);
}
// Rotation: kill the old refresh token immediately
$refresh->update(['revoked' => true]);
$newRefresh = RefreshToken::issueFor($refresh->user, family: $refresh->family);
return response()->json([
'access_token' => JWT::issue($refresh->user, ttl: 900), // 15 min
'refresh_token' => $newRefresh->plain, // new token
]);
}Breaking it down:
token_hash— store only a hash of the refresh token, never the raw value (same logic as password storage).ttl: 900— 15-minute access token in seconds.revoked => true— rotation invalidates the predecessor on every use.family— ties rotated tokens together so reuse detection can nuke the whole chain.
Refresh token rotation and reuse detection
A long-lived refresh token is now your highest-value secret, so it needs its own defenses. The current best practice — codified in RFC 9700 (Jan 2025) — is rotation with reuse detection. RFC 9700, published by the IETF in January 2025 as the updated Best Current Practice for OAuth 2.0 Security, makes refresh token rotation standard practice rather than an optional enhancement, alongside mandatory PKCE and sender-constrained tokens.
How reuse detection works: each refresh issues a new token in the same family and invalidates the old one. If an already-used, invalidated refresh token ever reappears, the server revokes the entire token family, forcing full re-authentication and limiting attacker persistence.
⚠️ Warning: Tight rotation can false-positive on legitimate concurrent requests — retries, multi-tab browsers, flaky mobile networks. A short reuse grace period (a few seconds where the previous token still works) avoids logging real users out on a dropped connection.
One honest caveat worth internalizing: rotation and sender-constraining reduce risk and limit exposure windows, but they don't detect active abuse — an attacker with a valid refresh token looks identical to a legitimate application, so behavioral monitoring is required to catch compromise during exploitation.
Sender-constrained tokens (DPoP / mTLS)
Short lifetimes shrink the window; rotation protects the refresh token. The next layer makes a stolen access token useless on its own.
A normal JWT is a bearer token — whoever holds it can use it. Sender-constrained tokens bind the token to a key the client must prove it owns on every request.
| Mechanism | RFC | Binds to | Best for |
|---|---|---|---|
| DPoP | RFC 9449 | Client-held private key (proof JWT per request) | SPAs, mobile, public clients |
| mTLS | RFC 8705 | Client TLS certificate | Service-to-service, confidential clients |
With DPoP, the client includes a proof-of-possession of its private key on each API request — a JWT signed with that key containing a hash of the access token — and the resource server validates both the access token and the DPoP proof to confirm the request comes from the legitimate client. A leaked token without the matching private key simply doesn't work.
💡 Tip: For browser apps, DPoP only goes so far — injected malicious code can run its own DPoP-keyed flow. The stronger recommendation for browser-based apps is the BFF (Backend-for-Frontend) pattern, where the SPA isn't an OAuth client at all and tokens live server-side in a confidential client.
Where to store tokens on the client
A short lifetime is wasted if the token is trivially stealable.
- ❌
localStorage/sessionStorage— readable by any JavaScript on the page, so a single XSS payload exfiltrates the token. - ✅
HttpOnly,Secure,SameSitecookies — invisible to JavaScript, sent only over HTTPS. Best for the refresh token. - ✅ BFF pattern — the browser never touches the tokens at all; the backend holds them and proxies API calls.
- ✅ OS secure storage (Keychain / Keystore) — for native mobile apps.
Production checklist
- Set access token
expto 5–15 minutes — tier by sensitivity (banking → 5; general web → up to 60). - Use a separate, stateful refresh token — store only its hash, give it a hard absolute lifetime (e.g. 7–14 days).
- Rotate refresh tokens on every use with reuse detection that revokes the whole family on replay.
- Never extend the refresh family's absolute lifetime on rotation — otherwise you've built an infinite session.
- Add a small reuse grace period to survive retries and multi-tab clients without false logouts.
- Validate signature,
exp,iss, andaudon every request — reject tokens minted for another service. - Sign with asymmetric keys (RS256/ES256) and centralize key management; never ship the secret to clients.
- Sender-constrain high-value tokens with DPoP or mTLS; use BFF for browser apps.
- Store tokens in HttpOnly Secure cookies or server-side — never
localStorage. - Monitor refresh patterns — rapid or geographically impossible refreshes signal compromise that rotation alone won't catch.
When short-lived JWTs are not the whole answer
Short lifetimes are necessary, not sufficient. Reach for additional state when you need:
- Instant, hard revocation — a "kill this token now" requirement (e.g. detected fraud) needs a denylist, which reintroduces a per-request lookup. Accept the state for the security.
- Truly long sessions with sensitive permissions — bind tokens (DPoP/mTLS) and shorten further rather than stretching
exp. - High-assurance environments — combine all layers plus behavioral monitoring; no single control is enough on its own.
Conclusion
When I implement stateless auth, I treat time as the primary security control. You can't easily revoke a JWT, so I stop trying to fight that and instead make the token's life so short that revocation barely matters — 15 minutes of exp, a stateful rotating refresh token behind it, and a database row I can flip to cut access.
Start there: a short access token, a rotating refresh token with reuse detection, and HttpOnly storage. Then layer DPoP or a BFF where the value of the data justifies it. The clock is the cheapest, most reliable defense you have — let it tick in your favor.
FAQ
Can you revoke a JWT before it expires?
Not directly. A standard JWT is self-contained and stateless, so the server doesn't track it. You revoke access by invalidating the stateful refresh token in your database and keeping the access token's lifetime short (5–15 min) so it dies on its own quickly. For instant kill-switch behavior you need a denylist, which reintroduces state.
How long should a JWT access token last?
OWASP and most security guidance recommend 5–15 minutes for sensitive applications. General web apps often use 15–60 minutes. Finance and healthcare typically stay at the 5–15 minute end. The refresh token then lasts days to weeks.
What is refresh token rotation?
Every time a client exchanges a refresh token for a new access token, the server issues a brand-new refresh token and immediately invalidates the old one. If an invalidated token is ever replayed, the server revokes the entire token family, forcing re-authentication. RFC 9700 (Jan 2025) treats rotation as standard practice for public clients.
Where should I store JWTs on the client?
Avoid localStorage and sessionStorage — they're readable by JavaScript and exposed to XSS. Prefer HttpOnly, Secure, SameSite cookies for the refresh token. For browser apps, the strongest pattern is a Backend-for-Frontend (BFF) that keeps tokens server-side entirely.
What are sender-constrained tokens?
Tokens cryptographically bound to a client's private key using DPoP (RFC 9449) or mTLS (RFC 8705). The client must prove possession of the key on every request, so a stolen token alone is useless. RFC 9700 recommends them as defense-in-depth alongside short lifetimes and rotation.
Does a short token lifetime alone stop token theft?
No — it shrinks the exposure window, but an attacker who steals a token still has those minutes. Short lifetimes are one layer. Combine them with refresh token rotation, sender-constrained tokens, and behavioral monitoring, since rotation and binding reduce risk but don't detect active abuse on their own.