JWT Not Active: Every Cause of the nbf Claim Error and How to Fix It
A JWT can have a perfect signature, an unexpired exp, and still get rejected — because of a claim most debugging guides skip over: nbf, "not before." Libraries throw it as NotBeforeError: jwt not active (node-jsonwebtoken) or JWTClaimValidationFailed: "nbf" claim timestamp check failed (jose), and the confusing part is that it's sometimes not a bug at all. This walkthrough covers five real causes — one intentional, four accidental — with a live, prefilled JWT Verifier example for each so you can watch the exact token fail instead of reading a description of one.
nbf is a NumericDate claim (seconds since the Unix epoch, not milliseconds) that means "don't accept this token before this moment." Verification compares it to the current time and refuses the token — signature and all — until that moment passes. If a token is deliberately dated for a real future activation, that's correct behavior. If the nbf value itself is wrong — units, timezone, clock drift, or a stale constant — it's a bug, and the fix is in whatever code set the claim, not in the verifier.
Quick reference
All five results below were verified live in JWT Verifier against a real build — HS256 tokens signed with Node's crypto.createHmac and cross-checked against the tool's own Web Crypto HMAC implementation before publishing.
| Symptom | Most likely cause | Fix |
|---|---|---|
| Token is dated for a real future activation, and that's expected | Deliberate delayed activation | Working as designed — verify it once that date actually arrives |
nbf decodes to a date decades or millennia out | Date.now() (milliseconds) used where seconds were required | Divide by 1000: Math.floor(Date.now() / 1000) |
| Fails only in the seconds right after issuing, then passes | Clock skew between the issuing and verifying servers | Sync clocks via NTP; add a small clockTolerance on verify |
| Fails only for tokens issued from one server or region | Local wall-clock time used instead of true UTC | Compute epoch time directly, never rebuild it from local date fields |
Every token from one code path shares the exact same nbf | Hardcoded value left over from test/template code | Derive nbf from issuance time or an actual business rule, not a constant |
1. Deliberate delayed activation — not a bug
Before chasing a fix, check whether the token is supposed to fail right now. Issuing a token ahead of a real future date is a legitimate pattern: a subscription that unlocks on its paid start date, a scheduled access grant, an invite that shouldn't be redeemable before an event begins. The token exists, it's correctly signed, and it will verify successfully — just not yet.
// Correct and intentional: a pro-plan token issued today, active on the
// plan's real start date. jsonwebtoken's numeric `notBefore` option is a
// DURATION in seconds relative to iat, not an absolute date — and it
// doesn't accept an ISO string at all. For a specific future date, set
// the NumericDate directly in the payload instead.
const token = jwt.sign(
{ sub: userId, plan: 'pro-annual', nbf: Math.floor(new Date('2030-01-01T00:00:00Z').getTime() / 1000) },
secret
);
jwt.verify(token, secret);
// NotBeforeError: jwt not active <- expected, right now. Not a bug.
The signal that you're in this case rather than one of the four below: decode the token and ask whether a human deliberately chose that date for a real reason. If yes, the fix isn't code — it's verifying again once that date arrives, or explaining to whoever filed the bug report that the token is doing exactly what it was told to.
See it fail live
Signed today, activates 1 Jan 2030. Decode it in JWT Verifier and the nbf line reads that date plainly — this is the tell that it's by design:
2. Milliseconds where the spec wants seconds
RFC 7519 defines every JWT timestamp claim — exp, nbf, iat — as a NumericDate: seconds since the Unix epoch. Date.now() in JavaScript returns milliseconds. Pass it straight into nbf without dividing by 1000, and the claim ends up roughly a thousand times too large — which a validator reads as a date so far in the future it might as well be permanent.
// Bug: Date.now() is milliseconds; nbf needs seconds
const payload = { sub: 'svc_batch_worker', scope: 'jobs:run', nbf: Date.now() };
const token = jwt.sign(payload, secret, { algorithm: 'HS256' });
jwt.verify(token, secret);
// NotBeforeError: jwt not active
// decode the claim and it reads as a date tens of thousands of years out
The fix is the same one-line correction every time: Math.floor(Date.now() / 1000), or let the library's own notBefore/expiresIn options (which already expect seconds or a duration string) compute it instead of hand-rolling the epoch math.
See it fail live
This token's nbf was set directly from Date.now(). Decode it in JWT Verifier and watch the nbf line render a date in the year 58659 — that's what a thousand-fold units error actually looks like once you decode it:
3. Clock skew between issuing and verifying servers
In production this is usually the least visible cause, because it's intermittent by nature: the token is issued with a correct nbf equal to "now" on the issuing server's clock, but if the verifying server's clock is even a few seconds behind, "now" hasn't arrived there yet. The token fails for a few seconds and then starts passing on its own — the kind of bug that's gone by the time someone opens the incident channel to ask about it.
// Issuing server, clock running a few seconds fast. jsonwebtoken adds iat
// automatically but never adds nbf on its own — it has to be set explicitly
// for this failure to be possible at all.
const token = jwt.sign(
{ sub: userId, nbf: Math.floor(Date.now() / 1000) }, // nbf stamped from ITS "now"
secret
);
// Verifying server, clock a few seconds behind — verified moments later
jwt.verify(token, secret);
// NotBeforeError: jwt not active <- only in the window right after issuing
Two fixes, best applied together: keep clocks synced with NTP so drift stays in the sub-second range, and give verification a small grace window so ordinary drift doesn't fail a token that was genuinely issued right now:
jwt.verify(token, secret, { clockTolerance: 10 }); // seconds of allowed skew
See it fail live
The demo token below uses an exaggerated future nbf (2031) rather than a few real seconds of drift, so this example keeps failing no matter when you're reading this — real clock skew in production is almost always seconds, not years:
4. Local timezone treated as UTC
This one shows up almost exclusively on servers not running in UTC. If the code that builds nbf reads the individual year/month/day/hour fields off a local Date and feeds them into Date.UTC() — instead of just taking the already-correct epoch value straight from Date.now() — it silently re-interprets the server's local wall-clock numbers as if they were UTC. A server running five and a half hours ahead of UTC ends up stamping every token five and a half hours into the future.
// Server running with TZ=Asia/Kolkata (UTC+5:30)
const local = new Date();
const nbf = Math.floor(Date.UTC(
local.getFullYear(), local.getMonth(), local.getDate(),
local.getHours(), local.getMinutes(), local.getSeconds()
) / 1000);
// Date.UTC() takes LOCAL wall-clock numbers here and treats them as UTC —
// the result is 5.5 hours ahead of the real UTC "now"
The fix is to never round-trip through local date components at all: Math.floor(Date.now() / 1000) already returns true UTC epoch seconds regardless of the server's configured timezone, so there's nothing to convert.
See it fail live
Same exaggerated-gap caveat as above — the demo uses a 2032 nbf so the example stays broken; a real timezone-offset bug is usually a fixed few hours, matching whatever UTC±N the server runs in:
5. A hardcoded nbf template that never got parameterized
The most avoidable version: someone builds or tests a delayed-activation feature, hardcodes a real future timestamp to make the "not yet valid" case reproducible while developing, and the literal constant survives into the code path that issues real tokens. Every token that function issues afterward carries the exact same fixed nbf — not relative to when it was actually issued.
// Copied from a test fixture; the literal timestamp was never made relative.
// A numeric `notBefore` option is a duration in seconds relative to iat, not
// an absolute date, so a stale absolute literal like this has to live in the
// payload's own nbf claim to actually reproduce a fixed shared date.
function issueSignupToken(userId) {
return jwt.sign(
{ sub: userId, scope: 'account:active', nbf: 2016057600 }, // hardcoded during testing, never revisited
secret
);
}
// every real signup issued through this function inherits the same fixed nbf
The tell here is different from the other four: check whether every affected token — across different users, issued at different times — decodes to the identical nbf value. If so, it's not a computation bug, it's a literal that needs to become a variable (or be removed entirely if the delayed-activation behavior was only ever needed for that one test).
See it fail live
This token carries a hardcoded nbf of 20 Nov 2033 — the same value every token from this function would carry, regardless of when it was actually issued:
Debugging checklist
- Decode the token first (no verification needed) and check whether
nbfis even present — plenty of tokens don't carry one, and it's easy to assume it's the cause of a different failure. - Compare the raw
nbfnumber to the current epoch seconds (Math.floor(Date.now() / 1000), roughly 10 digits in 2026). Ifnbfis dramatically larger — 13+ digits — suspect a milliseconds-vs-seconds bug before anything else. - If
nbfis only seconds or low minutes ahead, suspect clock skew between servers before assuming a logic error — comparedate -uoutput on both independently. - If the gap is a clean number of hours (or a half-hour, like 5.5), suspect a timezone offset baked into the calculation instead of true UTC.
- If every token from one code path carries the identical
nbfregardless of when it was issued, look for a hardcoded literal rather than a computed value. - If none of the above fit, decode the token and ask a human whether that date was chosen on purpose — sometimes "not yet valid" just means not yet.
Two related JWT failures worth knowing the difference from: a signature verification failure means the token's bytes don't match what was signed at all, and a malformed JWT error means the token isn't even structurally valid before verification gets a chance to run. An nbf failure is different from both — the token is well-formed and correctly signed, it's just not time yet.
🔐 Paste your token and see exactly where it fails
JWT Verifier decodes the header and claims, checks exp/nbf, and verifies HS256 signatures entirely in your browser using the Web Crypto API — nothing is uploaded, and the secret field is never stored.
← Back to Blog · See also: JWT Signature Verification Failed →