DevRivet

Blog · · Updated

How to Check JWT Expiration Time (exp Claim)

The JWT exp claim is a Unix timestamp in seconds (UTC). Check it in JavaScript, Node.js and Python by decoding the payload and comparing exp with now.

JWT Decoder

Decode a JWT's header and payload instantly.

Try it free →

The exp claim holds a JWT's expiration time as a Unix timestamp: the number of seconds since 1970-01-01 00:00:00 UTC. To check when a token expires, decode its payload, read exp, convert it from seconds to a date, and compare it with the current time. The JWT Decoder does the first two steps for you and shows exp as a readable date.

What the exp claim means

exp is a registered claim defined in RFC 7519. Its value is a NumericDate: seconds since the Unix epoch, in UTC. A token is expired once the current time is at or after that moment. The spec says the current time must be before the exp time for the token to be accepted.

Because the value is absolute, there is no time zone to worry about. The same exp number refers to the same instant everywhere.

Seconds, not milliseconds

This is the most common mistake. exp is in seconds, but JavaScript's Date and Date.now() use milliseconds. If you pass exp straight to new Date(), you get a date in January 1970.

For example, an exp of 1700000000 is 2023-11-14 22:13:20 UTC. Passed directly to new Date(1700000000), it is read as 1,700,000,000 milliseconds, which is under 20 days after the epoch. Multiply by 1000 first.

A quick sanity check: a 10-digit value is almost certainly seconds, and a 13-digit value is milliseconds. If you need to convert a value by hand, the Unix Timestamp Converter handles both units.

Read the exp claim in JavaScript

function getClaims(token) {
  const base64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
  const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
  return JSON.parse(new TextDecoder().decode(bytes));
}

const claims = getClaims(token);
const expiresAt = new Date(claims.exp * 1000);
const isExpired = Date.now() >= claims.exp * 1000;

console.log(expiresAt.toISOString());
console.log(isExpired ? 'expired' : 'still valid');

atob returns one character per byte, not UTF-8 text, so the bytes go through TextDecoder before JSON.parse. Without that step, a claim with non-ASCII characters, such as the name José, comes out garbled. The function works in browsers and in Node.js 18 and later.

Read the exp claim in Node.js

Node's Buffer decodes base64url directly, so no character replacement is needed:

const payload = token.split('.')[1];
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));

function secondsUntilExpiry(claims, now = Math.floor(Date.now() / 1000)) {
  if (typeof claims.exp !== 'number') return null; // no exp claim
  return claims.exp - now; // 0 or less means expired
}

For a token with iat 1700000000 and exp 1700003600, the lifetime is 3600 seconds (one hour), new Date(1700003600 * 1000).toISOString() is 2023-11-14T23:13:20.000Z, and secondsUntilExpiry(claims, 1700000000) returns 3600.

Read the exp claim in Python

import base64
import json
from datetime import datetime, timezone

def get_claims(token):
    payload = token.split('.')[1]
    payload += '=' * (-len(payload) % 4)
    return json.loads(base64.urlsafe_b64decode(payload))

claims = get_claims(token)
expires_at = datetime.fromtimestamp(claims['exp'], tz=timezone.utc)
print(expires_at.isoformat())

The padding line matters. JWTs use base64url without = padding, and urlsafe_b64decode expects padding, so the code adds it back before decoding.

Check whether a token has expired

A token is expired when its exp is less than or equal to the current time, both in seconds. In JavaScript, use Math.floor(Date.now() / 1000) to get the current Unix time in seconds. In Python, time.time() returns seconds as a float, so compare it with claims['exp'] directly.

Clock skew and leeway

Servers and clients do not always agree on the time. If the issuer's clock is a few seconds ahead of yours, a freshly issued token can appear to be issued in the future, and a token can appear expired a moment early.

Most JWT libraries let you allow a small leeway when checking exp, nbf and iat. The jsonwebtoken package for Node calls this option clockTolerance, and PyJWT calls it leeway. Check your library's documentation for the exact name and units.

Leeway absorbs small differences between clocks. It is not a way to extend a token's life, so keep it small.

Decoding is not verification

Reading exp from a payload tells you what the issuer claimed. It does not prove the claim is true. Anyone can edit a JWT payload, and only the signature check detects that change.

The JWT Decoder reads the header and payload and shows dates, but it does not verify the signature. In your application, verify the signature with the trusted key and let your JWT library check exp as part of verification. Never make an authorization decision based only on a decoded payload.

Let a JWT library check exp

In production, verify the signature and check exp in one step. Both libraries below reject an expired token with a specific error, and both accept a small leeway for clock skew.

import jwt from 'jsonwebtoken';

try {
  const claims = jwt.verify(token, secret, { algorithms: ['HS256'], clockTolerance: 30 });
  console.log('valid until', new Date(claims.exp * 1000).toISOString());
} catch (err) {
  if (err.name === 'TokenExpiredError') {
    console.log('expired at', err.expiredAt.toISOString());
  } else {
    throw err;
  }
}
import jwt  # PyJWT

try:
    claims = jwt.decode(
        token,
        secret,
        algorithms=["HS256"],
        leeway=30,
        options={"require": ["exp"]},
    )
except jwt.ExpiredSignatureError:
    print("token expired")

clockTolerance and leeway are in seconds. Passing algorithms explicitly stops a token from choosing its own algorithm. PyJWT's require option also rejects tokens that have no exp at all, which the plain decode-and-compare approach above has to handle itself.

Common pitfalls

  • Milliseconds in a seconds field: if exp has 13 digits, it is probably milliseconds, or the issuer used the wrong unit. Divide by 1000 before comparing with seconds.
  • Missing exp: the claim is optional. A token without exp does not expire according to the claim, so your application must decide whether to reject it.
  • Confusing iat with expiry: iat is the time the token was issued. The lifetime is exp minus iat, in seconds.
  • Comparing formatted strings: exp is an absolute instant in UTC. Convert it to a date object and compare instants, not local-time strings.
  • Stale tokens in logs or caches: a token copied from a log may be expired by the time you read it. Compare exp with the current time at the moment of use.

Privacy note

Access tokens are credentials. The DevRivet JWT Decoder runs entirely in your browser, so the token is not sent to a server. Even so, avoid pasting live production tokens into any online tool, and use a test token when you can.

FAQ

Is the exp claim in seconds or milliseconds?

Seconds. RFC 7519 defines NumericDate as the number of seconds since the Unix epoch. Multiply by 1000 before passing the value to JavaScript's Date constructor.

What happens when a JWT expires?

Verifying libraries reject it with an error. jsonwebtoken throws a TokenExpiredError, and PyJWT raises an ExpiredSignatureError. Your application should treat that as a signal to re-authenticate or refresh the token.

Can I extend a JWT's expiration by editing exp?

No. Changing the payload breaks the signature, so verification fails. The issuer has to create a new token with a new exp, usually through a refresh flow.

How do I check expiry without a JWT library?

Decode the payload, read exp, and compare it with the current Unix time in seconds. That tells you whether the token is past its expiration, but it does not prove the token is genuine. You still need signature verification before trusting any claim.

JWT Decoder

Decode a JWT's header and payload instantly.

Try it free →