Decode JWT exp and iat Timestamps — Check When Tokens Expire
Table of Contents
JWT (JSON Web Token) authentication breaks for one reason more than any other: clock skew between systems. The token has an exp claim that is a Unix timestamp. Your server checks current time against that timestamp. If they disagree by more than a few seconds, the token is rejected and the user gets logged out.
Decoding the exp and iat values is the first step in debugging any JWT auth issue. This guide shows how, plus the common patterns for token timing bugs.
The Three Time Claims in a JWT
RFC 7519 defines three timestamp claims for JWT. All three are Unix timestamps in seconds (10 digits, not milliseconds — this trips up JavaScript developers constantly).
| Claim | Meaning | Required |
|---|---|---|
| iat | Issued At — when the token was created | No, but typically present |
| exp | Expiration — token is invalid after this time | Strongly recommended |
| nbf | Not Before — token is invalid before this time | Optional, rarely used |
To decode these you do not need the signing key — JWT payloads are base64-encoded JSON, not encrypted. You can read them with any base64 decoder. The signature only matters for verifying authenticity, not for inspecting contents.
Quick decode
Use the free JWT decoder to paste a token and see the header, payload, and timestamps. Then paste the exp and iat values into the Unix timestamp converter for human-readable dates.
Common JWT Timing Bugs
Bug 1: Server clock skew
Token issuer and validator have different system clocks. Server A issues a token with iat = now. Server B receives it three seconds later but its clock is six seconds behind. Server B sees iat in the future, rejects the token as "not yet valid". The fix is to sync clocks with NTP and add a small leeway window (typically 30-60 seconds) when validating.
Bug 2: Milliseconds vs seconds
Some libraries get the JWT time format wrong. The spec says seconds. JavaScript developers default to milliseconds. A token with exp: 1711000000000 is a token that expires in the year 56000 — clearly wrong, but the validator will accept it as "not expired" forever. The reverse bug (a token with exp in seconds being interpreted as milliseconds) makes the token expire 1000 times sooner than intended.
Bug 3: Timezone confusion
Unix timestamps have no timezone. They are always the count of seconds since 1970 UTC. If your code does new Date(exp).toLocaleString() and the server is in EST, the displayed string is in EST but the underlying value is still UTC. This is fine for display but causes bugs when developers manually compute "expires in 1 hour from now" using local time arithmetic.
Bug 4: exp in the past at issue time
Refresh logic that subtracts seconds instead of adding them, or that uses signed integers and overflows. Symptom: tokens are expired the instant they are issued. Easy to spot by decoding a fresh token and checking exp against current time.
Sell Custom Apparel — We Handle Printing & Free ShippingValidation Patterns Across Languages
// JavaScript / Node.js
const decoded = JSON.parse(atob(token.split('.')[1]));
const now = Math.floor(Date.now() / 1000);
const isExpired = decoded.exp < now;
const expiresIn = decoded.exp - now; // seconds until expiry
// Python
import json, base64
payload = json.loads(base64.urlsafe_b64decode(token.split('.')[1] + '=='))
now = int(time.time())
is_expired = payload['exp'] < now
// Go
import "github.com/golang-jwt/jwt/v5"
parsed, _ := jwt.Parse(tokenStr, keyFunc)
exp, _ := parsed.Claims.GetExpirationTime()
isExpired := exp.Before(time.Now())
All three patterns do the same thing: decode the payload, extract exp, compare to current time. The actual JWT libraries do this for you with a leeway window built in, but if you are debugging an auth issue you often need to do it manually to see the raw values.
Adding leeway for clock skew
const LEEWAY_SECONDS = 30;
const isExpired = decoded.exp + LEEWAY_SECONDS < now;
30 seconds is the typical default. Higher values reduce clock-skew rejections but expand the window during which a stolen token still works after it should have expired. Pick based on how strict your security model is.
How to Debug a "Token Expired" Error
- Get a fresh failing token. Reproduce the error and capture the exact JWT string from the request headers.
- Decode the payload. Paste the token into a JWT decoder. You will see a JSON object with the claims.
- Extract iat and exp. Both should be 10-digit Unix timestamps. If you see 13 digits, you have a milliseconds bug.
- Convert iat to a human date. Paste it into the timestamp converter. Confirm it matches when you actually requested the token (within a few seconds).
- Convert exp to a human date. Confirm it is in the future relative to when you sent the failing request.
- Check the validator's clock. SSH into the validating server, run
date, compare to your local clock and to the iat value. Any drift over 30 seconds is suspicious. - Check leeway settings. Look at the JWT library config in the validator. The default leeway varies by library.
Most "token expired" errors turn out to be clock skew between servers, not actual expiration. NTP everywhere fixes 80% of these. The remaining 20% are usually milliseconds-vs-seconds bugs in custom token issuance code.
For a quick decode of any token, the JWT decoder shows the payload instantly without sending the token to a server.
Try It Free — No Signup Required
Runs 100% in your browser. No data is collected, stored, or sent anywhere.
Open Free Unix Timestamp ConverterFrequently Asked Questions
What is the JWT exp claim?
exp (expiration time) is a JWT claim that contains a Unix timestamp in seconds. After this time, the token is considered invalid and validators should reject it. It is defined in RFC 7519 as a registered claim and is the standard way JWTs encode their expiration.
What is the difference between iat and exp in a JWT?
iat (issued at) is the Unix timestamp when the token was created. exp (expiration) is when the token becomes invalid. Both are seconds since the Unix epoch. The difference between them is the token lifetime — typically 15 minutes for access tokens and 7-30 days for refresh tokens.
Why does my JWT expire immediately after issuing?
Two common causes. First, the issuer set exp in the past due to a code bug (subtracting instead of adding, or wrong integer overflow). Second, the issuer used milliseconds instead of seconds for exp, causing the validator to interpret it as a date 1000 times sooner than intended. Decode the token and check the exp value.
Can I change a JWT exp without re-signing the token?
No. The exp is part of the signed payload. Changing it invalidates the signature, which any properly-configured validator will reject. The only way to extend a token is to issue a new one with a later exp — that is what refresh tokens are for.
How do I check if a JWT is expired without using a library?
Decode the payload (it is base64url-encoded JSON, not encrypted). Read the exp field. Compare to the current Unix timestamp. If exp is less than now, the token is expired. This is exactly what JWT libraries do internally — there is no magic.
What is JWT leeway and why does it matter?
Leeway is a small time window (usually 30-60 seconds) added when validating exp and iat to account for clock skew between servers. Without it, every minor clock drift causes valid tokens to be rejected. Too much leeway expands the window during which stolen tokens still work.

