JWT Expiration Strategy: What Token Lifetimes the Big Providers Actually Use
GitHub caps app JWTs at 10 minutes, Google and Microsoft issue roughly one-hour access tokens, and Microsoft randomizes each lifetime between 60 and 90 minutes. What the documented defaults of real identity platforms teach about choosing exp, designing refresh flows, and handling clock skew.
Every team shipping JWT auth eventually argues about one number: how long should the access token live? Fifteen minutes feels paranoid, twenty-four hours feels lazy, and most articles answer with "it depends." The identity platforms that issue billions of tokens a day, though, have all published their answer — so instead of arguing from vibes, we collected the documented defaults from GitHub, Google, Microsoft, and Auth0, and worked backwards to the strategy they encode.
(One thing we fixed while writing this: our own JWT decoder page promised to show exp "in human-readable form" — but the tool printed raw Unix seconds and left the math to you. As of today it renders a timestamp-claims panel: each of exp, nbf, and iat as a UTC date with a live expired/valid status, plus a warning when the value is 13 digits — the milliseconds-instead-of-seconds bug we cover below.)
What the platforms actually issue
Every figure below comes from the provider's current documentation, linked in place:
| Token | Documented lifetime |
|---|---|
| GitHub App authentication JWT | max 10 minutes — "The time must be no more than 10 minutes into the future" |
| Google Cloud access tokens (user; service-account default) | 1 hour (service accounts configurable 5 min–12 h) |
| Google Cloud ID tokens | 1 hour — "valid for one hour, and can't be revoked" |
| Microsoft Entra access tokens | random 60–90 minutes (75-minute average); configurable 10 min–24 h |
| Microsoft Entra ID / SAML tokens | 1 hour |
| Auth0 /userinfo access tokens (implicit flow) | 2 hours |
| Auth0 custom-API access tokens | 24 hours by default |
Three details in that table repay a closer look.
The most privileged token is the shortest. A GitHub App JWT is signed with the app's own private key and can mint installation tokens for every repository the app touches — and GitHub will not accept one whose exp is more than ten minutes out. The blast radius of a leak, not the convenience of the holder, sets the number.
Microsoft randomizes lifetimes. Each Entra access token gets a random lifetime between 60 and 90 minutes. Among other things, that spreads token renewals across time instead of letting a fleet of clients that authenticated together all come back for refresh in the same second — a stampede-control trick worth remembering if you operate your own issuer.
The long-lived exception proves the rule. Entra will extend tokens to 24–28 hours only for clients that support Continuous Access Evaluation — a channel that lets those tokens be "revoked in near real time in response to critical events such as account disablement." Long life is granted exactly when statelessness is given up.
Why exp is a security control, not a session length
A JWT is a bearer credential: whoever holds it, is it. And a signed token stays cryptographically valid until exp, no matter what happens in your database — password change, account ban, "log out everywhere." Google's documentation states this with unusual honesty: its ID tokens are "valid for one hour, and can't be revoked."
So exp is really the answer to one question: how long are you willing for a stolen token to keep working? Framed that way, the platform defaults stop looking arbitrary. An hour is the window Google and Microsoft accepted at planetary scale; ten minutes is what GitHub accepted for a key that can touch thousands of repos; and Auth0's 24-hour default is the generous end, suited to lower-stakes APIs — and adjustable downward, which their own docs let you do per API.
What exp is not is the user's session. Nobody wants to re-enter a password hourly, and no platform above makes them. The session lives one layer down.
The split: short access token, revocable refresh token
The pattern all of these providers share is a two-token design:
- The access token is the stateless JWT your APIs verify on every request without a database hit. It is short-lived because it cannot be recalled.
- The refresh token is a long-lived credential presented back to the issuer — one place, with state — to mint fresh access tokens. Because redeeming it touches the issuer, it can be revoked, rotated, and monitored.
The long lifetimes live here, with server-side control attached: Microsoft's refresh tokens have a maximum inactive time of 90 days, while Google issues 7-day refresh tokens to OAuth apps still in "testing" status precisely to keep unreviewed apps on a short leash. Modern guidance adds rotation: every redemption returns a new refresh token and retires the old one, so a stolen refresh token dies the moment either its thief or its owner uses it a second time — and the collision is your intrusion alarm.
Two practical numbers complete the design. First, renew early: AWS Cognito's docs recommend using a token for about 75% of its lifetime before fetching a fresh one, so renewal happens while the old token still works and a slow network can't strand a request mid-expiry. Second, cache with headroom: if you cache tokens (or introspection results), cap the cache below the token lifetime, or you will serve dead tokens from a healthy cache.
Clock skew: the leeway you must configure
Distributed clocks disagree, so a token minted now can arrive at a validator whose clock says two seconds ago — instant "expired token" errors on perfectly fresh tokens, or nbf rejections on tokens that are valid everywhere else. RFC 7519 anticipates this for both exp and nbf with identical language: implementers "MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew."
Note the MAY: leeway is permitted, not automatic. Common JWT libraries default to zero tolerance, and the symptom — intermittent auth failures that vanish on retry — looks exactly like flaky infrastructure. Setting an explicit 30–60 seconds of leeway follows the RFC's spirit while staying well under its "few minutes" ceiling; Microsoft's SAML validation, for reference, applies a five-minute skew factor. What leeway is not for: papering over an issuer whose clock is minutes wrong. That machine needs NTP, not a bigger tolerance.
The off-by-1000 bug
RFC 7519 defines exp, nbf, and iat as NumericDate values: "the number of seconds from 1970-01-01T00:00:00Z UTC" — seconds, while Date.now() in JavaScript returns milliseconds. Write Date.now() + 3600 into exp and you have created a token that expires 50,000 years from now… as far as any validator that also forgot to divide is concerned — while a correct validator sees a nonsense date. Flip the mistake around (seconds where milliseconds were expected) and every token looks like it expired in 1970.
The tell is the digit count: seconds-precision timestamps are 10 digits long in this era; 13 digits means milliseconds. Paste a suspect token into our decoder and the timestamp panel now calls this out explicitly, alongside the plain-English expiry status. (For the full tour of what each claim means and how to validate it, see JWT claims explained.)
When you genuinely need to kill a token early
Sometimes "wait out the hour" is unacceptable — an employee is terminated, a token appears in a paste site. Your options, in ascending order of cost:
- Shrink exp until waiting is acceptable. With 10–15-minute tokens, doing nothing is a revocation strategy; the exposure window is one coffee break. This is the option the GitHub number embodies.
- Denylist by
jti. Store the IDs of revoked tokens until their naturalexp, and check the list during validation. You have reintroduced a database read — but only a small, bounded list, checked cheaply, rather than full session state. - Rotate the signing key. The nuclear option: every outstanding token from that key dies at once, including every legitimate one, and all clients re-authenticate. Right answer for a suspected key compromise; wrong answer for banning one user.
The quiet lesson of the provider table is that option 1 does most of the work at scale. Every design above spends its complexity budget on making renewal smooth — rotation, inactivity windows, randomized lifetimes — so that the access token itself can stay short, stateless, and disposable.
The bottom line
Set the access token's exp to the longest outage-of-control you can tolerate — the platforms' own answers range from 10 minutes for repo-controlling keys to an hour for general APIs. Put the session in a revocable, rotating refresh token, renew at about three-quarters of the lifetime, allow 30–60 seconds of validator leeway, and write the timestamps in seconds. None of these numbers is exotic: they are what the issuers of billions of daily tokens have already converged on, in documentation anyone can read — and now you have read it.
Frequently asked questions
How long should a JWT access token live?
The documented defaults of major providers cluster between 10 minutes and about an hour: GitHub App JWTs are capped at 10 minutes, Google issues one-hour access tokens, and Microsoft Entra randomizes between 60 and 90 minutes. Shorter is safer because a leaked bearer token stays usable until exp. Pick the shortest lifetime your refresh flow makes painless — 15 minutes to an hour covers most APIs.
What is refresh token rotation?
Each time the client redeems a refresh token for a new access token, the server also issues a new refresh token and invalidates the old one. A stolen refresh token then fails on its second use, and the reuse attempt itself is a detectable signal that lets the server kill the whole session family. Rotation is what makes short access tokens livable.
Can a JWT be revoked before it expires?
Not by default — that is the core trade-off of stateless tokens. Google documents its ID tokens as not revocable during their one-hour life. Your options are to keep exp short so the exposure window is small, maintain a server-side denylist keyed on the jti claim, or rotate the signing key, which invalidates every outstanding token at once.
Why does my JWT expire immediately after being issued?
Two usual suspects. First, units: exp must be Unix seconds, and writing Date.now() (milliseconds) into it produces a 13-digit value that validators misread — our decoder flags this pattern explicitly. Second, clock skew: if the issuing server's clock runs ahead of the validator's, a fresh token can arrive already "expired." RFC 7519 permits a few minutes of leeway for exactly this reason; 30-60 seconds is the common setting.