Table of Contents
1. The Three Time-Based Claims
JWT defines three registered claims that control when a token is valid. All three use Unix timestamps — the number of seconds since January 1, 1970 00:00:00 UTC.
{
"sub": "user_12345",
"iat": 1735686000, // Issued: Jan 1, 2025 10:00:00 UTC
"nbf": 1735686000, // Valid from: Jan 1, 2025 10:00:00 UTC
"exp": 1735689600 // Expires: Jan 1, 2025 11:00:00 UTC
}iatWhen it was created
nbfWhen it becomes valid
expWhen it stops being valid
2. exp — Expiration Time
The exp claim is the most important time-based claim. It sets a hard deadline after which the token must be rejected. Without exp, a compromised token could be used indefinitely.
// Token valid for 1 hour from now
{
"exp": 1735689600 // Unix timestamp in seconds
}
// JavaScript: Set expiration to 1 hour from now
const exp = Math.floor(Date.now() / 1000) + 3600;Common mistake: Using milliseconds instead of seconds. JavaScript's Date.now() returns milliseconds, but JWT exp uses seconds. Always divide by 1000:Math.floor(Date.now() / 1000) + ttlInSeconds
Validation Logic
When a server receives a JWT, it checks:
const now = Math.floor(Date.now() / 1000);
if (payload.exp && now >= payload.exp) {
throw new Error("Token has expired");
}
// Token is still valid — proceed3. iat — Issued At
The iat claim records when the token was created. It's not used for expiration directly, but serves several purposes:
- Age checking: Reject tokens older than a certain threshold, even if
exphasn't passed - Revocation: Invalidate all tokens issued before a specific time (e.g., after a password change)
- Auditing: Know exactly when each token was generated for debugging and logging
// Reject tokens issued before user changed password
const passwordChangedAt = user.passwordChangedAt;
if (payload.iat && payload.iat < passwordChangedAt) {
throw new Error("Token issued before password change");
}4. nbf — Not Before
The nbf claim sets the earliest time a token can be used. Tokens presented before this time must be rejected. This is useful for:
- Scheduled access: Grant access starting at a future date
- Pre-issued tokens: Generate tokens in advance that activate later
- Time-limited promotions: Tokens that only work during a specific window
// Token valid from tomorrow at midnight to 7 days later
{
"nbf": 1735776000, // Tomorrow 00:00 UTC
"exp": 1736380800, // 7 days after nbf
"sub": "user_12345",
"access": "premium_content"
}5. Recommended Token Lifetimes
There's no one-size-fits-all answer, but here are industry-standard recommendations:
| Token Type | Recommended Lifetime | Why |
|---|---|---|
| Access token | 15 minutes — 1 hour | Short-lived to limit damage if compromised |
| Refresh token | 7 — 30 days | Long-lived but stored securely, used only to get new access tokens |
| ID token (OIDC) | 5 — 60 minutes | Used once for authentication, then exchanged |
| Email verification | 24 — 72 hours | One-time use, give user time to check email |
| Password reset | 15 — 60 minutes | Security-sensitive, should expire quickly |
| API key token | 90 days — 1 year | Long-lived for automation, but should support revocation |
Rule of thumb: The more sensitive the action the token authorizes, the shorter its lifetime should be. A token that can read public data can live longer than one that can delete accounts.
6. Access Tokens vs Refresh Tokens
The standard pattern for balancing security and user experience is the access + refresh token model:
The Flow
- User logs in → server issues an access token (15 min) and a refresh token (30 days)
- Client uses the access token for API requests
- Access token expires → client sends the refresh token to get a new access token
- Server validates the refresh token and issues a new access token
- Refresh token expires → user must log in again
// Access token — short-lived, sent with every request
{
"sub": "user_12345",
"role": "editor",
"type": "access",
"iat": 1735686000,
"exp": 1735686900 // 15 minutes
}
// Refresh token — long-lived, stored securely
{
"sub": "user_12345",
"type": "refresh",
"jti": "refresh_abc123",
"iat": 1735686000,
"exp": 1738278000 // 30 days
}For a deeper dive into these patterns, see our authentication patterns guide.
7. Handling Clock Skew
In distributed systems, server clocks may not be perfectly synchronized. A token issued by Server A might appear "from the future" to Server B if their clocks differ by a few seconds.
// Allow 30 seconds of clock skew
const CLOCK_SKEW_SECONDS = 30;
const now = Math.floor(Date.now() / 1000);
// Check expiration with tolerance
if (payload.exp && now >= payload.exp + CLOCK_SKEW_SECONDS) {
throw new Error("Token has expired");
}
// Check nbf with tolerance
if (payload.nbf && now < payload.nbf - CLOCK_SKEW_SECONDS) {
throw new Error("Token is not yet valid");
}Recommended tolerance: 30-60 seconds is standard. More than 5 minutes suggests a clock synchronization problem that should be fixed at the infrastructure level (use NTP).
8. What to Do When a Token Expires
When a JWT expires, the server should return a 401 Unauthorized response. The client then decides how to handle it:
Option 1: Refresh the Token
If you have a refresh token, use it to silently get a new access token without interrupting the user.
Option 2: Re-authenticate
If no refresh token is available or it's also expired, redirect the user to the login page.
Option 3: Proactive Refresh
Check the token's exp before each request. If it expires within the next few minutes, refresh proactively to avoid failed requests.
// Proactive refresh — check before each API call
function isTokenExpiringSoon(token, thresholdSeconds = 300) {
const payload = JSON.parse(atob(token.split('.')[1]));
const now = Math.floor(Date.now() / 1000);
return payload.exp - now < thresholdSeconds;
}
// Usage
if (isTokenExpiringSoon(accessToken)) {
accessToken = await refreshAccessToken(refreshToken);
}
await callApi(accessToken);9. How to Check JWT Expiration
You can check whether a JWT has expired without verifying its signature — just decode the payload and compare the exp claim to the current time.
JavaScript
function isJwtExpired(token) {
const payload = JSON.parse(atob(token.split('.')[1]));
if (!payload.exp) return false; // No expiration set
return Date.now() >= payload.exp * 1000;
}Python
import base64, json, time
def is_jwt_expired(token):
payload = token.split('.')[1]
payload += '=' * (4 - len(payload) % 4)
decoded = json.loads(base64.urlsafe_b64decode(payload))
if 'exp' not in decoded:
return False
return time.time() >= decoded['exp']Using Our Tool
The fastest way: paste your token into our JWT decoder. The decoded payload shows the exp timestamp, which you can compare to the current time.
Check Your Token Expiration
Paste a JWT to instantly see its expiration time, issued-at timestamp, and whether it's still valid.