Guide Updated

JWT exp Claim (Expiration)

The JWT exp (expiration) claim is a Unix timestamp after which the token must be rejected. Learn exp validation, the milliseconds-vs-seconds trap, clock skew leeway, and recommended token lifetimes.

The `exp` (expiration time) claim is a Unix timestamp (seconds since the Unix epoch, January 1 1970 UTC) after which the token must not be accepted. It is defined in RFC 7519 §4.1.4 and is the most security-critical registered claim. A token without `exp` is valid until the signing key rotates: a stolen token with no expiration is a permanent credential.

Format and values

A numeric Unix timestamp in **seconds** (not milliseconds). Example: `"exp": 1750090800`. JavaScript's `Date.now()` returns milliseconds; a missing division by 1000 is the most common JWT bug in production.

Validation rule

Reject any token whose `exp` is in the past. Apply a clock-skew leeway of 30–60 seconds to handle distributed-system clock differences. Most JWT libraries check `exp` automatically, but you must confirm your library rejects expired tokens by default rather than optionally. Apply leeway only to `exp` and `nbf`, never to `iat`.

Common mistakes

  • Setting exp in milliseconds instead of seconds. A token with exp set in milliseconds has a value around 1,750,000,000,000: roughly the year 57,000 CE in Unix seconds. The token appears valid indefinitely. Always use Math.floor(Date.now() / 1000).
  • Issuing tokens without exp. A token without exp is valid forever; a single leak becomes a permanent credential.
  • Disabling exp validation to debug. Re-enable it before shipping; an expired-token check disabled in development that reaches production is a common incident.

Code examples

Set exp correctly (Node.js)

Set exp correctly (Node.js)
// Wrong: milliseconds, ~year 57,000 CE
const exp = Date.now() + 15 * 60 * 1000;

// Correct: seconds
const exp = Math.floor(Date.now() / 1000) + 15 * 60;

// Or let the library do it
const token = jwt.sign({ sub: "user_123" }, secret, { expiresIn: "15m" });

Verify exp with clock skew (Python: PyJWT)

Verify exp with clock skew (Python: PyJWT)
jwt.decode(
    token,
    secret,
    algorithms=["HS256"],
    leeway=60,   # 60s skew tolerance for exp/nbf
)

Frequently asked questions

  • What is the exp claim in a JWT?

    exp (expiration time) is an RFC 7519 registered claim: a Unix timestamp in seconds after which the token must be rejected. It is the most security-critical registered claim: without exp, a stolen token is valid until the signing key rotates. Standard access token lifetimes are 15 minutes; high-security APIs use 5 minutes or less.

  • Is JWT exp in seconds or milliseconds?

    Seconds. JWT timestamps (exp, nbf, iat) are Unix timestamps in seconds since the epoch. JavaScript's Date.now() returns milliseconds, so a missing division by 1000 is the most common JWT bug: the token appears valid until roughly the year 57,000 CE. Always use Math.floor(Date.now() / 1000) when computing exp manually, or let your library handle it (jwt.sign({ ... }, secret, { expiresIn: '15m' })).

  • How do I handle clock skew when validating JWT exp?

    Apply a clock-skew leeway of 30 to 60 seconds. In jsonwebtoken use clockTolerance: 60; in PyJWT use leeway=60; in Spring Security the default is 60s. The leeway accounts for clock differences between the issuer and verifier. Apply leeway only to exp and nbf: never to iat, since a token claiming to be issued in the future is suspicious and should be rejected.

Related