Debugging JWT Expiry and Clock Skew Errors
JWT expiry errors are almost always a clock problem. The exp claim is a Unix timestamp in seconds (not milliseconds), and a mismatch in units produces tokens that appear expired the moment they are issued.1 Paste any token into the decoder above to read the exp value as a human-readable date, immediately confirming whether the token was already expired when you received it.
Clock skew is the second most common cause of expiry failures. Client and server clocks drifting by more than 60 seconds cause a valid token to appear expired on the server side. JWT libraries typically allow a configurable clock skew tolerance (usually between 30 and 300 seconds) to absorb minor drift.2 Because both NTP synchronisation failures and container clock drift produce this symptom, checking the decoded iat and exp timestamps against the current server time is the first debugging step.
Reading the exp claim
The exp claim is a Unix timestamp: the number of seconds elapsed since January 1, 1970, at 00:00:00 UTC. Converting it to a human-readable date requires multiplying by 1000 (for JavaScript's new Date()) or using datetime.fromtimestamp() in Python. Pasting the token into the decoder above does this conversion automatically, showing 'Expires at 2026-06-01 14:30:00 UTC' rather than a raw number. Consequently, the most common debugging mistake: reading the number and assuming it is milliseconds: is immediately visible when the year shows as 1970 or 47000 CE.
Comparing exp with server time
After reading the decoded expiry, compare it with the server clock that rejected the token. A fresh token should still have minutes or hours remaining; a token shown as already expired points to milliseconds, clock drift, or issuer-side misconfiguration. If the decoded exp is more than a few seconds in the past but the token was just issued, the most likely cause is a server clock that is running ahead of the issuer clock, which you can confirm by comparing the current time on both machines using a shared reference like time.google.com.
Understanding clock skew
Clock skew occurs when the clock on the machine that issued the token differs from the clock on the machine that receives it, causing the verifier to evaluate the exp claim against the wrong reference time. A server with a clock 90 seconds ahead of the issuer rejects a valid token as expired even though the issuer created it with the correct future exp, and the rejection persists until the clocks are re-synchronized. Verifying that both clocks sync to NTP eliminates systematic skew, while Docker containers and virtual machines are particularly prone to clock drift after host sleep or migration, so always check the container clock independently of the host when debugging skew in containerised environments.
The nbf and iat claims
Two additional time claims complement exp by defining the full validity window for a token.1 nbf (not before) specifies the earliest time at which the token may be accepted: a token with nbf in the future is valid only after that timestamp, even if the exp is far in the future, making it useful for tokens that should not be valid immediately upon issuance. iat (issued at) records when the token was created, enabling you to calculate how long ago it was minted and to detect tokens with suspiciously old issue times that may indicate a replay attempt. Yet nbf is optional and many providers omit it entirely, so if a token is rejected as 'not yet valid', check the nbf value in the decoder rather than assuming an expiry problem.
Clock drift in containerised and serverless environments
Docker containers and Lambda functions inherit the host clock, but the inherited clock can drift relative to real time after the host sleeps, migrates, or skips NTP sync. A container clock that drifts 90 seconds ahead of an Auth0 issuer clock produces tokens whose exp appears already passed at the moment of issuance. Check container clock accuracy with date -u in a running container and compare the output to an external time source before concluding the problem originates on the issuer side.
Configuring clock skew tolerance per library
Configuring clock skew tolerance in your JWT library absorbs minor drift without requiring infrastructure changes. In PyJWT, pass leeway=timedelta(seconds=60).3 In jose (JavaScript), pass clockTolerance: 60.4 In .NET, set ClockSkew = TimeSpan.FromSeconds(60) in TokenValidationParameters.5 Set tolerance to the smallest value that stops spurious rejections: large tolerances extend the window during which a near-expired stolen token remains accepted by your API. A tolerance of 30 seconds covers most NTP-synchronized environments, while values above 300 seconds are rarely justified and should trigger a review of your infrastructure clock synchronization before being applied as a configuration fix.
Comparing clocks before adjusting token lifetimes
When debugging a single provider, compare the issuer clock, the API server clock, and the client clock before changing token lifetimes. A provider may issue a token with a valid 300-second lifetime, while your server clock reads 90 seconds ahead and rejects it immediately. Fixing the clock source often resolves the issue more safely than increasing every token lifetime across the platform.
A clock fix also benefits every token your system issues, not just the one failing request you investigated. Extending lifetimes instead hides the drift and widens the window in which a stolen token stays valid. Re-check the clock after the change to confirm the skew falls inside your configured tolerance before you move on to other debugging steps.
When to use this
When a 401 Unauthorized error mentions 'expired token', 'invalid exp', or 'clock skew', debugging JWT expiry and clock skew takes less time than adding debug logging to your backend. It converts iat, nbf, and exp to readable local time automatically, so you do not have to do the seconds-to-date math by hand while debugging.
Examples
exp value appears to be in the past on a fresh token
If exp decodes to a timestamp in 1970 or shows a nonsensical date, the issuer sent exp in milliseconds instead of seconds. Divide by 1000 to get the correct Unix timestamp.
Token is valid in the issuer dashboard but rejected by your API
Compare the decoded iat and exp values against your server's current time using date -u on Linux. A gap larger than your clock skew tolerance (typically 60 seconds) confirms a drift problem.
- 1.
M. Jones et al., "JSON Web Token (JWT)," RFC 7519, IETF, May 2015. https://datatracker.ietf.org/doc/html/rfc7519
- 2.
Wikipedia, "Network Time Protocol," en.wikipedia.org, accessed June 2026. https://en.wikipedia.org/wiki/Network_Time_Protocol
- 3.
jpadilla/pyjwt, "jwt/__init__.py," github.com, accessed June 2026. https://github.com/jpadilla/pyjwt/blob/master/jwt/__init__.py
- 4.
panva/jose, "docs/jwt/verify/interfaces/JWTVerifyOptions.md," github.com, accessed June 2026. https://github.com/panva/jose/blob/main/docs/jwt/verify/interfaces/JWTVerifyOptions.md
- 5.
Microsoft, "TokenValidationParameters.ClockSkew Property," learn.microsoft.com, accessed June 2026. https://learn.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.clockskew