[{"data":1,"prerenderedAt":348},["ShallowReactive",2],{"blog-diagrams/session-vs-jwt-revocation-gap":3},{"id":4,"title":5,"body":6,"category":315,"date":316,"dateModified":316,"description":317,"draft":318,"extension":319,"faq":320,"featured":318,"headerVariant":331,"image":332,"keywords":334,"meta":335,"navigation":197,"ogDescription":336,"ogTitle":337,"path":338,"readTime":339,"schemaOrg":340,"schemaType":341,"seo":342,"sitemap":343,"stem":344,"tags":345,"twitterCard":346,"__hash__":347},"blog/blog/diagrams/session-vs-jwt-revocation-gap.md","The JWT Revocation Gap, Timed (Diagram)",{"type":7,"value":8,"toc":309},"minimark",[9,13,94,103,109,115,121,124,128,275,293,305],[10,11,12],"p",{},"You click log out. In one app the credential stops working before the page finishes reloading. In the other it keeps working for another fifteen minutes, and there is nothing you can do about it from the server.",[14,15,22,27,30,37,43,49,52,63,67,70,73,84],"diagram",{"alt":16,"caption":17,"height":18,"href":19,"src":20,"width":21},"A sequence diagram comparing logout in two apps. The server-session app deletes the session row, so a stolen cookie sent one second later gets a 401. The stateless JWT app deletes nothing, so a stolen token sent one second later still returns 200 OK and keeps working until the expiry baked into the token.","One logout, two outcomes, and a window measured in minutes.",1232,"/blog/comparisons/session-vs-jwt","/diagrams/session-vs-jwt-revocation-gap.png",1932,[23,24,26],"h2",{"id":25},"reading-the-diagram","Reading the diagram",[10,28,29],{},"Follow the two logouts first, then the two requests that come after them.",[10,31,32,36],{},[33,34,35],"strong",{},"Steps 1 and 2, the session app."," Logging out is a write. Your server deletes the row that says this session exists. From that instant the credential in the browser points at nothing, and it does not matter how many copies of it are floating around.",[10,38,39,42],{},[33,40,41],{},"Steps 3 and 4, the JWT app."," Logging out is not a write. There is no row, because that is the entire design: the token proves itself with a signature, so the server never needed a record of it. All logout can do is tell the browser to forget its copy. Your server did not learn anything.",[10,44,45,48],{},[33,46,47],{},"Steps 5 through 8, the red band."," This is the part prose keeps failing to make concrete. One second after logout, the same credential arrives from somewhere else. The session app looks for the row, finds nothing, and answers 401. The JWT app checks the signature, finds it valid and unexpired, and answers 200 OK.",[10,50,51],{},"Both apps behaved exactly as designed. Only one of them is still trusting a credential the user tried to cancel.",[10,53,54,57,58,62],{},[33,55,56],{},"The note at the bottom."," The window closes when the ",[59,60,61],"code",{},"exp"," claim inside the token passes, and never earlier. That number was chosen when the token was minted, by whoever wrote the auth code. If an AI wrote it and picked seven days because the example it learned from picked seven days, your revocation gap is seven days.",[23,64,66],{"id":65},"why-this-matters-more-than-it-sounds","Why this matters more than it sounds",[10,68,69],{},"The gap only bites when there is a second copy of the credential, which is exactly the situation logout is for. Someone logs out of a shared laptop. An admin revokes a contractor's access. A user changes their password because they think they were phished.",[10,71,72],{},"In each case the person taking the action believes it took effect. With a stateless JWT it did not, and no error told them so.",[74,75,76,81],"warning-box",{},[77,78,80],"h4",{"id":79},"password-changes-are-the-sharp-edge","Password changes are the sharp edge",[10,82,83],{},"\"Change your password if you think someone has your account\" is advice every app gives. On a stateless JWT setup it does not evict the attacker, because their existing token is unaffected by anything that happened to the password. They keep the session until it expires on its own schedule.",[85,86,87],"faq-section",{},[88,89,91],"faq-item",{"question":90},"Why does logging out not invalidate a JWT?",[10,92,93],{},"Because nothing on your server was holding it. A stateless JWT carries its own proof of validity in the signature, and your server verifies that signature rather than looking the token up anywhere. Logging out clears the copy in the browser, which has no effect on any other copy that already exists.",[88,95,97],{"question":96},"How long does the gap last?",[10,98,99,100,102],{},"Exactly as long as the ",[59,101,61],{}," claim inside the token, because that is the only thing that can end it. Fifteen minutes is a common access-token setting. Longer values show up regularly in generated auth code, and the gap is however long that value says.",[88,104,106],{"question":105},"Does a refresh token fix this?",[10,107,108],{},"It shortens the window, it does not close it. Revoking the refresh token stops new access tokens being issued, capping the damage at the current token's remaining life. That is a genuine improvement and the reason short access-token expiry matters, but the token already in play still works.",[88,110,112],{"question":111},"What actually revokes a JWT then?",[10,113,114],{},"A blocklist your server checks on every request. That means a lookup per request, which is server-side state, which is the thing stateless tokens were picked to avoid. You end up paying the cost of sessions and the complexity of tokens together.",[88,116,118],{"question":117},"So should I just use server sessions?",[10,119,120],{},"For a normal browser-based web app, usually yes. Sessions revoke instantly, and the lookup you were avoiding comes back the moment you add a blocklist anyway. JWTs earn their keep between services, or where the party doing the checking genuinely cannot reach your database.",[10,122,123],{},"::",[23,125,127],{"id":126},"the-source","The source",[129,130,135],"pre",{"className":131,"code":132,"language":133,"meta":134,"style":134},"language-mermaid shiki shiki-themes github-dark","---\ntitle: \"Logging out does not log out a JWT\"\n---\nsequenceDiagram\n    autonumber\n    participant U as You, clicking\u003Cbr/>log out\n    participant T as Someone with a\u003Cbr/>stolen copy\n    participant S as App using\u003Cbr/>server sessions\n    participant J as App using\u003Cbr/>stateless JWTs\n\n    U->>S: log out\n    S-->>U: the session row is deleted\n    U->>J: log out\n    J-->>U: nothing to delete.\u003Cbr/>your browser drops its copy\n\n    rect rgb(254, 242, 242)\n        T->>S: same cookie, one second later\n        S-->>T: 401. no row left to look up\n        T->>J: same token, one second later\n        J-->>T: 200 OK. still signed in\n    end\n\n    Note over U,J: one logout, two outcomes. the JWT keeps working\u003Cbr/>until the expiry baked into it, often fifteen minutes.\n","mermaid","",[59,136,137,145,151,156,162,168,174,180,186,192,199,205,211,217,223,228,234,240,246,252,258,264,269],{"__ignoreMap":134},[138,139,142],"span",{"class":140,"line":141},"line",1,[138,143,144],{},"---\n",[138,146,148],{"class":140,"line":147},2,[138,149,150],{},"title: \"Logging out does not log out a JWT\"\n",[138,152,154],{"class":140,"line":153},3,[138,155,144],{},[138,157,159],{"class":140,"line":158},4,[138,160,161],{},"sequenceDiagram\n",[138,163,165],{"class":140,"line":164},5,[138,166,167],{},"    autonumber\n",[138,169,171],{"class":140,"line":170},6,[138,172,173],{},"    participant U as You, clicking\u003Cbr/>log out\n",[138,175,177],{"class":140,"line":176},7,[138,178,179],{},"    participant T as Someone with a\u003Cbr/>stolen copy\n",[138,181,183],{"class":140,"line":182},8,[138,184,185],{},"    participant S as App using\u003Cbr/>server sessions\n",[138,187,189],{"class":140,"line":188},9,[138,190,191],{},"    participant J as App using\u003Cbr/>stateless JWTs\n",[138,193,195],{"class":140,"line":194},10,[138,196,198],{"emptyLinePlaceholder":197},true,"\n",[138,200,202],{"class":140,"line":201},11,[138,203,204],{},"    U->>S: log out\n",[138,206,208],{"class":140,"line":207},12,[138,209,210],{},"    S-->>U: the session row is deleted\n",[138,212,214],{"class":140,"line":213},13,[138,215,216],{},"    U->>J: log out\n",[138,218,220],{"class":140,"line":219},14,[138,221,222],{},"    J-->>U: nothing to delete.\u003Cbr/>your browser drops its copy\n",[138,224,226],{"class":140,"line":225},15,[138,227,198],{"emptyLinePlaceholder":197},[138,229,231],{"class":140,"line":230},16,[138,232,233],{},"    rect rgb(254, 242, 242)\n",[138,235,237],{"class":140,"line":236},17,[138,238,239],{},"        T->>S: same cookie, one second later\n",[138,241,243],{"class":140,"line":242},18,[138,244,245],{},"        S-->>T: 401. no row left to look up\n",[138,247,249],{"class":140,"line":248},19,[138,250,251],{},"        T->>J: same token, one second later\n",[138,253,255],{"class":140,"line":254},20,[138,256,257],{},"        J-->>T: 200 OK. still signed in\n",[138,259,261],{"class":140,"line":260},21,[138,262,263],{},"    end\n",[138,265,267],{"class":140,"line":266},22,[138,268,198],{"emptyLinePlaceholder":197},[138,270,272],{"class":140,"line":271},23,[138,273,274],{},"    Note over U,J: one logout, two outcomes. the JWT keeps working\u003Cbr/>until the expiry baked into it, often fifteen minutes.\n",[276,277,278,283,288],"related-articles",{},[279,280],"related-card",{"description":281,"href":19,"title":282},"The full comparison this diagram belongs to, including when a stateless token is the right call.","Sessions vs JWTs",[279,284],{"description":285,"href":286,"title":287},"The same gap drawn as a state machine: a stateless token has only one exit, and it is the clock.","/blog/diagrams/session-lifecycle-and-the-jwt-gap","Where a JWT Cannot Be Revoked",[279,289],{"description":290,"href":291,"title":292},"How rotation shortens the window this diagram measures, and how reuse detection catches a stolen refresh token.","/blog/diagrams/jwt-refresh-rotation","Refresh Token Rotation",[294,295,298,302],"cta-box",{"href":296,"label":297},"/","Start Free Scan",[23,299,301],{"id":300},"check-how-your-tokens-are-configured","Check How Your Tokens Are Configured",[10,303,304],{},"CheckYourVibe reads what your deployed app actually ships, including token lifetimes long enough to make a logout meaningless.",[306,307,308],"style",{},"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);}",{"title":134,"searchDepth":147,"depth":147,"links":310},[311,312,313,314],{"id":25,"depth":147,"text":26},{"id":65,"depth":147,"text":66},{"id":126,"depth":147,"text":127},{"id":300,"depth":147,"text":301},"diagrams","2026-08-25","A sequence diagram of one logout against two designs. The session app refuses the next request. The JWT app returns 200 OK until the token's own expiry runs out.",false,"md",[321,323,325,327,329],{"question":90,"answer":322},"Because nothing on your server was holding it. A stateless JWT carries its own proof of validity in the signature, and your server checks that signature rather than looking the token up anywhere. Logging out clears the copy in your browser, which does nothing to any other copy that already exists.",{"question":96,"answer":324},"Exactly as long as the expiry claim inside the token, because that is the only thing that can end it. Fifteen minutes is a common access-token setting. If someone configured hours or days, which happens more often than it should in AI-generated auth code, the gap is that long.",{"question":105,"answer":326},"It shortens the window rather than closing it. Revoking the refresh token stops new access tokens being issued, so the damage is capped at the current access token's remaining life. That is a real improvement and it is why short access-token expiry matters, but the current token still works until it expires.",{"question":111,"answer":328},"A blocklist your server checks on every request, which means a database lookup per request. That is server-side state, which is the thing stateless JWTs were chosen to avoid. You end up carrying the cost of sessions and the complexity of tokens at the same time.",{"question":117,"answer":330},"For a normal web app where users log in through a browser, usually yes. Sessions revoke instantly and the database lookup you were trying to avoid happens anyway once you add a blocklist. JWTs earn their place between services, or where the checking party genuinely cannot reach your database.","blue",{"src":333,"alt":16},"https://checkyourvibe.dev/diagrams/session-vs-jwt-revocation-gap.png","jwt revocation diagram, jwt logout not working, revoke jwt token, session vs jwt security, jwt still valid after logout, jwt blocklist",{"ogImage":333},"You clicked log out. One app stopped trusting the credential immediately. The other kept honouring it for another fifteen minutes.",null,"/blog/diagrams/session-vs-jwt-revocation-gap","4 min read","[object Object]","Article",{"title":5,"description":317},{"loc":338},"blog/diagrams/session-vs-jwt-revocation-gap",[],"summary_large_image","PAJdmD_Op0K0xM_0XOQHD8FT0YaA1_usKsr427E2yVM",1787862046434]