JWT Not Active: Every Cause of the nbf Claim Error and How to Fix It

Published 9 Sep 2026 · 7 min read

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.

Quick answer: 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.

SymptomMost likely causeFix
Token is dated for a real future activation, and that's expectedDeliberate delayed activationWorking as designed — verify it once that date actually arrives
nbf decodes to a date decades or millennia outDate.now() (milliseconds) used where seconds were requiredDivide by 1000: Math.floor(Date.now() / 1000)
Fails only in the seconds right after issuing, then passesClock skew between the issuing and verifying serversSync clocks via NTP; add a small clockTolerance on verify
Fails only for tokens issued from one server or regionLocal wall-clock time used instead of true UTCCompute epoch time directly, never rebuild it from local date fields
Every token from one code path shares the exact same nbfHardcoded value left over from test/template codeDerive 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:

Open this example in JWT Verifier

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:

Open this example in JWT Verifier

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:

Open this example in JWT Verifier

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:

Open this example in JWT Verifier

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:

Open this example in JWT Verifier

Debugging checklist

  1. Decode the token first (no verification needed) and check whether nbf is even present — plenty of tokens don't carry one, and it's easy to assume it's the cause of a different failure.
  2. Compare the raw nbf number to the current epoch seconds (Math.floor(Date.now() / 1000), roughly 10 digits in 2026). If nbf is dramatically larger — 13+ digits — suspect a milliseconds-vs-seconds bug before anything else.
  3. If nbf is only seconds or low minutes ahead, suspect clock skew between servers before assuming a logic error — compare date -u output on both independently.
  4. 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.
  5. If every token from one code path carries the identical nbf regardless of when it was issued, look for a hardcoded literal rather than a computed value.
  6. 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.

Open JWT Verifier →

← Back to Blog · See also: JWT Signature Verification Failed →