Error message
"nbf" claim timestamp check failed
jose (JavaScript) — JWTClaimValidationFailed, code ERR_JWT_CLAIM_VALIDATION_FAILED
What it means
The token carries an `nbf` ("not before") claim that is still in the future according to the verifier's clock, so jose refuses it as not yet valid. The signature and the other claims may be perfectly fine; the error is about time only. It almost always means the issuer's and the verifier's clocks disagree by more than the tolerance you allow (zero by default).
Why it happens
The verifier's clock is behind the issuer's
commonMany issuers set `nbf` equal to the issue time. If the verifying host's clock lags by even a few seconds, every freshly issued token is "from the future" for that long. Containers and VMs inherit the host clock, so one drifting host breaks every service on it at once.
Token used the moment it was issued, with no tolerance
commonTests and service-to-service calls often mint a token and send it within milliseconds. With `clockTolerance` at its default of 0 seconds, normal skew between two machines is enough to fail intermittently.
The issuer set nbf in the future on purpose
occasionalSome systems pre-issue tokens that become valid later (scheduled jobs, delayed links). Using such a token early is a correct rejection — the caller must wait until `nbf`.
nbf written in milliseconds
rareJWT times are seconds since the epoch (RFC 7519, NumericDate). An issuer that writes `Date.now()` produces an `nbf` about 50,000 years in the future, and the token is never valid.
How to fix it
- 1.
Allow a small, explicit clock skew
`clockTolerance` accepts seconds or a duration string. 30–60 seconds absorbs real-world drift; it is not a way to accept tokens minutes early.
js import { jwtVerify } from "jose"; const { payload } = await jwtVerify(token, jwks, { algorithms: ["ES256"], clockTolerance: "30s", // applies to nbf, exp and iat }); - 2.
Print the timeline before changing configuration
Compare `nbf` with the verifier's current time. A difference of seconds points at skew; hours at a time-zone or unit bug in the issuer; a 13-digit `nbf` at milliseconds.
js import { decodeJwt } from "jose"; const { iat, nbf, exp } = decodeJwt(token); const now = Math.floor(Date.now() / 1000); console.log({ iat, nbf, exp, now, nbfInSeconds: nbf - now }); - 3.
Fix the clock that drifts
Run NTP (chrony or systemd-timesyncd) on every host that issues or verifies tokens. Tolerance hides small skew; it should not have to hide large skew.
bash timedatectl status # "System clock synchronized: yes" chronyc tracking # offset from the NTP reference date -u +%s # compare with the token's nbf