[{"data":1,"prerenderedAt":767},["ShallowReactive",2],{"guide-jwt-token-expiration":3},{"id":4,"title":5,"body":6,"date":759,"description":760,"extension":761,"meta":762,"navigation":63,"path":763,"readingTime":120,"seo":764,"stem":765,"__hash__":766},"guides\u002Fguides\u002Fjwt-token-expiration.md","JWT Token Expiration Best Practices",{"type":7,"value":8,"toc":752},"minimark",[9,18,23,29,140,159,162,229,233,236,253,357,360,364,367,643,647,653,666,672,678,694,698,701,730,748],[10,11,12,13,17],"p",{},"JSON Web Tokens (JWTs) carry their own expiration time in the ",[14,15,16],"code",{},"exp"," claim. This self-contained design is a core strength—any party can verify token validity without querying a central store. But it also creates challenges: once issued, a token cannot be revoked before its expiration, and poorly chosen expiration windows expose your system to abuse. Getting token expiration right requires balancing security, user experience, and operational complexity.",[19,20,22],"h2",{"id":21},"setting-the-exp-claim","Setting the exp Claim",[10,24,25,26,28],{},"The ",[14,27,16],{}," (expiration) claim is a NumericDate value—the number of seconds since the Unix epoch (1970-01-01 00:00:00Z). JWT libraries handle the encoding automatically:",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-typescript shiki shiki-themes github-light github-dark","import jwt from 'jsonwebtoken'\n\nconst token = jwt.sign(\n  { sub: 'user123', role: 'editor' },\n  process.env.JWT_SECRET,\n  { expiresIn: '15m' }  \u002F\u002F 15 minutes\n)\n","typescript","",[14,37,38,58,65,88,106,118,134],{"__ignoreMap":35},[39,40,43,47,51,54],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"szBVR","import",[39,48,50],{"class":49},"sVt8B"," jwt ",[39,52,53],{"class":45},"from",[39,55,57],{"class":56},"sZZnC"," 'jsonwebtoken'\n",[39,59,61],{"class":41,"line":60},2,[39,62,64],{"emptyLinePlaceholder":63},true,"\n",[39,66,68,71,75,78,81,85],{"class":41,"line":67},3,[39,69,70],{"class":45},"const",[39,72,74],{"class":73},"sj4cs"," token",[39,76,77],{"class":45}," =",[39,79,80],{"class":49}," jwt.",[39,82,84],{"class":83},"sScJk","sign",[39,86,87],{"class":49},"(\n",[39,89,91,94,97,100,103],{"class":41,"line":90},4,[39,92,93],{"class":49},"  { sub: ",[39,95,96],{"class":56},"'user123'",[39,98,99],{"class":49},", role: ",[39,101,102],{"class":56},"'editor'",[39,104,105],{"class":49}," },\n",[39,107,109,112,115],{"class":41,"line":108},5,[39,110,111],{"class":49},"  process.env.",[39,113,114],{"class":73},"JWT_SECRET",[39,116,117],{"class":49},",\n",[39,119,121,124,127,130],{"class":41,"line":120},6,[39,122,123],{"class":49},"  { expiresIn: ",[39,125,126],{"class":56},"'15m'",[39,128,129],{"class":49}," }  ",[39,131,133],{"class":132},"sJ8bj","\u002F\u002F 15 minutes\n",[39,135,137],{"class":41,"line":136},7,[39,138,139],{"class":49},")\n",[10,141,25,142,145,146,148,149,148,152,155,156,158],{},[14,143,144],{},"expiresIn"," option accepts human-readable strings (",[14,147,126],{},", ",[14,150,151],{},"'1h'",[14,153,154],{},"'7d'",") or numeric values in seconds. The library computes the ",[14,157,16],{}," claim by adding this duration to the current time.",[10,160,161],{},"Recommended expiration windows by token type:",[163,164,165,181],"table",{},[166,167,168],"thead",{},[169,170,171,175,178],"tr",{},[172,173,174],"th",{},"Token Type",[172,176,177],{},"Recommended Lifetime",[172,179,180],{},"Rationale",[182,183,184,196,207,218],"tbody",{},[169,185,186,190,193],{},[187,188,189],"td",{},"Access token",[187,191,192],{},"5–15 minutes",[187,194,195],{},"Short window limits damage if stolen",[169,197,198,201,204],{},[187,199,200],{},"Refresh token",[187,202,203],{},"7–30 days",[187,205,206],{},"Balances security with user convenience",[169,208,209,212,215],{},[187,210,211],{},"Email verification",[187,213,214],{},"1–24 hours",[187,216,217],{},"Time-limited action confirmation",[169,219,220,223,226],{},[187,221,222],{},"API key (service-to-service)",[187,224,225],{},"90–365 days",[187,227,228],{},"Long-lived; rotate on schedule",[19,230,232],{"id":231},"the-short-lived-access-token-pattern","The Short-Lived Access Token Pattern",[10,234,235],{},"Industry best practice splits authentication into two tokens:",[237,238,239,247],"ol",{},[240,241,242,246],"li",{},[243,244,245],"strong",{},"Access token:"," Short-lived (5–15 minutes), carries authorization claims, sent with every API request.",[240,248,249,252],{},[243,250,251],{},"Refresh token:"," Long-lived (7–30 days), carries only an identifier, used exclusively to obtain new access tokens.",[30,254,256],{"className":32,"code":255,"language":34,"meta":35,"style":35},"\u002F\u002F Issue both tokens at login\nconst accessToken = jwt.sign(\n  { sub: userId, role: user.role },\n  ACCESS_SECRET,\n  { expiresIn: '15m' }\n)\n\nconst refreshToken = jwt.sign(\n  { sub: userId, jti: crypto.randomUUID() },\n  REFRESH_SECRET,\n  { expiresIn: '7d' }\n)\n",[14,257,258,263,278,283,290,299,303,307,323,335,343,352],{"__ignoreMap":35},[39,259,260],{"class":41,"line":42},[39,261,262],{"class":132},"\u002F\u002F Issue both tokens at login\n",[39,264,265,267,270,272,274,276],{"class":41,"line":60},[39,266,70],{"class":45},[39,268,269],{"class":73}," accessToken",[39,271,77],{"class":45},[39,273,80],{"class":49},[39,275,84],{"class":83},[39,277,87],{"class":49},[39,279,280],{"class":41,"line":67},[39,281,282],{"class":49},"  { sub: userId, role: user.role },\n",[39,284,285,288],{"class":41,"line":90},[39,286,287],{"class":73},"  ACCESS_SECRET",[39,289,117],{"class":49},[39,291,292,294,296],{"class":41,"line":108},[39,293,123],{"class":49},[39,295,126],{"class":56},[39,297,298],{"class":49}," }\n",[39,300,301],{"class":41,"line":120},[39,302,139],{"class":49},[39,304,305],{"class":41,"line":136},[39,306,64],{"emptyLinePlaceholder":63},[39,308,310,312,315,317,319,321],{"class":41,"line":309},8,[39,311,70],{"class":45},[39,313,314],{"class":73}," refreshToken",[39,316,77],{"class":45},[39,318,80],{"class":49},[39,320,84],{"class":83},[39,322,87],{"class":49},[39,324,326,329,332],{"class":41,"line":325},9,[39,327,328],{"class":49},"  { sub: userId, jti: crypto.",[39,330,331],{"class":83},"randomUUID",[39,333,334],{"class":49},"() },\n",[39,336,338,341],{"class":41,"line":337},10,[39,339,340],{"class":73},"  REFRESH_SECRET",[39,342,117],{"class":49},[39,344,346,348,350],{"class":41,"line":345},11,[39,347,123],{"class":49},[39,349,154],{"class":56},[39,351,298],{"class":49},[39,353,355],{"class":41,"line":354},12,[39,356,139],{"class":49},[10,358,359],{},"When the access token expires, the client sends the refresh token to a dedicated endpoint to obtain a new access token without requiring the user to re-authenticate. This pattern minimizes the window during which a stolen access token is valid while maintaining a seamless user experience.",[19,361,363],{"id":362},"refresh-token-rotation","Refresh Token Rotation",[10,365,366],{},"Refresh token rotation issues a new refresh token with every use, invalidating the previous one. If an attacker steals a refresh token and uses it, the legitimate client's next refresh attempt fails because its token was already rotated. This detection mechanism alerts you to potential compromise.",[30,368,370],{"className":32,"code":369,"language":34,"meta":35,"style":35},"async function refreshTokenHandler(oldRefreshToken: string) {\n  \u002F\u002F Verify the old token\n  const payload = jwt.verify(oldRefreshToken, REFRESH_SECRET)\n\n  \u002F\u002F Check if it's in the revoked list (reuse detection)\n  if (await isTokenRevoked(payload.jti)) {\n    \u002F\u002F Potential theft: revoke entire family\n    await revokeTokenFamily(payload.sub)\n    throw new Error('Token reuse detected')\n  }\n\n  \u002F\u002F Revoke the old refresh token\n  await revokeToken(payload.jti)\n\n  \u002F\u002F Issue new token pair\n  const newAccessToken = jwt.sign(\n    { sub: payload.sub, role: payload.role },\n    ACCESS_SECRET,\n    { expiresIn: '15m' }\n  )\n\n  const newRefreshToken = jwt.sign(\n    { sub: payload.sub, jti: crypto.randomUUID() },\n    REFRESH_SECRET,\n    { expiresIn: '7d' }\n  )\n\n  return { accessToken: newAccessToken, refreshToken: newRefreshToken }\n}\n",[14,371,372,399,404,427,431,436,453,458,469,487,492,496,501,513,518,524,540,546,554,564,570,575,591,601,609,618,623,628,637],{"__ignoreMap":35},[39,373,374,377,380,383,386,390,393,396],{"class":41,"line":42},[39,375,376],{"class":45},"async",[39,378,379],{"class":45}," function",[39,381,382],{"class":83}," refreshTokenHandler",[39,384,385],{"class":49},"(",[39,387,389],{"class":388},"s4XuR","oldRefreshToken",[39,391,392],{"class":45},":",[39,394,395],{"class":73}," string",[39,397,398],{"class":49},") {\n",[39,400,401],{"class":41,"line":60},[39,402,403],{"class":132},"  \u002F\u002F Verify the old token\n",[39,405,406,409,412,414,416,419,422,425],{"class":41,"line":67},[39,407,408],{"class":45},"  const",[39,410,411],{"class":73}," payload",[39,413,77],{"class":45},[39,415,80],{"class":49},[39,417,418],{"class":83},"verify",[39,420,421],{"class":49},"(oldRefreshToken, ",[39,423,424],{"class":73},"REFRESH_SECRET",[39,426,139],{"class":49},[39,428,429],{"class":41,"line":90},[39,430,64],{"emptyLinePlaceholder":63},[39,432,433],{"class":41,"line":108},[39,434,435],{"class":132},"  \u002F\u002F Check if it's in the revoked list (reuse detection)\n",[39,437,438,441,444,447,450],{"class":41,"line":120},[39,439,440],{"class":45},"  if",[39,442,443],{"class":49}," (",[39,445,446],{"class":45},"await",[39,448,449],{"class":83}," isTokenRevoked",[39,451,452],{"class":49},"(payload.jti)) {\n",[39,454,455],{"class":41,"line":136},[39,456,457],{"class":132},"    \u002F\u002F Potential theft: revoke entire family\n",[39,459,460,463,466],{"class":41,"line":309},[39,461,462],{"class":45},"    await",[39,464,465],{"class":83}," revokeTokenFamily",[39,467,468],{"class":49},"(payload.sub)\n",[39,470,471,474,477,480,482,485],{"class":41,"line":325},[39,472,473],{"class":45},"    throw",[39,475,476],{"class":45}," new",[39,478,479],{"class":83}," Error",[39,481,385],{"class":49},[39,483,484],{"class":56},"'Token reuse detected'",[39,486,139],{"class":49},[39,488,489],{"class":41,"line":337},[39,490,491],{"class":49},"  }\n",[39,493,494],{"class":41,"line":345},[39,495,64],{"emptyLinePlaceholder":63},[39,497,498],{"class":41,"line":354},[39,499,500],{"class":132},"  \u002F\u002F Revoke the old refresh token\n",[39,502,504,507,510],{"class":41,"line":503},13,[39,505,506],{"class":45},"  await",[39,508,509],{"class":83}," revokeToken",[39,511,512],{"class":49},"(payload.jti)\n",[39,514,516],{"class":41,"line":515},14,[39,517,64],{"emptyLinePlaceholder":63},[39,519,521],{"class":41,"line":520},15,[39,522,523],{"class":132},"  \u002F\u002F Issue new token pair\n",[39,525,527,529,532,534,536,538],{"class":41,"line":526},16,[39,528,408],{"class":45},[39,530,531],{"class":73}," newAccessToken",[39,533,77],{"class":45},[39,535,80],{"class":49},[39,537,84],{"class":83},[39,539,87],{"class":49},[39,541,543],{"class":41,"line":542},17,[39,544,545],{"class":49},"    { sub: payload.sub, role: payload.role },\n",[39,547,549,552],{"class":41,"line":548},18,[39,550,551],{"class":73},"    ACCESS_SECRET",[39,553,117],{"class":49},[39,555,557,560,562],{"class":41,"line":556},19,[39,558,559],{"class":49},"    { expiresIn: ",[39,561,126],{"class":56},[39,563,298],{"class":49},[39,565,567],{"class":41,"line":566},20,[39,568,569],{"class":49},"  )\n",[39,571,573],{"class":41,"line":572},21,[39,574,64],{"emptyLinePlaceholder":63},[39,576,578,580,583,585,587,589],{"class":41,"line":577},22,[39,579,408],{"class":45},[39,581,582],{"class":73}," newRefreshToken",[39,584,77],{"class":45},[39,586,80],{"class":49},[39,588,84],{"class":83},[39,590,87],{"class":49},[39,592,594,597,599],{"class":41,"line":593},23,[39,595,596],{"class":49},"    { sub: payload.sub, jti: crypto.",[39,598,331],{"class":83},[39,600,334],{"class":49},[39,602,604,607],{"class":41,"line":603},24,[39,605,606],{"class":73},"    REFRESH_SECRET",[39,608,117],{"class":49},[39,610,612,614,616],{"class":41,"line":611},25,[39,613,559],{"class":49},[39,615,154],{"class":56},[39,617,298],{"class":49},[39,619,621],{"class":41,"line":620},26,[39,622,569],{"class":49},[39,624,626],{"class":41,"line":625},27,[39,627,64],{"emptyLinePlaceholder":63},[39,629,631,634],{"class":41,"line":630},28,[39,632,633],{"class":45},"  return",[39,635,636],{"class":49}," { accessToken: newAccessToken, refreshToken: newRefreshToken }\n",[39,638,640],{"class":41,"line":639},29,[39,641,642],{"class":49},"}\n",[19,644,646],{"id":645},"common-pitfalls","Common Pitfalls",[10,648,649,652],{},[243,650,651],{},"Setting expiration too far out."," An access token that lasts 24 hours means a stolen token grants access for a full day. Keep access tokens under 15 minutes.",[10,654,655,658,659,661,662,665],{},[243,656,657],{},"Ignoring clock skew."," JWT validation compares ",[14,660,16],{}," against the server's clock. If client and server clocks differ by more than the token lifetime, valid tokens may be rejected or expired tokens accepted. Most JWT libraries accept a ",[14,663,664],{},"clockTolerance"," option (typically 30 seconds) to handle minor skew.",[10,667,668,671],{},[243,669,670],{},"Not storing refresh tokens securely."," Refresh tokens must be stored server-side (in a database or Redis) so they can be revoked. Stateless refresh tokens cannot be invalidated, defeating the purpose of rotation.",[10,673,674,677],{},[243,675,676],{},"Using the same secret for access and refresh tokens."," Always use separate signing keys. If an access token secret is compromised, refresh tokens should remain secure.",[10,679,680,687,688,690,691,693],{},[243,681,682,683,686],{},"Missing the ",[14,684,685],{},"iat"," (issued at) claim."," Including ",[14,689,685],{}," alongside ",[14,692,16],{}," enables you to calculate the token's age and enforce maximum token age policies independent of expiration.",[19,695,697],{"id":696},"token-revocation-strategies","Token Revocation Strategies",[10,699,700],{},"Since JWTs cannot be revoked by design, you need supplementary mechanisms:",[702,703,704,714,724],"ul",{},[240,705,706,709,710,713],{},[243,707,708],{},"Token blocklist:"," Store revoked token IDs (",[14,711,712],{},"jti"," claims) in Redis with TTLs matching their remaining lifetime. Check the blocklist on every request.",[240,715,716,719,720,723],{},[243,717,718],{},"Versioned claims:"," Include a ",[14,721,722],{},"tokenVersion"," claim that maps to a user record. Incrementing the version invalidates all outstanding tokens for that user.",[240,725,726,729],{},[243,727,728],{},"Short lifetimes + rotation:"," The simplest strategy—keep access tokens so short that revocation is rarely needed, and rotate refresh tokens for detection.",[10,731,732,733,738,739,148,741,743,744,747],{},"For decoding and inspecting JWT expiration times, the ",[734,735,737],"a",{"href":736},"\u002Ftools\u002Fjwt-decoder","JWT Decoder tool"," displays all claims including ",[14,740,16],{},[14,742,685],{},", and ",[14,745,746],{},"nbf"," in a readable format.",[749,750,751],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":35,"searchDepth":60,"depth":60,"links":753},[754,755,756,757,758],{"id":21,"depth":60,"text":22},{"id":231,"depth":60,"text":232},{"id":362,"depth":60,"text":363},{"id":645,"depth":60,"text":646},{"id":696,"depth":60,"text":697},"2026-05-28","How to set exp claims, handle refresh tokens, and avoid common expiration pitfalls.","md",{},"\u002Fguides\u002Fjwt-token-expiration",{"title":5,"description":760},"guides\u002Fjwt-token-expiration","R-AYbhFhUeeHtL8GoMNnzirBru-WtXo8eBUki-gBDOg",1780401334804]