[{"data":1,"prerenderedAt":1249},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fcaching-html-for-logged-in-users-safely":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fcaching-html-for-logged-in-users-safely":1241},{"id":4,"title":5,"body":6,"description":1220,"extension":1221,"meta":1222,"navigation":1234,"path":1235,"seo":1236,"stem":1239,"__hash__":1240},"content\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fcaching-html-for-logged-in-users-safely\u002Findex.md","Caching HTML for Logged-In Users Safely",{"type":7,"value":8,"toc":1203},"minimark",[9,14,29,37,137,142,176,180,186,192,198,210,340,344,349,352,574,577,581,584,588,666,670,679,683,882,1012,1016,1019,1023,1029,1033,1062,1066,1072,1078,1084,1090,1094,1110,1119,1128,1137,1161,1165,1188,1193,1196,1199],[10,11,13],"h1",{"id":12},"how-to-cache-html-for-logged-in-users-safely","How to Cache HTML for Logged-In Users Safely",[15,16,17,18,23,24,28],"p",{},"This guide covers the highest-stakes pattern in ",[19,20,22],"a",{"href":21},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002F","Edge Compute & Dynamic Caching",", within ",[19,25,27],{"href":26},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced Caching Strategies & CDN Architecture",". Logged-in users are often a site's most valuable visitors, and they usually get its slowest pages: authenticated HTML bypasses the CDN, so every request pays the full origin render. Caching that HTML promises the biggest TTFB improvement available — and carries the worst possible failure mode. If a personalised page is cached and served to someone else, you have leaked one user's data to another.",[15,30,31,32,36],{},"Safe caching for logged-in users is therefore an architecture, not a header change. The cached artefact must be ",[33,34,35],"em",{},"provably"," free of per-user data; per-user data must arrive through a separate, uncached channel; and automated tests must check isolation continuously. With those in place, logged-in pages can be served at cache speed while personal data stays private.",[15,38,39],{},[40,41,47,48,47,55,47,59,47,62,47,71,47,77,47,85,47,92,47,96,47,102,47,107,47,111,47,114,47,120,47,124,47,127,47,130,47,134,47],"svg",{"viewBox":42,"width":43,"role":44,"ariaLabel":45,"style":46},"0 0 760 280","100%","img","Layers that make caching authenticated HTML safe, from personal-free shell rendering to continuous isolation testing.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[49,50],"rect",{"className":51,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],"svg-canvas","0","#ffffff",[56,57,58],"title",{},"Safe architecture for authenticated pages",[60,61,45],"desc",{},[49,63],{"x":64,"y":64,"width":65,"height":66,"rx":67,"fill":68,"stroke":69,"style":70},"1","758","278","10","none","currentColor","stroke-opacity:0.18",[72,73,58],"text",{"x":74,"y":75,"fill":69,"style":76},"28.0","34.0","font-size:16px;font-weight:700",[49,78],{"x":74,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":83,"style":84},"56.0","704.0","52.0","6","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[72,86,91],{"x":87,"y":88,"fill":69,"style":89,"textAnchor":90},"42.0","78.0","font-size:13px;font-weight:700","start","Shell rendering without the request",[72,93,95],{"x":87,"y":94,"fill":69,"style":89,"textAnchor":90},"95.0","user",[72,97,101],{"x":98,"y":99,"fill":69,"style":100,"textAnchor":90},"278.0","86.5","font-size:12px","templates cannot access session or user objects",[49,103],{"x":74,"y":104,"width":80,"height":105,"rx":82,"fill":69,"stroke":69,"style":106},"118.0","40.0","fill-opacity:0.06;stroke-opacity:0.4",[72,108,110],{"x":87,"y":109,"fill":69,"style":89,"textAnchor":90},"142.5","Explicit, low-cardinality cache keys",[72,112,113],{"x":98,"y":109,"fill":69,"style":100,"textAnchor":90},"role or plan tier at most — never user IDs or raw cookies",[49,115],{"x":74,"y":116,"width":80,"height":105,"rx":82,"fill":117,"stroke":118,"style":119},"168.0","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[72,121,123],{"x":87,"y":122,"fill":69,"style":89,"textAnchor":90},"192.5","Personal data via private endpoints",[72,125,126],{"x":98,"y":122,"fill":69,"style":100,"textAnchor":90},"Cache-Control: private, no-store; fetched by edge or client",[49,128],{"x":74,"y":129,"width":80,"height":105,"rx":82,"fill":83,"stroke":83,"style":84},"218.0",[72,131,133],{"x":87,"y":132,"fill":69,"style":89,"textAnchor":90},"242.5","Isolation tests",[72,135,136],{"x":98,"y":132,"fill":69,"style":100,"textAnchor":90},"two users, same URL, compare responses — in CI and in production",[138,139,141],"h2",{"id":140},"rapid-diagnosis","Rapid Diagnosis",[143,144,145,153,159,170],"ul",{},[146,147,148,152],"li",{},[149,150,151],"strong",{},"Inventory personal data in templates."," Names, emails, avatars, addresses, order data, saved items, CSRF tokens and feature flags per user — anything that differs between users.",[146,154,155,158],{},[149,156,157],{},"Check how the CDN treats authenticated requests."," Many configurations bypass the cache when a session cookie is present; some cache them accidentally if headers allow.",[146,160,161,169],{},[149,162,163,164,168],{},"Check ",[165,166,167],"code",{},"Set-Cookie"," on HTML."," Session refreshes on page responses both prevent caching and, if cached, would hand one user's cookie to others.",[146,171,172,175],{},[149,173,174],{},"Check role-based content."," Admin links or plan-specific features in the shell must be part of the key or moved to fragments.",[138,177,179],{"id":178},"root-cause-analysis","Root Cause Analysis",[15,181,182,185],{},[149,183,184],{},"1. Templates with implicit access to the user."," Frameworks make the current user available everywhere, so personal data seeps into shared markup unnoticed.",[15,187,188,191],{},[149,189,190],{},"2. Over-broad cache keys or none at all."," Keying on full cookies fragments the cache; ignoring cookies entirely without removing personal data from the shell leaks data.",[15,193,194,197],{},[149,195,196],{},"3. Tokens in markup."," Per-session CSRF tokens embedded in forms make every page personal.",[15,199,200,47,203,205,206,209],{},[149,201,202],{},"4. Header leakage.",[165,204,167],{},", per-user ",[165,207,208],{},"ETag","s or debug headers containing user identifiers can be cached along with the body.",[15,211,212],{},[40,213,47,216,47,219,47,222,47,224,47,227,47,229,47,233,47,239,47,243,47,248,47,251,47,255,47,259,47,263,47,265,47,268,47,270,47,273,47,276,47,280,47,282,47,285,47,287,47,290,47,293,47,297,47,299,47,301,47,303,47,306,47,309,47,313,47,315,47,318,47,320,47,323,47,326,47,330,47,332,47,335,47,337,47],{"viewBox":214,"width":43,"role":44,"ariaLabel":215,"style":46},"0 0 760 258","Common personal elements on authenticated pages and how to serve each safely when the shell is cached.",[49,217],{"className":218,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,220,221],{},"Personal elements and where they belong",[60,223,215],{},[49,225],{"x":64,"y":64,"width":65,"height":226,"rx":67,"fill":68,"stroke":69,"style":70},"256",[72,228,221],{"x":74,"y":75,"fill":69,"style":76},[49,230],{"x":74,"y":79,"width":231,"height":232,"rx":53,"fill":69,"stroke":69,"style":106},"165.2","30.0",[72,234,238],{"x":235,"y":236,"fill":69,"style":237,"textAnchor":90},"38.0","75.5","font-size:12.5px;font-weight:700","Element",[49,240],{"x":241,"y":79,"width":242,"height":232,"rx":53,"fill":69,"stroke":69,"style":106},"193.2","269.4",[72,244,247],{"x":245,"y":236,"fill":69,"style":237,"textAnchor":246},"327.9","middle","In cached shell?",[49,249],{"x":250,"y":79,"width":242,"height":232,"rx":53,"fill":69,"stroke":69,"style":106},"462.6",[72,252,254],{"x":253,"y":236,"fill":69,"style":237,"textAnchor":246},"597.3","Serve via",[49,256],{"x":74,"y":257,"width":231,"height":232,"rx":53,"fill":68,"stroke":69,"style":258},"86.0","stroke-opacity:0.35",[72,260,262],{"x":235,"y":261,"fill":69,"style":237,"textAnchor":90},"105.5","User name and avatar",[49,264],{"x":241,"y":257,"width":242,"height":232,"rx":53,"fill":117,"stroke":118,"style":119},[72,266,267],{"x":245,"y":261,"fill":69,"style":100,"textAnchor":246},"no",[49,269],{"x":250,"y":257,"width":242,"height":232,"rx":53,"fill":68,"stroke":69,"style":258},[72,271,272],{"x":253,"y":261,"fill":69,"style":100,"textAnchor":246},"fragment \u002F edge rewrite from signed token",[49,274],{"x":74,"y":275,"width":231,"height":232,"rx":53,"fill":68,"stroke":69,"style":258},"116.0",[72,277,279],{"x":235,"y":278,"fill":69,"style":237,"textAnchor":90},"135.5","Role-based navigation",[49,281],{"x":241,"y":275,"width":242,"height":232,"rx":53,"fill":69,"stroke":69,"style":106},[72,283,284],{"x":245,"y":278,"fill":69,"style":100,"textAnchor":246},"only if role is in key",[49,286],{"x":250,"y":275,"width":242,"height":232,"rx":53,"fill":68,"stroke":69,"style":258},[72,288,289],{"x":253,"y":278,"fill":69,"style":100,"textAnchor":246},"key by role or fragment",[49,291],{"x":74,"y":292,"width":231,"height":232,"rx":53,"fill":68,"stroke":69,"style":258},"146.0",[72,294,296],{"x":235,"y":295,"fill":69,"style":237,"textAnchor":90},"165.5","CSRF token",[49,298],{"x":241,"y":292,"width":242,"height":232,"rx":53,"fill":117,"stroke":118,"style":119},[72,300,267],{"x":245,"y":295,"fill":69,"style":100,"textAnchor":246},[49,302],{"x":250,"y":292,"width":242,"height":232,"rx":53,"fill":68,"stroke":69,"style":258},[72,304,305],{"x":253,"y":295,"fill":69,"style":100,"textAnchor":246},"fetched on form focus or per-session cookie",[49,307],{"x":74,"y":308,"width":231,"height":232,"rx":53,"fill":68,"stroke":69,"style":258},"176.0",[72,310,312],{"x":235,"y":311,"fill":69,"style":237,"textAnchor":90},"195.5","Product and content data",[49,314],{"x":241,"y":308,"width":242,"height":232,"rx":53,"fill":83,"stroke":83,"style":84},[72,316,317],{"x":245,"y":311,"fill":69,"style":100,"textAnchor":246},"yes",[49,319],{"x":250,"y":308,"width":242,"height":232,"rx":53,"fill":68,"stroke":69,"style":258},[72,321,322],{"x":253,"y":311,"fill":69,"style":100,"textAnchor":246},"cached shell",[49,324],{"x":74,"y":325,"width":231,"height":232,"rx":53,"fill":68,"stroke":69,"style":258},"206.0",[72,327,329],{"x":235,"y":328,"fill":69,"style":237,"textAnchor":90},"225.5","Set-Cookie headers",[49,331],{"x":241,"y":325,"width":242,"height":232,"rx":53,"fill":117,"stroke":118,"style":119},[72,333,334],{"x":245,"y":328,"fill":69,"style":100,"textAnchor":246},"never",[49,336],{"x":250,"y":325,"width":242,"height":232,"rx":53,"fill":68,"stroke":69,"style":258},[72,338,339],{"x":253,"y":328,"fill":69,"style":100,"textAnchor":246},"separate session endpoint",[138,341,343],{"id":342},"step-by-step-resolution","Step-by-Step Resolution",[345,346,348],"h3",{"id":347},"_1-make-shell-rendering-structurally-user-free","1. Make shell rendering structurally user-free",[15,350,351],{},"Render cacheable templates through a code path that does not receive the user at all — for example, a separate render function or a framework option that strips cookies from the request before rendering. Structural separation is far more reliable than discipline.",[353,354,359],"pre",{"className":355,"code":356,"language":357,"meta":358,"style":358},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F Express example: the cacheable route never sees cookies.\napp.get('\u002Fproducts\u002F:id', stripAuth, async (req, res) => {\n  const html = await renderProductShell(req.params.id);        \u002F\u002F no req.user, no cookies\n  res.set('Cache-Control', 'public, s-maxage=600, stale-while-revalidate=86400');\n  res.set('Cache-Tag', `product-${req.params.id}`);\n  res.send(html);\n});\nfunction stripAuth(req, _res, next) { delete req.headers.cookie; req.user = undefined; next(); }\n\u002F\u002F trade-off: anything that previously relied on the user in this template must\n\u002F\u002F move to a fragment; some features become a little more complex to build.\n","javascript","",[165,360,361,370,417,442,464,499,510,516,562,568],{"__ignoreMap":358},[362,363,366],"span",{"class":364,"line":365},"line",1,[362,367,369],{"class":368},"sjfSM","\u002F\u002F Express example: the cacheable route never sees cookies.\n",[362,371,373,377,381,384,388,391,395,398,402,405,408,411,414],{"class":364,"line":372},2,[362,374,376],{"class":375},"saISM","app.",[362,378,380],{"class":379},"smZ65","get",[362,382,383],{"class":375},"(",[362,385,387],{"class":386},"sZ8jY","'\u002Fproducts\u002F:id'",[362,389,390],{"class":375},", stripAuth, ",[362,392,394],{"class":393},"sPARh","async",[362,396,397],{"class":375}," (",[362,399,401],{"class":400},"sQw3B","req",[362,403,404],{"class":375},", ",[362,406,407],{"class":400},"res",[362,409,410],{"class":375},") ",[362,412,413],{"class":393},"=>",[362,415,416],{"class":375}," {\n",[362,418,420,423,427,430,433,436,439],{"class":364,"line":419},3,[362,421,422],{"class":393},"  const",[362,424,426],{"class":425},"sPXB4"," html",[362,428,429],{"class":393}," =",[362,431,432],{"class":393}," await",[362,434,435],{"class":379}," renderProductShell",[362,437,438],{"class":375},"(req.params.id);        ",[362,440,441],{"class":368},"\u002F\u002F no req.user, no cookies\n",[362,443,445,448,451,453,456,458,461],{"class":364,"line":444},4,[362,446,447],{"class":375},"  res.",[362,449,450],{"class":379},"set",[362,452,383],{"class":375},[362,454,455],{"class":386},"'Cache-Control'",[362,457,404],{"class":375},[362,459,460],{"class":386},"'public, s-maxage=600, stale-while-revalidate=86400'",[362,462,463],{"class":375},");\n",[362,465,467,469,471,473,476,478,481,483,486,489,491,494,497],{"class":364,"line":466},5,[362,468,447],{"class":375},[362,470,450],{"class":379},[362,472,383],{"class":375},[362,474,475],{"class":386},"'Cache-Tag'",[362,477,404],{"class":375},[362,479,480],{"class":386},"`product-${",[362,482,401],{"class":375},[362,484,485],{"class":386},".",[362,487,488],{"class":375},"params",[362,490,485],{"class":386},[362,492,493],{"class":375},"id",[362,495,496],{"class":386},"}`",[362,498,463],{"class":375},[362,500,502,504,507],{"class":364,"line":501},6,[362,503,447],{"class":375},[362,505,506],{"class":379},"send",[362,508,509],{"class":375},"(html);\n",[362,511,513],{"class":364,"line":512},7,[362,514,515],{"class":375},"});\n",[362,517,519,522,525,527,529,531,534,536,539,542,545,548,551,554,557,559],{"class":364,"line":518},8,[362,520,521],{"class":393},"function",[362,523,524],{"class":379}," stripAuth",[362,526,383],{"class":375},[362,528,401],{"class":400},[362,530,404],{"class":375},[362,532,533],{"class":400},"_res",[362,535,404],{"class":375},[362,537,538],{"class":400},"next",[362,540,541],{"class":375},") { ",[362,543,544],{"class":393},"delete",[362,546,547],{"class":375}," req.headers.cookie; req.user ",[362,549,550],{"class":393},"=",[362,552,553],{"class":425}," undefined",[362,555,556],{"class":375},"; ",[362,558,538],{"class":379},[362,560,561],{"class":375},"(); }\n",[362,563,565],{"class":364,"line":564},9,[362,566,567],{"class":368},"\u002F\u002F trade-off: anything that previously relied on the user in this template must\n",[362,569,571],{"class":364,"line":570},10,[362,572,573],{"class":368},"\u002F\u002F move to a fragment; some features become a little more complex to build.\n",[15,575,576],{},"Expected outcome: it is impossible for the shell to include user data, even by accident.",[345,578,580],{"id":579},"_2-key-only-on-safe-low-cardinality-attributes","2. Key only on safe, low-cardinality attributes",[15,582,583],{},"If content differs by role or plan, derive a key from a verified claim (a signed token's role field) at the edge — never from client-modifiable values unless the content is non-sensitive.",[345,585,587],{"id":586},"_3-serve-personal-data-privately","3. Serve personal data privately",[353,589,591],{"className":355,"code":590,"language":357,"meta":358,"style":358},"app.get('\u002Fapi\u002Fme', requireAuth, (req, res) => {\n  res.set('Cache-Control', 'private, no-store');\n  res.json({ name: req.user.firstName, cartCount: req.user.cart.length });\n});\n\u002F\u002F trade-off: one extra request per page view. Fetch it in parallel with the\n\u002F\u002F shell (at the edge or on the client) so it never sits on the LCP path.\n",[165,592,593,619,636,652,656,661],{"__ignoreMap":358},[362,594,595,597,599,601,604,607,609,611,613,615,617],{"class":364,"line":365},[362,596,376],{"class":375},[362,598,380],{"class":379},[362,600,383],{"class":375},[362,602,603],{"class":386},"'\u002Fapi\u002Fme'",[362,605,606],{"class":375},", requireAuth, (",[362,608,401],{"class":400},[362,610,404],{"class":375},[362,612,407],{"class":400},[362,614,410],{"class":375},[362,616,413],{"class":393},[362,618,416],{"class":375},[362,620,621,623,625,627,629,631,634],{"class":364,"line":372},[362,622,447],{"class":375},[362,624,450],{"class":379},[362,626,383],{"class":375},[362,628,455],{"class":386},[362,630,404],{"class":375},[362,632,633],{"class":386},"'private, no-store'",[362,635,463],{"class":375},[362,637,638,640,643,646,649],{"class":364,"line":419},[362,639,447],{"class":375},[362,641,642],{"class":379},"json",[362,644,645],{"class":375},"({ name: req.user.firstName, cartCount: req.user.cart.",[362,647,648],{"class":425},"length",[362,650,651],{"class":375}," });\n",[362,653,654],{"class":364,"line":444},[362,655,515],{"class":375},[362,657,658],{"class":364,"line":466},[362,659,660],{"class":368},"\u002F\u002F trade-off: one extra request per page view. Fetch it in parallel with the\n",[362,662,663],{"class":364,"line":501},[362,664,665],{"class":368},"\u002F\u002F shell (at the edge or on the client) so it never sits on the LCP path.\n",[345,667,669],{"id":668},"_4-remove-set-cookie-from-cacheable-responses","4. Remove Set-Cookie from cacheable responses",[15,671,672,673,676,677,485],{},"Refresh session cookies on the ",[165,674,675],{},"\u002Fapi\u002Fme"," call or another private endpoint, and configure the CDN to strip or refuse to cache responses that still carry ",[165,678,167],{},[345,680,682],{"id":681},"_5-test-isolation-continuously","5. Test isolation continuously",[353,684,686],{"className":355,"code":685,"language":357,"meta":358,"style":358},"\u002F\u002F Run in CI and as a production synthetic check every few minutes.\nconst [a, b] = await Promise.all(['userA', 'userB'].map((u) =>\n  fetch(URL, { headers: { cookie: `session=${tokens[u]}` } }).then((r) => r.text())));\nfor (const marker of ['userA@example.test', 'User A']) {\n  if (b.includes(marker)) throw new Error(`Leak: ${marker} appeared in userB's response`);\n}\n\u002F\u002F trade-off: marker checks catch known fields only; give test accounts unique\n\u002F\u002F values in every personal field so any leak is detectable.\n",[165,687,688,693,751,803,831,867,872,877],{"__ignoreMap":358},[362,689,690],{"class":364,"line":365},[362,691,692],{"class":368},"\u002F\u002F Run in CI and as a production synthetic check every few minutes.\n",[362,694,695,698,701,703,705,708,711,713,715,718,720,723,726,729,731,734,737,740,743,746,748],{"class":364,"line":372},[362,696,697],{"class":393},"const",[362,699,700],{"class":375}," [",[362,702,19],{"class":425},[362,704,404],{"class":375},[362,706,707],{"class":425},"b",[362,709,710],{"class":375},"] ",[362,712,550],{"class":393},[362,714,432],{"class":393},[362,716,717],{"class":425}," Promise",[362,719,485],{"class":375},[362,721,722],{"class":379},"all",[362,724,725],{"class":375},"([",[362,727,728],{"class":386},"'userA'",[362,730,404],{"class":375},[362,732,733],{"class":386},"'userB'",[362,735,736],{"class":375},"].",[362,738,739],{"class":379},"map",[362,741,742],{"class":375},"((",[362,744,745],{"class":400},"u",[362,747,410],{"class":375},[362,749,750],{"class":393},"=>\n",[362,752,753,756,758,761,764,767,770,773,775,778,780,783,786,788,791,793,795,798,800],{"class":364,"line":419},[362,754,755],{"class":379},"  fetch",[362,757,383],{"class":375},[362,759,760],{"class":425},"URL",[362,762,763],{"class":375},", { headers: { cookie: ",[362,765,766],{"class":386},"`session=${",[362,768,769],{"class":375},"tokens",[362,771,772],{"class":386},"[",[362,774,745],{"class":375},[362,776,777],{"class":386},"]",[362,779,496],{"class":386},[362,781,782],{"class":375}," } }).",[362,784,785],{"class":379},"then",[362,787,742],{"class":375},[362,789,790],{"class":400},"r",[362,792,410],{"class":375},[362,794,413],{"class":393},[362,796,797],{"class":375}," r.",[362,799,72],{"class":379},[362,801,802],{"class":375},"())));\n",[362,804,805,808,810,812,815,818,820,823,825,828],{"class":364,"line":444},[362,806,807],{"class":393},"for",[362,809,397],{"class":375},[362,811,697],{"class":393},[362,813,814],{"class":425}," marker",[362,816,817],{"class":393}," of",[362,819,700],{"class":375},[362,821,822],{"class":386},"'userA@example.test'",[362,824,404],{"class":375},[362,826,827],{"class":386},"'User A'",[362,829,830],{"class":375},"]) {\n",[362,832,833,836,839,842,845,848,851,854,856,859,862,865],{"class":364,"line":466},[362,834,835],{"class":393},"  if",[362,837,838],{"class":375}," (b.",[362,840,841],{"class":379},"includes",[362,843,844],{"class":375},"(marker)) ",[362,846,847],{"class":393},"throw",[362,849,850],{"class":393}," new",[362,852,853],{"class":379}," Error",[362,855,383],{"class":375},[362,857,858],{"class":386},"`Leak: ${",[362,860,861],{"class":375},"marker",[362,863,864],{"class":386},"} appeared in userB's response`",[362,866,463],{"class":375},[362,868,869],{"class":364,"line":501},[362,870,871],{"class":375},"}\n",[362,873,874],{"class":364,"line":512},[362,875,876],{"class":368},"\u002F\u002F trade-off: marker checks catch known fields only; give test accounts unique\n",[362,878,879],{"class":364,"line":518},[362,880,881],{"class":368},"\u002F\u002F values in every personal field so any leak is detectable.\n",[15,883,884],{},[40,885,47,888,47,891,47,894,47,896,47,899,47,901,47,906,47,910,47,914,47,917,47,921,47,925,47,928,47,932,47,936,47,939,47,943,47,947,47,950,47,954,47,958,47,965,47,969,47,973,47,977,47,982,47,984,47,987,47,991,47,994,47,998,47,1001,47,1005,47,1008,47],{"viewBox":886,"width":43,"role":44,"ariaLabel":887,"style":46},"0 0 760 381","Five steps to introduce caching for authenticated HTML without risking data leaks.",[49,889],{"className":890,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,892,893],{},"Safe rollout sequence",[60,895,887],{},[49,897],{"x":64,"y":64,"width":65,"height":898,"rx":67,"fill":68,"stroke":69,"style":70},"379",[72,900,893],{"x":74,"y":75,"fill":69,"style":76},[49,902],{"x":903,"y":79,"width":904,"height":905,"rx":82,"fill":83,"stroke":83,"style":84},"72.0","660.0","51.0",[72,907,909],{"x":257,"y":908,"fill":69,"style":89,"textAnchor":90},"77.0","Structurally user-free shell rendering",[72,911,913],{"x":257,"y":912,"fill":69,"style":100,"textAnchor":90},"94.0","Strip cookies before cacheable templates render",[49,915],{"x":903,"y":916,"width":904,"height":905,"rx":82,"fill":83,"stroke":83,"style":84},"119.0",[72,918,920],{"x":257,"y":919,"fill":69,"style":89,"textAnchor":90},"140.0","Personal data to private endpoints",[72,922,924],{"x":257,"y":923,"fill":69,"style":100,"textAnchor":90},"157.0","Cache-Control: private, no-store",[49,926],{"x":903,"y":927,"width":904,"height":905,"rx":82,"fill":83,"stroke":83,"style":84},"182.0",[72,929,931],{"x":257,"y":930,"fill":69,"style":89,"textAnchor":90},"203.0","No Set-Cookie on cacheable HTML",[72,933,935],{"x":257,"y":934,"fill":69,"style":100,"textAnchor":90},"220.0","Session refresh moved to API calls",[49,937],{"x":903,"y":938,"width":904,"height":905,"rx":82,"fill":83,"stroke":83,"style":84},"245.0",[72,940,942],{"x":257,"y":941,"fill":69,"style":89,"textAnchor":90},"266.0","Isolation tests in CI and production",[72,944,946],{"x":257,"y":945,"fill":69,"style":100,"textAnchor":90},"283.0","Two users, unique markers, every few minutes",[49,948],{"x":903,"y":949,"width":904,"height":905,"rx":82,"fill":83,"stroke":83,"style":84},"308.0",[72,951,953],{"x":257,"y":952,"fill":69,"style":89,"textAnchor":90},"329.0","Gradual enablement with kill switch",[72,955,957],{"x":257,"y":956,"fill":69,"style":100,"textAnchor":90},"346.0","Per-template flag to force bypass instantly",[364,959],{"x1":960,"y1":961,"x2":960,"y2":962,"stroke":69,"strokeWidth":963,"style":964},"43.0","95.5","130.5","1.5","stroke-opacity:0.3",[364,966],{"x1":960,"y1":967,"x2":960,"y2":968,"stroke":69,"strokeWidth":963,"style":964},"158.5","193.5",[364,970],{"x1":960,"y1":971,"x2":960,"y2":972,"stroke":69,"strokeWidth":963,"style":964},"221.5","256.5",[364,974],{"x1":960,"y1":975,"x2":960,"y2":976,"stroke":69,"strokeWidth":963,"style":964},"284.5","319.5",[978,979],"circle",{"cx":960,"cy":980,"r":981,"fill":83},"81.5","13",[72,983,64],{"x":960,"y":257,"fill":54,"style":89,"textAnchor":246},[978,985],{"cx":960,"cy":986,"r":981,"fill":83},"144.5",[72,988,990],{"x":960,"y":989,"fill":54,"style":89,"textAnchor":246},"149.0","2",[978,992],{"cx":960,"cy":993,"r":981,"fill":83},"207.5",[72,995,997],{"x":960,"y":996,"fill":54,"style":89,"textAnchor":246},"212.0","3",[978,999],{"cx":960,"cy":1000,"r":981,"fill":83},"270.5",[72,1002,1004],{"x":960,"y":1003,"fill":54,"style":89,"textAnchor":246},"275.0","4",[978,1006],{"cx":960,"cy":1007,"r":981,"fill":83},"333.5",[72,1009,1011],{"x":960,"y":1010,"fill":54,"style":89,"textAnchor":246},"338.0","5",[138,1013,1015],{"id":1014},"verification","Verification",[15,1017,1018],{},"Before enabling caching, run isolation tests against staging with caching on. After enabling it for a small share of traffic, monitor the production synthetic check and error logs, and compare TTFB p75 for authenticated traffic with and without caching. Confirm CDN logs show hits for authenticated requests on cacheable templates and never for private endpoints.",[138,1020,1022],{"id":1021},"worked-example-a-subscription-news-site","Worked Example: A Subscription News Site",[15,1024,1025,1026,1028],{},"A subscription publisher's subscribers (logged in) bypassed the cache entirely, with article TTFB p75 of 640ms versus 70ms for anonymous readers. Articles were the same for all subscribers except a \"Hi, Alex\" header, a saved-articles count and a CSRF token in the comment form. The team rendered articles without the user, moved the header details to an ",[165,1027,675],{}," fragment, fetched the CSRF token when the comment box was focused, and keyed the cache on a signed \"subscriber\" claim so paywalled content was never served to anonymous visitors. Subscriber TTFB p75 fell to 85ms. A continuous isolation check with two synthetic accounts ran every five minutes and never fired.",[138,1030,1032],{"id":1031},"common-mistakes","Common Mistakes",[143,1034,1035,1044,1050,1056],{},[146,1036,1037,1043],{},[149,1038,1039,1040,485],{},"Relying on ",[165,1041,1042],{},"Vary: Cookie"," It fragments the cache and still leaks if any proxy ignores it.",[146,1045,1046,1049],{},[149,1047,1048],{},"Keying on unverified client values for access control."," A paywall keyed on a client-editable cookie can be bypassed; use signed claims.",[146,1051,1052,1055],{},[149,1053,1054],{},"Caching error pages with personal content."," Error templates often include the user menu; ensure they are user-free too or not cached.",[146,1057,1058,1061],{},[149,1059,1060],{},"No kill switch."," You need a way to force bypass instantly per template.",[138,1063,1065],{"id":1064},"edge-cases","Edge Cases",[15,1067,1068,1071],{},[149,1069,1070],{},"Server-side A\u002FB assignments per user."," If assignment is per user, put the arm in the key (low cardinality), not the user.",[15,1073,1074,1077],{},[149,1075,1076],{},"Locale from user settings."," If language comes from a profile setting, derive it at the edge from a non-sensitive cookie and key on it.",[15,1079,1080,1083],{},[149,1081,1082],{},"Preview and draft modes."," Editors viewing drafts must bypass the cache entirely; detect preview mode at the edge before any cache lookup.",[15,1085,1086,1089],{},[149,1087,1088],{},"Embedded user-generated content."," Comments and reviews are shared content and safe to cache, but moderation removals must purge quickly.",[138,1091,1093],{"id":1092},"faq","FAQ",[1095,1096,1099,1103],"details",{"className":1097},[1098],"faq-item",[1100,1101,1102],"summary",{},"Is it ever acceptable to cache per-user HTML at the edge?",[15,1104,1105,1106,1109],{},"Only with the user identifier in the cache key and strict ",[165,1107,1108],{},"private"," semantics at every layer — at which point you gain little, because each entry is used by one person. The safer and faster approach is a shared shell plus private fragments.",[1095,1111,1113,1116],{"className":1112},[1098],[1100,1114,1115],{},"Does this work with server components or streaming SSR?",[15,1117,1118],{},"Yes. Render and cache the shell (or the static segments) without the user, and stream or fetch personal segments separately. Frameworks with partial prerendering support this split directly.",[1095,1120,1122,1125],{"className":1121},[1098],[1100,1123,1124],{},"How do I handle CSRF tokens?",[15,1126,1127],{},"Use the double-submit cookie pattern, SameSite cookies, or fetch a token from a private endpoint when the user starts interacting with a form. None require embedding a per-user token in cached HTML.",[1095,1129,1131,1134],{"className":1130},[1098],[1100,1132,1133],{},"What should the isolation test check?",[15,1135,1136],{},"That a response fetched as user B never contains markers unique to user A, for every cacheable template, including error pages. Run it after deploys and on a schedule, because cache contents change over time.",[1095,1138,1140,1143],{"className":1139},[1098],[1100,1141,1142],{},"Can the browser cache these pages too?",[15,1144,1145,1146,1149,1150,1153,1154,1157,1158,1160],{},"The shared shell can be cached by browsers with revalidation (",[165,1147,1148],{},"max-age=0"," with validators, or short ",[165,1151,1152],{},"max-age","). Private endpoints should use ",[165,1155,1156],{},"private, no-store"," or short ",[165,1159,1108],{}," caching as appropriate.",[138,1162,1164],{"id":1163},"related","Related",[143,1166,1167,1174,1181],{},[146,1168,1169,1173],{},[19,1170,1172],{"href":1171},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fpersonalizing-cached-pages-at-the-edge\u002F","Personalizing cached pages at the edge"," — rewriting small personal details safely.",[146,1175,1176,1180],{},[19,1177,1179],{"href":1178},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fback-forward-cache-bfcache\u002Fcache-control-no-store-and-bfcache\u002F","Cache-Control no-store and bfcache"," — browser-side header choices for authenticated pages.",[146,1182,1183,1187],{},[19,1184,1186],{"href":1185},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcache-invalidation-patterns\u002Fpurging-cdn-cache-by-tag-on-deploy\u002F","Purging CDN cache by tag on deploy"," — keeping shared shells fresh.",[1189,1190,1192],"script",{"type":1191},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Cache HTML for Logged-In Users Safely\",\n  \"description\": \"The rules, architecture and tests that let authenticated traffic use the edge cache while guaranteeing no user ever sees another user's content.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Make shell rendering structurally user-free\",\n      \"text\": \"Render cacheable templates through a code path that does not receive the user at all — for example, a separate render function or a framework option that strips cookies from the request before rendering.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Key only on safe, low-cardinality attributes\",\n      \"text\": \"If content differs by role or plan, derive a key from a verified claim (a signed token's role field) at the edge — never from client-modifiable values unless the content is non-sensitive.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Serve personal data privately\",\n      \"text\": \"Serve personal data privately\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Remove Set-Cookie from cacheable responses\",\n      \"text\": \"Refresh session cookies on the \u002Fapi\u002Fme call or another private endpoint, and configure the CDN to strip or refuse to cache responses that still carry Set-Cookie.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 5,\n      \"name\": \"Test isolation continuously\",\n      \"text\": \"Test isolation continuously\"\n    }\n  ]\n}\n",[1189,1194,1195],{"type":1191},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Cache HTML for Logged-In Users Safely\",\n  \"description\": \"The rules, architecture and tests that let authenticated traffic use the edge cache while guaranteeing no user ever sees another user's content.\",\n  \"datePublished\": \"2026-10-06\",\n  \"dateModified\": \"2026-10-06\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fcaching-html-for-logged-in-users-safely\u002F\"\n  }\n}\n",[1189,1197,1198],{"type":1191},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 1,\n      \"name\": \"Home\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 2,\n      \"name\": \"Advanced Caching Strategies & CDN Architecture\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Edge Compute & Dynamic Caching\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Caching HTML for Logged-In Users Safely\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fcaching-html-for-logged-in-users-safely\u002F\"\n    }\n  ]\n}\n",[1200,1201,1202],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}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 .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}",{"title":358,"searchDepth":372,"depth":372,"links":1204},[1205,1206,1207,1214,1215,1216,1217,1218,1219],{"id":140,"depth":372,"text":141},{"id":178,"depth":372,"text":179},{"id":342,"depth":372,"text":343,"children":1208},[1209,1210,1211,1212,1213],{"id":347,"depth":419,"text":348},{"id":579,"depth":419,"text":580},{"id":586,"depth":419,"text":587},{"id":668,"depth":419,"text":669},{"id":681,"depth":419,"text":682},{"id":1014,"depth":372,"text":1015},{"id":1021,"depth":372,"text":1022},{"id":1031,"depth":372,"text":1032},{"id":1064,"depth":372,"text":1065},{"id":1092,"depth":372,"text":1093},{"id":1163,"depth":372,"text":1164},"The rules, architecture and tests that let authenticated traffic use the edge cache while guaranteeing no user ever sees another user's content.","md",{"slug":1223,"type":1224,"breadcrumb":1225,"datePublished":1233,"dateModified":1233},"caching-html-for-logged-in-users-safely","article",[1226,1229,1230,1231],{"name":1227,"url":1228},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1232},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fcaching-html-for-logged-in-users-safely\u002F","2026-10-06",true,"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fcaching-html-for-logged-in-users-safely",{"title":1237,"description":1238},"Caching HTML for Logged-In Users Without Leaking Data","Cache HTML for authenticated users at the edge without serving one user's data to another: personal-free shells, safe keys, no Set-Cookie, isolation tests.","advanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fcaching-html-for-logged-in-users-safely\u002Findex","NHSQmypChQO6d1riFM1LEX9BoeUnePar2Mg0v61Zv8c",[1242,1245],{"title":22,"path":1243,"stem":1244,"children":-1},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching","advanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Findex",{"title":1246,"path":1247,"stem":1248,"children":-1},"Edge Side Includes vs Client-Side Fragments","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fedge-side-includes-vs-client-side-fragments","advanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fedge-side-includes-vs-client-side-fragments\u002Findex",1791308075352]