A JWT is expired when the current time is equal to or later than its exp claim. exp is a plain number of seconds since 1 January 1970 UTC, so 1793372400 means October 30, 2026 at 15:00:00 UTC. To check a token, decode its middle part, read exp and compare it with the clock. Our free JWT decoder does it for you: paste the token and it shows Expired or Not expired, plus the exact date in your time zone and in UTC.
The decoder is free, you don't need an account, and it runs entirely in your browser. When we tested it on October 31, 2026, the page sent no request containing the token. Guests get 5 tool uses a day, and a free My Botsy account (100% free, no spam) removes that daily cap. Every output below is real: the tool runs come from mybotsy.com on October 31, 2026, and the library results come from PyJWT 2.15.1, jsonwebtoken 9.0.3 and jose 6.2.14, run the same day.
What the exp claim means
RFC 7519, the JWT standard, defines three time claims. All three are optional, and all three are NumericDate values: seconds, not milliseconds.
- exp (expiration time): the token "MUST NOT be accepted for processing" on or after this moment.
- nbf (not before): the token must not be accepted before this moment.
- iat (issued at): when the token was created. It doesn't invalidate anything by itself, but you can use it to work out the token's age.
For exp and nbf, the RFC lets implementers allow "some small leeway, usually no more than a few minutes, to account for clock skew". That leeway is why two systems can disagree about a token that expired a few seconds ago (more on that below).
How to check if a JWT is expired, step by step
- Copy the token. It's the long string with two dots, often after
Authorization: Bearerin a request header, or stored in a cookie or in localStorage. - Paste it into the JWT decoder. You don't need to trim it first. The tool strips
Authorization:,Bearer, quotes, spaces and line breaks, and tells you it did. - Read the Status row. It shows Expired, Not expired, Not valid yet (when
nbfis in the future), No expiry (no exp claim) or Expiry unreadable: exp is not a valid number (whenexpis there but isn't a number). - Check the Expires (exp) row. It shows the date in your time zone, the same moment in UTC, a relative time such as "yesterday" or "in 2 months", and the raw number.
- Look at Lifetime. This is
expminusiat. If an access token says 30 days, the problem may be how tokens are issued, not this one token. - Read the notes. The decoder warns about millisecond dates,
alg: noneand padded tokens. It also reminds you that it doesn't check the signature.
Worked examples (real decoder output)
We signed these test tokens with HS256 and a throwaway demo secret, then pasted them into mybotsy.com/jwt-decoder on October 31, 2026 at about 15:07 UTC, in a browser set to New York time.
Example 1: an expired token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzg4NDEiLCJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJhdWQiOiJvcmRlcnMtYXBpIiwiaWF0IjoxNzkzMzY4ODAwLCJleHAiOjE3OTMzNzI0MDB9.FJV2oXvW0F-Q7x6rzZRKosQG_DhxLQXXz87wW-Ctq60
| Row | Decoder output |
|---|---|
| Status | Expired |
| Issued at (iat) | Friday, October 30, 2026 at 10:00:00 AM EDT · 2026-10-30T14:00:00Z · yesterday · 1793368800 |
| Expires (exp) | Friday, October 30, 2026 at 11:00:00 AM EDT · 2026-10-30T15:00:00Z · yesterday · 1793372400 |
| Lifetime | 1 hour |
All three libraries agreed: PyJWT raised ExpiredSignatureError: Signature has expired, jsonwebtoken raised TokenExpiredError: jwt expired with expiredAt 2026-10-30T15:00:00.000Z, and jose returned ERR_JWT_EXPIRED.
Example 2: a valid token pasted with its header
We pasted Authorization: Bearer eyJhbGciOiJIUzI1NiIs…, with the full header line. The decoder showed "The input was cleaned up before decoding", then Not expired, with exp on Thursday, December 31, 2026 at 7:00:00 PM EST = 2027-01-01T00:00:00Z ("in 2 months") and a Lifetime of 61 days 9 hours. Note the date: in New York, a token that expires at midnight UTC on New Year's Day dies on New Year's Eve. Reading only the raw number, or only the local date, is how teams end up off by a day.
Example 3: not valid yet
A token with nbf 1796083200 and exp 1796169600 showed Not valid yet, with Not before = Monday, November 30, 2026 at 7:00:00 PM EST (2026-12-01T00:00:00Z). PyJWT returned ImmatureSignatureError, jsonwebtoken returned NotBeforeError: jwt not active. If a freshly issued token gets rejected, check nbf and the server clock before you blame the expiry.
Example 4: no exp at all
A payload of {"sub":"svc-reporting","iat":1790856000} showed No expiry (no exp claim), and all three libraries accepted it. A leaked token like this one works until the key is rotated. In PyJWT, options={"require": ["exp"]} turned the same token into MissingRequiredClaimError: Token is missing the "exp" claim.
Pitfalls that give the wrong answer
exp in milliseconds
Writing Date.now() straight into exp is the classic bug. We signed a token with "exp":1793462400000. The decoder warned "The exp value looks like milliseconds" read it as milliseconds, so it showed the real date, October 31, 2026 at 16:00 UTC, and the right status and lifetime. jsonwebtoken and jose both accepted it, so in practice it never expires. PyJWT rejected it, but only because of its iat check, which reported "The token is not yet valid (iat)", not because of exp. The fix is to divide by 1000 when you issue the token, and multiply by 1000 when you build a JavaScript Date from exp.
exp as a string
"exp":"1798761600" (with quotes) breaks the standard. jsonwebtoken rejected it with invalid exp value, and jose rejected it with "exp" claim must be a number. PyJWT accepted it and still enforced it: an expired string date raised ExpiredSignatureError. So the same token passes in one service and fails in another. The decoder shows "Not a valid date: it must be a number of seconds" next to the value, a warning note, and the Status row says Expiry unreadable: exp is not a valid number.
Clock skew and leeway
We made a token that had expired 20 seconds earlier. With no tolerance, all three libraries rejected it. With 30 seconds (leeway=30 in PyJWT, clockTolerance: 30 in jsonwebtoken and jose), all three accepted it. Keep the tolerance small. Every second you allow extends the life of every token, including stolen ones.
Decoding with base64 --decode
JWTs use base64url, which uses - and _ instead of + and /. The popular one-liner cut -d. -f2 | base64 --decode | jq .exp works until a payload contains one of those characters. On GNU coreutils 9.7, a payload segment containing - printed half the JSON, then base64: invalid input, and jq failed with Unfinished string. Map the characters first with tr '_-' '/+', or paste the token into the decoder, which reads base64url natively. For other Base64 problems, see how to decode Base64.
Decoding is not verifying
Anyone can write a token with exp in the year 2100. Reading exp is fine for debugging, or for deciding in the browser when to refresh. Before your server trusts a request, it must verify the signature with a fixed list of allowed algorithms. RFC 8725 lists attacks in which the algorithm was changed to none, and our decoder shows a red warning for alg: none.
Check expiry in code
These are display or refresh helpers, not security checks. We tested both against the tokens above: the expired token returned true, the valid and base64url tokens returned false, and the tokens with no exp or a string exp returned no date.
// JavaScript (browser or Node 16+)
function jwtExpiry(token) {
const part = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
const { exp } = JSON.parse(atob(part));
if (typeof exp !== 'number') return null; // no exp, or a string
return new Date(exp * 1000); // seconds -> ms
}
function isExpired(token, leewaySeconds = 0) {
const exp = jwtExpiry(token);
return exp !== null && Date.now() >= exp.getTime() + leewaySeconds * 1000;
}
# Python 3
import base64, json, time
def jwt_exp(token):
seg = token.split(".")[1]
seg += "=" * (-len(seg) % 4) # restore the padding JWTs drop
exp = json.loads(base64.urlsafe_b64decode(seg)).get("exp")
return exp if isinstance(exp, (int, float)) else None
def is_expired(token, leeway=0):
exp = jwt_exp(token)
return exp is not None and time.time() >= exp + leeway
The comparison is "greater than or equal", because the RFC says a token is invalid on or after exp. A server should rely on its library's verify function (jwt.decode with a key in PyJWT, jwt.verify in jsonwebtoken, jwtVerify in jose), which checks the signature and the dates together.
Security habits worth keeping
- Treat a live token like a password. Until it expires, anyone holding it can act as that user. Use a decoder that works locally, like ours, rather than pasting tokens into chats or tickets.
- Keep access tokens short-lived and require
expon the server. - Use a long random HS256 secret. PyJWT warned that our 20-byte demo key was "below the minimum recommended length of 32 bytes for SHA256". A 64-character secret from the password generator clears that bar easily. Our guide to creating a strong password explains why length matters.
- Nothing secret goes in the payload. It is only encoded, so anyone can read it with a Base64 decoder. If a decoded payload won't parse, run it through the JSON validator, and see how to fix invalid JSON.
Expiry dates matter for certificates too. If an "expired" error comes from HTTPS rather than from a token, see how to check an SSL certificate's expiration date.
Check your token now
Paste it into the free JWT decoder. It's free, it needs no account, the token stays in your browser, and you get the status, every date in your time zone and in UTC, and warnings for the traps above.
Frequently asked questions
How do I know when my JWT expires?
Decode the payload and read the exp claim, which counts seconds since 1 January 1970 UTC. Multiply it by 1000 to get a JavaScript date, or paste the token into the My Botsy JWT decoder, which shows exp in your time zone and in UTC.
Is a JWT expired exactly at the exp time?
Yes. RFC 7519 says a token must not be accepted on or after the exp time, so use greater than or equal when you compare. Many servers add a few seconds of leeway for clock skew.
Why does my server reject a token the decoder says is not expired?
The usual causes are a different server clock, an nbf in the future, a wrong audience or issuer, or a bad signature. The decoder only reads the dates. Your server checks the signature and the other claims too.
What if a JWT has no exp claim?
Most libraries accept it, so the token never expires on its own. Require exp on the server, for example with options require exp in PyJWT, and keep access tokens short-lived.
Is it safe to paste a JWT into an online decoder?
Only if the decoder runs locally. The My Botsy JWT decoder decodes in your browser, and the token is never sent to our server. Still, treat any live token like a password, and prefer expired or test tokens when you share examples.
Is the My Botsy JWT decoder free?
Yes. It's free, needs no account and runs in your browser. Guests get 5 tool uses a day, and a free account removes that cap.