JWT expiry is one of those things developers often set arbitrarily at first, then never revisit. A 30-day access token feels convenient during development. In production, it means a compromised token stays valid for up to a month.
Understanding why expiry windows matter and how the refresh token pattern manages them well is fundamental to implementing JWT authentication correctly. This post covers both.
Why JWTs Cannot Be Revoked Like Session Tokens
Traditional session tokens work through a server-side lookup. The token is an opaque identifier; the server checks whether that identifier exists in the session store. Revocation is immediate: delete the record and the token stops working on the next request.
JWTs work differently. The token is validated cryptographically using a key. If the signature is valid and the claims pass, the token is accepted. The server does not need to look anything up. This is what makes JWTs fast and stateless.
The same property is what makes revocation hard. Once issued, a JWT is valid until it expires. There is no built-in mechanism to invalidate it early. Logging out a user, revoking access after a password change, or invalidating a compromised token all require either waiting for expiry or maintaining a blocklist of revoked token IDs that you check on every request (which partially recreates the stateful lookup you were trying to avoid).
The RFC 7519 specification documents the exp (expiration) and jti (JWT ID) claims. The jti claim is what enables token blocklisting if you need immediate revocation.
The Practical Risk of Long Expiry Windows
A JWT access token with a 7-day expiry means that a token stolen from a client stays valid for up to 7 days without any action from you. You can add the jti to a blocklist, but that requires knowing the token was stolen, which is the harder problem.
Shorter expiry windows reduce this window. If your access tokens expire in 15 minutes, a stolen token has a very short validity period. The attacker has to use it within that window, and any requests from an unexpected source are detectable because the legitimate client will try to refresh and the system will have two active sessions for the same user.
The tradeoff is user experience. If the token expires and there is no automatic refresh, the user sees an authentication error. This is why short-lived access tokens almost always come with a refresh token mechanism.

Photo by panumas nikhomkhai on Pexels
How Refresh Tokens Work
The refresh token pattern separates two concerns:
- Access token: short-lived credential used to authenticate API requests. Expires in minutes.
- Refresh token: longer-lived credential used only to obtain new access tokens. Stored securely on the client; not sent with every API request.
When the access token expires, the client sends the refresh token to the token endpoint. If the refresh token is valid and not revoked, the server issues a new access token (and optionally a new refresh token). The user stays logged in without needing to re-authenticate.
Refresh tokens can be revoked in a database because they are relatively few in number and are not validated on every request. A user logout revokes the refresh token, which stops new access tokens from being issued. Existing access tokens remain valid until they expire, but if the expiry window is 15 minutes, the practical impact is small.
Auth0 has detailed documentation on the refresh token pattern and rotation strategies. Token rotation, where each use of a refresh token issues a new refresh token and invalidates the old one, closes a class of token reuse attacks.
Choosing the Right Expiry Windows
There is no universal correct answer, but there are established patterns:
Access tokens: 5-30 minutes is a common range for user-facing APIs. The shorter the window, the smaller the compromise window, but the more frequent the refresh overhead. 15 minutes is a reasonable default for sensitive resources.
Refresh tokens: Hours to days, depending on the inactivity tolerance. A 24-hour refresh token works well for user sessions. A 30-day refresh token with inactivity sliding expiry (reset the expiry each time it is used) works for persistent login flows.
Long-lived API access: Machine-to-machine integrations where JWTs are used as service credentials can have longer access token expiry if immediate revocation is not required and the client environment is trusted. Some internal service integrations use 1-hour or 24-hour tokens with explicit re-issuance rather than refresh tokens.
The OpenID Connect specification defines the standard token types and flows used by modern identity providers. If you are integrating with an external identity provider, their documentation will specify recommended expiry windows for their platform.
Reading Expiry in a Decoded Token
The exp claim is a Unix timestamp. To check the expiry on a token without writing code, paste it into a JWT decoder. The EvvyTools JWT Decoder converts the timestamp to a human-readable value with a live countdown, which makes it easy to see how much time remains on a token during debugging.
The decoder also shows the iat (issued at) and nbf (not before) claims alongside expiry. Comparing all three shows you the full validity window and whether clock skew between services is affecting validation.
If you are reviewing the expiry windows used across environments, decoding a sample token from each environment is faster than reading configuration files. The differences are immediately visible in the decoded payload.

Photo by Beyzaa Yurtkuran on Pexels
Token Expiry and Clock Skew
One practical problem with short expiry windows is clock skew between services. If your token issuer and token validator have clocks that differ by more than a few seconds, a freshly issued token can fail validation because the validator thinks the nbf (not before) time is still in the future.
Most JWT libraries include a leeway parameter specifically for this reason. Setting a leeway of 5-10 seconds accounts for minor clock differences without meaningfully widening the effective expiry window. If you are seeing intermittent "token not yet valid" errors that appear immediately after login, clock skew is the most likely cause.
NTP synchronization across your services prevents the worst cases. For cloud deployments, the managed time sync provided by the cloud provider usually keeps drift under 1 second, which is within any reasonable leeway setting.
Debugging Expiry Failures Systematically
When an expiry-related error appears, the debugging process is:
- Decode the token and check the
exptimestamp. Is it actually in the past? - If expiry looks correct, check whether the validator's clock is ahead of the issuer's clock.
- If both look correct, check whether the token was reissued but the client cached the old one.
- Check whether the refresh token flow is working correctly: is the client requesting a new access token before expiry, or only after the first 401?
For step 1, the EvvyTools JWT Decoder shows the expiry countdown and the full claim set. For steps 2-4, structured logging of the token's exp and iat on authentication failures makes the pattern clear without needing to capture raw tokens.
The longer guide on How to Debug JWT Authentication Errors covers the full range of JWT failure modes, not just expiry. If you are working through a series of authentication errors, it walks through the systematic approach to narrowing down the cause.
The Takeaway
JWT expiry is not a detail to set once and forget. The window you choose determines your practical security posture for compromised tokens. Short access token expiry plus a refresh token mechanism is the standard pattern for a reason: it keeps the compromise window small without forcing users to re-authenticate constantly. Building this correctly from the start is easier than retrofitting it after discovering why long expiry is a problem.
Top comments (0)