Error message
Signature has expired
PyJWT (Python) — jwt.exceptions.ExpiredSignatureError
What it means
The token's exp claim is earlier than the verifier's current time, so PyJWT rejected it after (conceptually) a successful signature check. The wording is slightly misleading — the signature itself does not expire; the token's validity window has closed.
Why it happens
Stale token reused past its lifetime
commonCached credentials in a long-running worker, a token stored client-side and replayed after the TTL, or integration tests using a fixture token generated days ago. The rejection is correct behavior.
Clock drift on the verifying host
commonA verifier whose clock runs ahead shortens every token's effective lifetime. Containers and CI runners without time sync are the usual suspects; even 60–120 seconds of drift breaks short-lived tokens intermittently.
Issuer writes exp incorrectly
occasionalexp must be integer seconds since epoch. Passing datetime objects incorrectly serialized, or milliseconds, yields tokens that are expired at issue time — every verification then fails immediately, which distinguishes this from ordinary expiry.
Timezone-naive datetime used to mint exp
occasionalIssuers building exp from datetime.now() (naive local time) instead of datetime.now(timezone.utc) shift every token's lifetime by the server's UTC offset. Depending on which side of UTC the issuer sits, tokens either expire hours early or quietly outlive their intended TTL. PyJWT encodes aware datetimes correctly — naive ones are the trap.
How to fix it
- 1.
Absorb small skew with leeway
The leeway parameter (seconds) applies to exp and nbf checks. Use tens of seconds at most; larger values quietly extend token lifetimes.
python import jwt payload = jwt.decode( token, key, algorithms=["ES256"], leeway=30, # seconds of tolerated clock skew ) - 2.
Inspect the actual timestamps
Read the claims without verification and compare exp against the host clock — the gap size tells you whether it is expiry, skew, or an issuing bug.
python import jwt, time claims = jwt.decode(token, options={"verify_signature": False}) print("exp:", claims["exp"], "now:", int(time.time()), "delta:", int(time.time()) - claims["exp"], "s")