Unix Timestamps in JWT Tokens
Updated: May 2026
Every JWT token carries time information encoded as Unix timestamps. The three standard time claims — iat, exp and nbf — are always in seconds since the epoch. Misreading them (confusing seconds with milliseconds, ignoring clock skew, or misinterpreting expiry) causes authentication bugs that are hard to reproduce and easy to ship to production.
Free · No upload · Instant
The three time claims in JWT
iat(Issued At) — Unix timestamp in seconds when the token was created. Used to determine a token's age and to implement rolling refresh windows.exp(Expiration Time) — Unix timestamp in seconds after which the token must not be accepted. The most critical claim for security.nbf(Not Before) — Unix timestamp in seconds before which the token must not be accepted. Used for delayed activation, pre-issued tokens, and distributed systems where a token is created before its effective start time.
All three claims use seconds, not milliseconds. A common bug in JavaScript is writing exp: Date.now() + 3600000 (milliseconds + ms offset) which sets expiry 138 years in the future. The correct form is exp: Math.floor(Date.now() / 1000) + 3600.
Decoding a JWT payload by hand
A JWT consists of three Base64url-encoded segments separated by dots. The middle segment (payload) contains the claims. To read the timestamps without a library:
JavaScriptfunction decodeJwtPayload(token) {
const base64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
const json = atob(base64);
return JSON.parse(json);
}
const payload = decodeJwtPayload('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
console.log(payload.iat); // e.g. 1735689600
console.log(payload.exp); // e.g. 1735693200
console.log(new Date(payload.exp * 1000).toISOString());
// → "2025-01-01T01:00:00.000Z"
// Is the token expired?
const isExpired = payload.exp < Math.floor(Date.now() / 1000);
const secondsLeft = payload.exp - Math.floor(Date.now() / 1000);
console.log(`Expires in ${secondsLeft} seconds`);
Creating tokens with correct timestamps
Node.js (jsonwebtoken)const jwt = require('jsonwebtoken');
// expiresIn accepts a number (seconds) or a string ('1h', '7d', '30m')
const token = jwt.sign(
{ userId: 42, role: 'admin' },
process.env.JWT_SECRET,
{
expiresIn: 3600, // 1 hour in seconds — sets exp correctly
// DO NOT use: expiresIn: 3600000 // this is 1000 hours!
}
);
// Verify and get decoded payload
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(new Date(decoded.exp * 1000)); // expiry as Date
} catch (err) {
if (err.name === 'TokenExpiredError') {
console.log('Expired at:', new Date(err.expiredAt));
}
}
Python (PyJWT)
import jwt
from datetime import datetime, timezone, timedelta
payload = {
'sub': '42',
'iat': int(datetime.now(timezone.utc).timestamp()),
'exp': int((datetime.now(timezone.utc) + timedelta(hours=1)).timestamp()),
'role': 'admin'
}
token = jwt.encode(payload, 'secret', algorithm='HS256')
decoded = jwt.decode(token, 'secret', algorithms=['HS256'])
print(datetime.fromtimestamp(decoded['exp'], tz=timezone.utc))
# → 2025-01-01 01:00:00+00:00
Clock skew and validation leeway
In distributed systems, server clocks drift by a few seconds. A token created on Server A may arrive at Server B slightly before Server B's clock has advanced past the iat — making a brand-new token look invalid.
Most JWT libraries accept a leeway or clockTolerance option, typically set to 30–60 seconds. This allows a small window of clock skew without compromising security.
// Node.js jsonwebtoken
jwt.verify(token, secret, { clockTolerance: 30 }); // 30-second leeway
// PyJWT
jwt.decode(token, 'secret', algorithms=['HS256'], leeway=30)
Set your server clocks with NTP or a cloud time service. A well-configured server should have less than 100ms of drift. Leeway of 30 seconds is standard and generous; beyond 5 minutes, the security model of short-lived tokens breaks down.
Debugging an expired JWT
When you receive a TokenExpiredError or a 401 in production, paste the exp value into the Flowfiles timestamp converter to see the exact expiry date and time. Then compare it to the current timestamp to understand how long ago it expired and whether a clock skew or a misconfigured expiresIn is responsible.
You can also decode the full payload in your browser's console with JSON.parse(atob(token.split('.')[1])) to inspect all claims without sending the token to any server.