JWT Token Expiry — Managing Token Lifetimes and Expiration Strategies
In this tutorial, you will learn about JWT Token Expiry. We cover key concepts, practical examples, and best practices to help you master this topic.
JWT token expiry is managed through the exp claim which sets an absolute expiration timestamp, after which the token is rejected by the server.
What You'll Learn
How the exp claim works, choosing appropriate TTL values, handling clock skew, and implementing silent token refresh for seamless user experience.
Why It Matters
Token expiry is the primary defense against stolen tokens. Short expiry limits damage but creates more refresh requests. Long expiry improves UX but increases risk. Finding the right balance is critical.
Real-World Use
Google APIs use 3600-second access tokens. GitHub allows configurable token expiry (30-90 days for personal access tokens). Banking APIs use 5-minute tokens for high-security operations.
flowchart LR
A["Token Issued\niat + exp"] --> B["Active Period"]
B --> C{"Time > exp?"}
C -->|"No"| D["Process Request"]
C -->|"Yes"| E["401 Expired"]
E --> F["Client Refreshes"]
F --> G["New Token Issued"]
G --> B
style A fill:#dbeafe,stroke:#2563eb
style B fill:#dcfce7,stroke:#16a34a
style E fill:#fecaca,stroke:#dc2626
style G fill:#dbeafe,stroke:#2563eb
The exp Claim
The exp claim contains a Unix timestamp (seconds since epoch) indicating when the token expires.
{
"exp": 1718000000,
"iat": 1717996400
}
The token above was issued at timestamp 1717996400 and expires at 1718000000 — a 1-hour lifetime.
Choosing Token TTL
| Application | Access Token TTL | Reasoning |
|---|---|---|
| Banking API | 5 minutes | Maximum security |
| Standard Web App | 15 minutes | Good balance |
| Mobile App | 1 hour | Less frequent refresh |
| Internal Service | 1 hour | Trusted network |
| IoT Device | 24 hours | Limited connectivity |
Code Example: Handling Token Expiry
import jwt
import datetime
import time
SECRET = "your-secret"
# Issuing a token with 1-hour expiry
def issue_token(user_id):
payload = {
"sub": user_id,
"iat": datetime.datetime.utcnow(),
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}
return jwt.encode(payload, SECRET, algorithm="HS256")
# Client-side: check expiry before making request
def is_token_expired(token):
try:
# Decode without verification to check expiry
payload = jwt.decode(
token,
options={"verify_signature": False}
)
return time.time() > payload["exp"]
except Exception:
return True
# Client-side: refresh if expired
def make_api_call(token, refresh_token):
if is_token_expired(token):
# Refresh the token
response = requests.post(
"https://auth.example.com/refresh",
json={"refresh_token": refresh_token}
)
if response.status_code == 200:
token = response.json()["access_token"]
else:
raise Exception("Session expired. Re-login required.")
# Make the actual API call
return requests.get(
"https://api.example.com/data",
headers={"Authorization": f"Bearer {token}"}
)
Handling Clock Skew
Servers and clients may have slightly different clocks. JWT libraries typically allow a small leeway (default 0 seconds).
# Allow 30 seconds of clock skew
payload = jwt.decode(
token,
SECRET,
algorithms=["HS256"],
leeway=30 # seconds of tolerance
)
Grace Period Strategy
Some systems implement a grace period where an expired token is accepted once to allow a seamless refresh without the client seeing a 401:
# Server-side: allow expired tokens for refresh endpoint only
@app.route("/api/auth/refresh", methods=["POST"])
def refresh():
data = request.get_json()
old_access = data.get("access_token")
try:
# Allow expired tokens (but not invalid ones)
payload = jwt.decode(
old_access,
SECRET,
algorithms=["HS256"],
options={"verify_exp": False} # Accept expired
)
# Additional checks...
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid token"}), 401
Common Mistakes
1. Not Setting exp Claim
Tokens without expiry never expire. If stolen, the attacker has indefinite access. Always set exp.
2. Making Tokens Permanent
24-hour access tokens with 30-day refresh tokens are reasonable. Annual tokens are dangerous.
3. Forgetting Clock Skew
If servers are 2 minutes apart, a token valid on the issuer is expired on the verifier. Use leeway.
4. Not Checking Expiry Client-Side
Making a request with a definitely expired token wastes a round-trip. Check expiry client-side before calling.
5. Using String Dates Instead of Timestamps
The exp claim must be a numeric Unix timestamp. String dates are not standard and may not be validated.
Practice Questions
- What claim is used for JWT expiration?
- How does the server handle an expired token?
- What is clock skew and how do you handle it?
- Why should access tokens be shorter-lived than refresh tokens?
- What is a grace period for token refresh?
Answers:
- The
exp(expiration) claim, which contains a Unix timestamp. - The server returns 401 Unauthorized. Most JWT libraries throw
ExpiredSignatureError. - Clock skew is the time difference between servers. Handle it with the
leewayparameter in JWT decode. - Access tokens are sent with every request — short expiry limits exposure. Refresh tokens are sent rarely.
- A grace period allows an expired access token to be used once for the refresh endpoint, enabling seamless renewal.
Challenge: Implement a token refresh system with a 30-second grace period where expired tokens can still refresh, but the new token gets a reduced TTL as a safety measure.
FAQ
Mini Project
Build a token management system that issues tokens with configurable TTL, validates expiry on every request, handles clock skew with leeway, and provides a refresh endpoint with grace period.
What's Next
Now learn about JWT Token Storage — where to store tokens securely on web and mobile clients.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro