[{"data":1,"prerenderedAt":1211},["ShallowReactive",2],{"i-base:v-logo":3,"i-lucide:user":8,"i-lucide:folder-kanban":11,"i-lucide:heart":13,"i-lucide:file-text":15,"i-lucide:sun":17,"i-lucide:github":19,"i-lucide:twitter":22,"i-lucide:linkedin":24,"i-simple-icons:reddit":26,"i-lucide:globe":28,"article-workers-cache-on-an-authenticated-tile-api":30,"articles-all":1136,"i-lucide:arrow-left":1209},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,512,false,"\u003Cg fill=\"currentColor\">\u003Cg transform=\"translate(0,512) scale(0.1,-0.1)\">\n    \u003Cpath d=\"M2866 3193 c-203 -209 -341 -393 -541 -718 -57 -93 -198 -309 -212 -325 -9 -9 -22 26 -28 75 -4 27 -8 61 -10 75 -2 14 -7 70 -10 125 -21 363 -72 541 -187 656 -30 30 -54 64 -56 78 -7 55 -65 113 -130 130 -21 6 -34 1 -62 -23 -64 -57 -86 -84 -97 -119 -12 -41 -9 -201 7 -349 6 -57 12 -204 12 -327 0 -191 2 -232 18 -270 10 -27 20 -89 24 -148 5 -92 21 -182 84 -478 12 -56 30 -105 46 -129 14 -21 27 -48 27 -60 1 -12 3 -39 4 -61 1 -23 8 -41 16 -44 8 -2 39 -31 69 -63 66 -72 98 -90 159 -91 133 -1 245 118 254 271 4 67 7 74 45 114 22 24 50 55 61 70 33 42 81 138 81 162 0 12 21 44 50 74 27 28 63 80 80 114 57 114 131 230 244 378 48 64 213 266 249 305 78 85 167 207 167 230 0 14 11 47 23 73 34 67 29 133 -20 252 l-38 95 -50 6 c-28 3 -67 13 -87 22 -21 10 -47 17 -58 17 -13 0 -62 -43 -134 -117z\"\u002F>\n    \u003Cpath d=\"M3070 1649 c-30 -44 -76 -96 -102 -115 -27 -20 -48 -40 -48 -45 0 -5 8 -21 17 -36 17 -25 17 -27 -5 -41 l-22 -14 30 -35 c17 -18 30 -39 30 -47 0 -36 34 -118 58 -140 102 -92 120 -104 159 -100 108 10 153 24 153 49 0 7 27 28 59 45 62 33 92 74 90 121 -1 13 10 57 25 97 32 87 33 90 17 165 -15 70 -24 75 -177 111 -159 38 -194 48 -200 57 -12 21 -32 4 -84 -72z\"\u002F>\n  \u003C\u002Fg>\u003C\u002Fg>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":10},24,"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"\u002F>\u003Ccircle cx=\"12\" cy=\"7\" r=\"4\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":12},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2m4-10v4m4-4v2m4-2v6\"\u002F>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":14},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M2 9.5a5.5 5.5 0 0 1 9.591-3.676a.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5\"\u002F>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":16},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\"\u002F>\u003Cpath d=\"M14 2v5a1 1 0 0 0 1 1h5M10 9H8m8 4H8m8 4H8\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":18},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"4\"\u002F>\u003Cpath d=\"M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":20,"hidden":21},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5c.08-1.25-.27-2.48-1-3.5c.28-1.15.28-2.35 0-3.5c0 0-1 0-3 1.5c-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 4 9c0 3.5 3 5.5 6 5.5c-.39.49-.68 1.05-.85 1.65S8.93 17.38 9 18v4\"\u002F>\u003Cpath d=\"M9 18c-4.51 2-5-2-7-2\"\u002F>\u003C\u002Fg>",true,{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":23,"hidden":21},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6c2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4c-.9-4.2 4-6.6 7-3.8c1.1 0 3-1.2 3-1.2\"\u002F>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":25,"hidden":21},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2a2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6M2 9h4v12H2z\"\u002F>\u003Ccircle cx=\"4\" cy=\"4\" r=\"2\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":27},"\u003Cpath fill=\"currentColor\" d=\"M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286A.72.72 0 0 0 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0m4.388 3.199a1.999 1.999 0 1 1-1.947 2.46v.002a2.37 2.37 0 0 0-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363a2.802 2.802 0 1 1 2.908 4.753c-.088 3.256-3.637 5.876-7.997 5.876c-4.361 0-7.905-2.617-7.998-5.87a2.8 2.8 0 0 1 1.189-5.34c.645 0 1.239.218 1.712.585c1.275-.79 2.881-1.291 4.64-1.365v-.01a3.23 3.23 0 0 1 2.88-3.207a2 2 0 0 1 1.959-1.595m-8.085 8.376c-.784 0-1.459.78-1.506 1.797s.64 1.429 1.426 1.429s1.371-.369 1.418-1.385s-.553-1.841-1.338-1.841m7.406 0c-.786 0-1.385.824-1.338 1.841s.634 1.385 1.418 1.385c.785 0 1.473-.413 1.426-1.429c-.046-1.017-.721-1.797-1.506-1.797m-3.703 4.013c-.974 0-1.907.048-2.77.135a.222.222 0 0 0-.183.305a3.2 3.2 0 0 0 2.953 1.964a3.2 3.2 0 0 0 2.953-1.964a.222.222 0 0 0-.184-.305a28 28 0 0 0-2.769-.135\"\u002F>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":29},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"M12 2a14.5 14.5 0 0 0 0 20a14.5 14.5 0 0 0 0-20M2 12h20\"\u002F>\u003C\u002Fg>",{"id":31,"title":32,"body":33,"category":1120,"date":1121,"description":1122,"extension":1123,"image":1124,"meta":1125,"navigation":21,"path":1126,"seo":1127,"status":1128,"stem":1129,"tags":1130,"updatedAt":1124,"__hash__":1135},"articles\u002Farticles\u002F9.workers-cache-on-an-authenticated-tile-api.md","Workers Cache on an authenticated tile API: the split-entrypoint pattern",{"type":34,"value":35,"toc":1114},"minimark",[36,48,51,56,64,110,117,138,142,149,181,188,295,549,782,793,796,839,867,871,878,888,978,984,988,999,1097,1100,1103,1110],[37,38,39,40,47],"p",{},"Cloudflare ",[41,42,46],"a",{"href":43,"rel":44},"https:\u002F\u002Fblog.cloudflare.com\u002Fworkers-cache\u002F",[45],"nofollow","Workers Cache"," has one property that makes it dangerous for any authenticated API: a cache HIT skips your Worker entirely. No code runs. For a public marketing page that is the whole point. For a usage-metered vector-tile API it is a billing hole — anyone could hammer a cached tile URL forever and you would never count a single request.",[37,49,50],{},"This post is the pattern that gets you both: every request authenticated, quota-checked, and metered, while the expensive part (R2 reads, PMTiles directory traversal, tile extraction) is served from Cloudflare's cache. Real numbers at the end: warm-viewport cache hits went from ~10% to 100%, and median warm tile latency dropped from 874ms to under 100ms.",[52,53,55],"h2",{"id":54},"why-you-cannot-just-enable-the-cache","Why you cannot just enable the cache",[37,57,58,59,63],{},"The naive setup is one block in ",[60,61,62],"code",{},"wrangler.json",":",[65,66,71],"pre",{"className":67,"code":68,"language":69,"meta":70,"style":70},"language-json shiki shiki-themes github-dark github-dark github-light","{\n  \"cache\": { \"enabled\": true }\n}\n","json","",[60,72,73,82,104],{"__ignoreMap":70},[74,75,78],"span",{"class":76,"line":77},"line",1,[74,79,81],{"class":80},"sAvwS","{\n",[74,83,85,89,92,95,98,101],{"class":76,"line":84},2,[74,86,88],{"class":87},"sXWYR","  \"cache\"",[74,90,91],{"class":80},": { ",[74,93,94],{"class":87},"\"enabled\"",[74,96,97],{"class":80},": ",[74,99,100],{"class":87},"true",[74,102,103],{"class":80}," }\n",[74,105,107],{"class":76,"line":106},3,[74,108,109],{"class":80},"}\n",[37,111,112,113,116],{},"With ",[60,114,115],{},"Cache-Control"," headers on your responses, repeated requests now serve from cache without invoking the Worker. Two problems for an authenticated API:",[118,119,120,128],"ol",{},[121,122,123,127],"li",{},[124,125,126],"strong",{},"Cache HITs skip auth and metering."," The Worker never runs, so nothing validates the API key, nothing checks the quota, nothing increments usage. Cached responses become free and unauthenticated.",[121,129,130,133,134,137],{},[124,131,132],{},"The query string is part of the cache key."," Tile URLs carry ",[60,135,136],{},"?key=\u003Capi-key>",", so every customer's identical tile is a separate cache entry. Your hit rate fragments by the number of API keys.",[52,139,141],{"id":140},"the-split-entrypoint-pattern","The split-entrypoint pattern",[37,143,144,145,148],{},"Workers Cache is configurable ",[124,146,147],{},"per entrypoint"," (Wrangler 4.107.0+). That is the escape hatch. You split the Worker in two:",[150,151,152,164],"ul",{},[121,153,154,155,158,159,163],{},"The ",[124,156,157],{},"default entrypoint"," is the gateway: cache ",[160,161,162],"em",{},"disabled",". It runs on every request — auth, origin\u002Fscope checks, quota enforcement, usage metering.",[121,165,166,167,173,174,177,178,180],{},"A named ",[124,168,169,172],{},[60,170,171],{},"Origin"," entrypoint"," is the dumb data reader: cache ",[160,175,176],{},"enabled",". It only fetches tile bytes from R2 and sets ",[60,179,115],{},".",[37,182,183,184,187],{},"The gateway calls the origin over the ",[60,185,186],{},"ctx.exports"," loopback, and the cache sits between them:",[65,189,191],{"className":67,"code":190,"language":69,"meta":70,"style":70},"{\n  \"cache\": { \"enabled\": true },\n  \"exports\": {\n    \"default\": { \"type\": \"worker\", \"cache\": { \"enabled\": false } },\n    \"Origin\": { \"type\": \"worker\", \"cache\": { \"enabled\": true } }\n  }\n}\n",[60,192,193,197,212,220,255,284,290],{"__ignoreMap":70},[74,194,195],{"class":76,"line":77},[74,196,81],{"class":80},[74,198,199,201,203,205,207,209],{"class":76,"line":84},[74,200,88],{"class":87},[74,202,91],{"class":80},[74,204,94],{"class":87},[74,206,97],{"class":80},[74,208,100],{"class":87},[74,210,211],{"class":80}," },\n",[74,213,214,217],{"class":76,"line":106},[74,215,216],{"class":87},"  \"exports\"",[74,218,219],{"class":80},": {\n",[74,221,223,226,228,231,233,237,240,243,245,247,249,252],{"class":76,"line":222},4,[74,224,225],{"class":87},"    \"default\"",[74,227,91],{"class":80},[74,229,230],{"class":87},"\"type\"",[74,232,97],{"class":80},[74,234,236],{"class":235},"s4Y1p","\"worker\"",[74,238,239],{"class":80},", ",[74,241,242],{"class":87},"\"cache\"",[74,244,91],{"class":80},[74,246,94],{"class":87},[74,248,97],{"class":80},[74,250,251],{"class":87},"false",[74,253,254],{"class":80}," } },\n",[74,256,258,261,263,265,267,269,271,273,275,277,279,281],{"class":76,"line":257},5,[74,259,260],{"class":87},"    \"Origin\"",[74,262,91],{"class":80},[74,264,230],{"class":87},[74,266,97],{"class":80},[74,268,236],{"class":235},[74,270,239],{"class":80},[74,272,242],{"class":87},[74,274,91],{"class":80},[74,276,94],{"class":87},[74,278,97],{"class":80},[74,280,100],{"class":87},[74,282,283],{"class":80}," } }\n",[74,285,287],{"class":76,"line":286},6,[74,288,289],{"class":80},"  }\n",[74,291,293],{"class":76,"line":292},7,[74,294,109],{"class":80},[65,296,300],{"className":297,"code":298,"language":299,"meta":70,"style":70},"language-ts shiki shiki-themes github-dark github-dark github-light","import { WorkerEntrypoint } from 'cloudflare:workers';\n\nexport class Origin extends WorkerEntrypoint\u003CEnv> {\n  async fetch(request: Request): Promise\u003CResponse> {\n    const tile = await readTileFromR2(this.env, new URL(request.url).pathname);\n    if (!tile) return new Response(null, { status: 204 });\n    return new Response(tile.data, {\n      headers: {\n        'Content-Type': 'application\u002Fx-protobuf',\n        'Cache-Control': 'public, max-age=604800',\n        'Cache-Tag': `archive:${tile.archive}`,\n      },\n    });\n  }\n}\n","ts",[60,301,302,320,325,352,387,421,458,470,476,490,503,527,533,539,544],{"__ignoreMap":70},[74,303,304,308,311,314,317],{"class":76,"line":77},[74,305,307],{"class":306},"s6ubI","import",[74,309,310],{"class":80}," { WorkerEntrypoint } ",[74,312,313],{"class":306},"from",[74,315,316],{"class":235}," 'cloudflare:workers'",[74,318,319],{"class":80},";\n",[74,321,322],{"class":76,"line":84},[74,323,324],{"emptyLinePlaceholder":21},"\n",[74,326,327,330,333,337,340,343,346,349],{"class":76,"line":106},[74,328,329],{"class":306},"export",[74,331,332],{"class":306}," class",[74,334,336],{"class":335},"s5n6i"," Origin",[74,338,339],{"class":306}," extends",[74,341,342],{"class":335}," WorkerEntrypoint",[74,344,345],{"class":80},"\u003C",[74,347,348],{"class":335},"Env",[74,350,351],{"class":80},"> {\n",[74,353,354,357,360,363,367,369,372,375,377,380,382,385],{"class":76,"line":222},[74,355,356],{"class":306},"  async",[74,358,359],{"class":335}," fetch",[74,361,362],{"class":80},"(",[74,364,366],{"class":365},"sAzo5","request",[74,368,63],{"class":306},[74,370,371],{"class":335}," Request",[74,373,374],{"class":80},")",[74,376,63],{"class":306},[74,378,379],{"class":335}," Promise",[74,381,345],{"class":80},[74,383,384],{"class":335},"Response",[74,386,351],{"class":80},[74,388,389,392,395,398,401,404,406,409,412,415,418],{"class":76,"line":257},[74,390,391],{"class":306},"    const",[74,393,394],{"class":87}," tile",[74,396,397],{"class":306}," =",[74,399,400],{"class":306}," await",[74,402,403],{"class":335}," readTileFromR2",[74,405,362],{"class":80},[74,407,408],{"class":87},"this",[74,410,411],{"class":80},".env, ",[74,413,414],{"class":306},"new",[74,416,417],{"class":335}," URL",[74,419,420],{"class":80},"(request.url).pathname);\n",[74,422,423,426,429,432,435,438,441,444,446,449,452,455],{"class":76,"line":286},[74,424,425],{"class":306},"    if",[74,427,428],{"class":80}," (",[74,430,431],{"class":306},"!",[74,433,434],{"class":80},"tile) ",[74,436,437],{"class":306},"return",[74,439,440],{"class":306}," new",[74,442,443],{"class":335}," Response",[74,445,362],{"class":80},[74,447,448],{"class":87},"null",[74,450,451],{"class":80},", { status: ",[74,453,454],{"class":87},"204",[74,456,457],{"class":80}," });\n",[74,459,460,463,465,467],{"class":76,"line":292},[74,461,462],{"class":306},"    return",[74,464,440],{"class":306},[74,466,443],{"class":335},[74,468,469],{"class":80},"(tile.data, {\n",[74,471,473],{"class":76,"line":472},8,[74,474,475],{"class":80},"      headers: {\n",[74,477,479,482,484,487],{"class":76,"line":478},9,[74,480,481],{"class":235},"        'Content-Type'",[74,483,97],{"class":80},[74,485,486],{"class":235},"'application\u002Fx-protobuf'",[74,488,489],{"class":80},",\n",[74,491,493,496,498,501],{"class":76,"line":492},10,[74,494,495],{"class":235},"        'Cache-Control'",[74,497,97],{"class":80},[74,499,500],{"class":235},"'public, max-age=604800'",[74,502,489],{"class":80},[74,504,506,509,511,514,517,519,522,525],{"class":76,"line":505},11,[74,507,508],{"class":235},"        'Cache-Tag'",[74,510,97],{"class":80},[74,512,513],{"class":235},"`archive:${",[74,515,516],{"class":80},"tile",[74,518,180],{"class":235},[74,520,521],{"class":80},"archive",[74,523,524],{"class":235},"}`",[74,526,489],{"class":80},[74,528,530],{"class":76,"line":529},12,[74,531,532],{"class":80},"      },\n",[74,534,536],{"class":76,"line":535},13,[74,537,538],{"class":80},"    });\n",[74,540,542],{"class":76,"line":541},14,[74,543,289],{"class":80},[74,545,547],{"class":76,"line":546},15,[74,548,109],{"class":80},[65,550,552],{"className":297,"code":551,"language":299,"meta":70,"style":70},"\u002F\u002F Default entrypoint — runs on EVERY request, HIT or MISS\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext) {\n    const token = await validateApiKey(request, env); \u002F\u002F 401\u002F403 on failure\n    await enforceQuota(env, token); \u002F\u002F 429 when exhausted\n\n    \u002F\u002F Strip ?key= so every customer shares one cache entry per tile\n    const clean = new URL(request.url);\n    clean.search = '';\n\n    const origin = await ctx.exports.Origin.fetch(new Request(clean));\n    const hit = origin.headers.get('cf-cache-status') === 'HIT';\n\n    recordUsage(env, ctx, token, hit); \u002F\u002F metered on HIT and MISS alike\n\n    return withCorsAndCacheStatus(origin, hit);\n  },\n};\n",[60,553,554,560,570,607,627,641,645,650,666,679,683,709,740,744,755,759,770,776],{"__ignoreMap":70},[74,555,556],{"class":76,"line":77},[74,557,559],{"class":558},"sxXm1","\u002F\u002F Default entrypoint — runs on EVERY request, HIT or MISS\n",[74,561,562,564,567],{"class":76,"line":84},[74,563,329],{"class":306},[74,565,566],{"class":306}," default",[74,568,569],{"class":80}," {\n",[74,571,572,574,576,578,580,582,584,586,589,591,594,596,599,601,604],{"class":76,"line":106},[74,573,356],{"class":306},[74,575,359],{"class":335},[74,577,362],{"class":80},[74,579,366],{"class":365},[74,581,63],{"class":306},[74,583,371],{"class":335},[74,585,239],{"class":80},[74,587,588],{"class":365},"env",[74,590,63],{"class":306},[74,592,593],{"class":335}," Env",[74,595,239],{"class":80},[74,597,598],{"class":365},"ctx",[74,600,63],{"class":306},[74,602,603],{"class":335}," ExecutionContext",[74,605,606],{"class":80},") {\n",[74,608,609,611,614,616,618,621,624],{"class":76,"line":222},[74,610,391],{"class":306},[74,612,613],{"class":87}," token",[74,615,397],{"class":306},[74,617,400],{"class":306},[74,619,620],{"class":335}," validateApiKey",[74,622,623],{"class":80},"(request, env); ",[74,625,626],{"class":558},"\u002F\u002F 401\u002F403 on failure\n",[74,628,629,632,635,638],{"class":76,"line":257},[74,630,631],{"class":306},"    await",[74,633,634],{"class":335}," enforceQuota",[74,636,637],{"class":80},"(env, token); ",[74,639,640],{"class":558},"\u002F\u002F 429 when exhausted\n",[74,642,643],{"class":76,"line":286},[74,644,324],{"emptyLinePlaceholder":21},[74,646,647],{"class":76,"line":292},[74,648,649],{"class":558},"    \u002F\u002F Strip ?key= so every customer shares one cache entry per tile\n",[74,651,652,654,657,659,661,663],{"class":76,"line":472},[74,653,391],{"class":306},[74,655,656],{"class":87}," clean",[74,658,397],{"class":306},[74,660,440],{"class":306},[74,662,417],{"class":335},[74,664,665],{"class":80},"(request.url);\n",[74,667,668,671,674,677],{"class":76,"line":478},[74,669,670],{"class":80},"    clean.search ",[74,672,673],{"class":306},"=",[74,675,676],{"class":235}," ''",[74,678,319],{"class":80},[74,680,681],{"class":76,"line":492},[74,682,324],{"emptyLinePlaceholder":21},[74,684,685,687,690,692,694,697,700,702,704,706],{"class":76,"line":505},[74,686,391],{"class":306},[74,688,689],{"class":87}," origin",[74,691,397],{"class":306},[74,693,400],{"class":306},[74,695,696],{"class":80}," ctx.exports.Origin.",[74,698,699],{"class":335},"fetch",[74,701,362],{"class":80},[74,703,414],{"class":306},[74,705,371],{"class":335},[74,707,708],{"class":80},"(clean));\n",[74,710,711,713,716,718,721,724,726,729,732,735,738],{"class":76,"line":529},[74,712,391],{"class":306},[74,714,715],{"class":87}," hit",[74,717,397],{"class":306},[74,719,720],{"class":80}," origin.headers.",[74,722,723],{"class":335},"get",[74,725,362],{"class":80},[74,727,728],{"class":235},"'cf-cache-status'",[74,730,731],{"class":80},") ",[74,733,734],{"class":306},"===",[74,736,737],{"class":235}," 'HIT'",[74,739,319],{"class":80},[74,741,742],{"class":76,"line":535},[74,743,324],{"emptyLinePlaceholder":21},[74,745,746,749,752],{"class":76,"line":541},[74,747,748],{"class":335},"    recordUsage",[74,750,751],{"class":80},"(env, ctx, token, hit); ",[74,753,754],{"class":558},"\u002F\u002F metered on HIT and MISS alike\n",[74,756,757],{"class":76,"line":546},[74,758,324],{"emptyLinePlaceholder":21},[74,760,762,764,767],{"class":76,"line":761},16,[74,763,462],{"class":306},[74,765,766],{"class":335}," withCorsAndCacheStatus",[74,768,769],{"class":80},"(origin, hit);\n",[74,771,773],{"class":76,"line":772},17,[74,774,775],{"class":80},"  },\n",[74,777,779],{"class":76,"line":778},18,[74,780,781],{"class":80},"};\n",[37,783,784,785,788,789,792],{},"The invariant this buys you: a client hammering the same cached tile a million times causes a million auth checks, a million quota checks, and a million usage increments — and roughly ",[124,786,787],{},"one"," R2 read. Cache hits stay billable. The ",[60,790,791],{},"cf-cache-status"," header on the loopback response even tells the gateway whether it was a HIT, so your analytics can track hit rate per key.",[37,794,795],{},"Three details worth stealing:",[150,797,798,811,829],{},[121,799,800,806,807,810],{},[124,801,802,805],{},[60,803,804],{},"Cache-Tag"," on origin responses"," enables ",[60,808,809],{},"ctx.cache.purge({ tags: ['archive:planet'] })"," — re-upload a tile archive, purge only its tiles.",[121,812,813,816,817,820,821,824,825,828],{},[124,814,815],{},"Range requests are handled for you."," Workers Cache strips ",[60,818,819],{},"Range"," before invoking the entrypoint, caches the full body, and slices the ",[60,822,823],{},"206"," itself. Your origin should return plain ",[60,826,827],{},"200","s and never implement range logic.",[121,830,831,838],{},[124,832,833,834,837],{},"Quota rejection happens ",[160,835,836],{},"before"," the loopback",", so over-quota traffic never even consults the cache.",[37,840,841,842,845,846,849,850,853,854,858,859,862,863,866],{},"If you deploy through Nuxt\u002FNitro rather than hand-written Workers, the ",[60,843,844],{},"cache"," and ",[60,847,848],{},"exports"," blocks pass straight through ",[60,851,852],{},"nitro.cloudflare.wrangler"," — I covered that passthrough in ",[41,855,857],{"href":856},"\u002Farticles\u002Ftil-workers-cache-nitro-passthrough","an earlier TIL",". The extra step for Nitro is wrapping the generated entry so the gateway logic owns ",[60,860,861],{},"\u002Fv1\u002F*"," routes at the top level; a small build module that rewrites Nitro's virtual ",[60,864,865],{},"cloudflare-server-entry"," does it in ~90 lines.",[52,868,870],{"id":869},"the-measurement-trap","The measurement trap",[37,872,873,874,877],{},"My first deployment of this pattern looked broken: a ",[60,875,876],{},"console.log"," inside the cached entrypoint fired on every request, so I concluded the cache never engaged and spent days chasing framework bugs that did not exist.",[37,879,880,881,884,885,887],{},"Do not measure a cached entrypoint by whether its logs fire — background ",[60,882,883],{},"waitUntil()"," work and launch-week rollout noise both poison that signal. The reliable probe is to call the cached entrypoint twice with the same URL from inside the Worker and read ",[60,886,791],{}," off the loopback responses:",[65,889,891],{"className":297,"code":890,"language":299,"meta":70,"style":70},"const r1 = await ctx.exports.Origin.fetch(new Request(url));\nawait new Promise((r) => setTimeout(r, 1500));\nconst r2 = await ctx.exports.Origin.fetch(new Request(url));\n\u002F\u002F healthy: r1 MISS, r2 HIT, and Cache-Tag stripped from r2\n",[60,892,893,918,950,973],{"__ignoreMap":70},[74,894,895,898,901,903,905,907,909,911,913,915],{"class":76,"line":77},[74,896,897],{"class":306},"const",[74,899,900],{"class":87}," r1",[74,902,397],{"class":306},[74,904,400],{"class":306},[74,906,696],{"class":80},[74,908,699],{"class":335},[74,910,362],{"class":80},[74,912,414],{"class":306},[74,914,371],{"class":335},[74,916,917],{"class":80},"(url));\n",[74,919,920,923,925,927,930,933,935,938,941,944,947],{"class":76,"line":84},[74,921,922],{"class":306},"await",[74,924,440],{"class":306},[74,926,379],{"class":87},[74,928,929],{"class":80},"((",[74,931,932],{"class":365},"r",[74,934,731],{"class":80},[74,936,937],{"class":306},"=>",[74,939,940],{"class":335}," setTimeout",[74,942,943],{"class":80},"(r, ",[74,945,946],{"class":87},"1500",[74,948,949],{"class":80},"));\n",[74,951,952,954,957,959,961,963,965,967,969,971],{"class":76,"line":106},[74,953,897],{"class":306},[74,955,956],{"class":87}," r2",[74,958,397],{"class":306},[74,960,400],{"class":306},[74,962,696],{"class":80},[74,964,699],{"class":335},[74,966,362],{"class":80},[74,968,414],{"class":306},[74,970,371],{"class":335},[74,972,917],{"class":80},[74,974,975],{"class":76,"line":222},[74,976,977],{"class":558},"\u002F\u002F healthy: r1 MISS, r2 HIT, and Cache-Tag stripped from r2\n",[37,979,980,981,983],{},"If ",[60,982,804],{}," leaks through on the second response, the response never transited the cache — that header is stripped by the cache layer on the way out.",[52,985,987],{"id":986},"the-numbers","The numbers",[37,989,990,991,994,995,998],{},"Methodology: identical k6 suites (single-user smoke; 10 VU × 30s baseline; a 20-tile z10 viewport fetched with 20-way parallelism, twice) against the same PMTiles archive on R2. BEFORE is the old hand-rolled ",[60,992,993],{},"caches.default"," implementation, which had no request collapsing and racy async cache writes. AFTER is the split-entrypoint Workers Cache deployment — measured on a ",[124,996,997],{},"freshly deployed Worker with a fully cold cache",", which if anything handicaps the AFTER numbers.",[1000,1001,1002,1020],"table",{},[1003,1004,1005],"thead",{},[1006,1007,1008,1012,1017],"tr",{},[1009,1010,1011],"th",{},"Metric",[1009,1013,1014,1015,374],{},"Before (",[60,1016,993],{},[1009,1018,1019],{},"After (Workers Cache)",[1021,1022,1023,1037,1050,1063,1076,1087],"tbody",{},[1006,1024,1025,1029,1032],{},[1026,1027,1028],"td",{},"Warm 20-tile viewport hit rate",[1026,1030,1031],{},"~10% (18\u002F20 re-MISSED)",[1026,1033,1034],{},[124,1035,1036],{},"100% (20\u002F20 HIT)",[1006,1038,1039,1042,1045],{},[1026,1040,1041],{},"Warm viewport avg tile latency",[1026,1043,1044],{},"re-fetches at ~750ms",[1026,1046,1047],{},[124,1048,1049],{},"70ms",[1006,1051,1052,1055,1058],{},[1026,1053,1054],{},"k6 smoke median (single user, warm)",[1026,1056,1057],{},"874ms",[1026,1059,1060],{},[124,1061,1062],{},"~98ms",[1006,1064,1065,1068,1071],{},[1026,1066,1067],{},"k6 baseline median (10 VU \u002F 30s)",[1026,1069,1070],{},"686ms",[1026,1072,1073],{},[124,1074,1075],{},"377ms",[1006,1077,1078,1081,1084],{},[1026,1079,1080],{},"k6 baseline p95",[1026,1082,1083],{},"45.7s",[1026,1085,1086],{},"12.1s",[1006,1088,1089,1092,1095],{},[1026,1090,1091],{},"Errors under load",[1026,1093,1094],{},"0%",[1026,1096,1094],{},[37,1098,1099],{},"The headline is the viewport row. A map view loads 16–24 tiles at once; the old implementation re-missed 18 of 20 tiles three seconds after first load because parallel requests raced past each other into R2 and the async cache writes mostly had not landed. Workers Cache does its own request collapsing, so the second viewport load is 20 HITs at a median of 70ms — the difference between a map that stutters on every pan and one that feels instant.",[37,1101,1102],{},"The p95 numbers stay ugly in both columns for the same reason: a cold z10 tile over a 232 GiB planet archive is a genuine R2 range-read plus PMTiles directory walk, and no request-path cache fixes a cold miss. That is what tiered caching upstream and archive-level prewarming are for — a different post.",[37,1104,1105,1106,1109],{},"Median warm-tile latency dropped roughly ",[124,1107,1108],{},"9×"," (874ms → 98ms) and warm-viewport hit rate went from 10% to 100%, while every single request — hit or miss — still passed auth, quota, and usage metering. That is the whole trick: cache the bytes, never the gate.",[1111,1112,1113],"style",{},"html pre.shiki code .sAvwS, html code.shiki .sAvwS{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8;--shiki-light:#24292E}html pre.shiki code .sXWYR, html code.shiki .sXWYR{--shiki-default:#79B8FF;--shiki-dark:#79B8FF;--shiki-light:#005CC5}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);}html pre.shiki code .s4Y1p, html code.shiki .s4Y1p{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF;--shiki-light:#032F62}html pre.shiki code .s6ubI, html code.shiki .s6ubI{--shiki-default:#F97583;--shiki-dark:#F97583;--shiki-light:#D73A49}html pre.shiki code .s5n6i, html code.shiki .s5n6i{--shiki-default:#B392F0;--shiki-dark:#B392F0;--shiki-light:#6F42C1}html pre.shiki code .sAzo5, html code.shiki .sAzo5{--shiki-default:#FFAB70;--shiki-dark:#FFAB70;--shiki-light:#E36209}html pre.shiki code .sxXm1, html code.shiki .sxXm1{--shiki-default:#6A737D;--shiki-dark:#6A737D;--shiki-light:#6A737D}",{"title":70,"searchDepth":84,"depth":84,"links":1115},[1116,1117,1118,1119],{"id":54,"depth":84,"text":55},{"id":140,"depth":84,"text":141},{"id":869,"depth":84,"text":870},{"id":986,"depth":84,"text":987},"cloudflare","2026-07-13T23:45:00.000+05:30","How to enable Cloudflare Workers Cache on an API that must authenticate and meter every request, using an uncached gateway entrypoint in front of a cached origin entrypoint. Warm viewport hit rate went from 10% to 100% and median warm tile latency dropped from 874ms to under 100ms.","md",null,{"position":478},"\u002Farticles\u002Fworkers-cache-on-an-authenticated-tile-api",{"title":32,"description":1122},"published","articles\u002F9.workers-cache-on-an-authenticated-tile-api",[1120,1131,844,1132,1133,1134],"workers","pmtiles","nitro","gis","MI53rrc3F8u7j1iF085m46cN1AjKwS9MczWS74MNA_E",[1137,1139,1150,1162,1171,1179,1186,1195,1200],{"path":1126,"title":32,"description":1122,"date":1121,"updatedAt":1124,"category":1120,"tags":1138,"image":1124,"status":1128},[1120,1131,844,1132,1133,1134],{"path":1140,"title":1141,"description":1142,"date":1143,"updatedAt":1124,"category":1134,"tags":1144,"image":1124,"status":1128},"\u002Farticles\u002Fencoding-elevation-into-pixels","Encoding elevation into pixels: DEM terrain tiles in Rust","How tileserver-rs turns raw GDAL elevation rasters into Terrarium and Mapbox-RGB terrain tiles that MapLibre renders as 3D terrain. The math, the classic 1-LSB encoding bug, and why the alpha channel will betray you.","2026-07-12T11:30:00.000+05:30",[1145,1134,1146,1147,1148,1149],"rust","maplibre","dem","terrain","gdal",{"path":1151,"title":1152,"description":1153,"date":1154,"updatedAt":1124,"category":1120,"tags":1155,"image":1124,"status":1128},"\u002Farticles\u002Fcutting-a-supabase-bill-to-zero-with-cloudflare-d1","How I cut a $35 Supabase bill to $0 with Cloudflare D1","Moving a live multi-tenant SaaS off Supabase Postgres onto Cloudflare D1 with zero downtime. The schema rewrite was the easy part. The date-coercion bug, the PostGIS audit, and the break-even math were not.","2026-07-07T18:30:00.000+05:30",[1120,1156,1157,1158,1159,1160,1161],"d1","supabase","sqlite","drizzle","nuxt","migration",{"path":1163,"title":1164,"description":1165,"date":1166,"updatedAt":1124,"category":1160,"tags":1167,"image":1124,"status":1128},"\u002Farticles\u002Ftil-shiki-multitheme-code-block-background","TIL: Shiki multi-theme highlighting does not switch the code-block background","Shiki multi-theme mode swaps token colours per colour scheme but leaves the pre background variable empty, so light-theme text can land on a dark block. You have to theme the container yourself.","2026-07-07T02:30:00.000+05:30",[1160,1168,1169,1170],"shiki","nuxt-content","dark-mode",{"path":1172,"title":1173,"description":1174,"date":1175,"updatedAt":1124,"category":1120,"tags":1176,"image":1124,"status":1128},"\u002Farticles\u002Fdynamic-og-images-on-cloudflare-workers","Generating dynamic Open Graph images on Cloudflare Workers","Rendering per-page and per-article social cards at the edge with Satori and @cf-wasm\u002Fog: how the route works, what Satori refuses to render, and the cold-versus-warm numbers.","2026-07-06T22:00:00.000+05:30",[1120,1131,1177,1178,1160],"open-graph","satori",{"path":1180,"title":1181,"description":1182,"date":1183,"updatedAt":1124,"category":1120,"tags":1184,"image":1124,"status":1128},"\u002Farticles\u002Fmigrating-a-nuxt-site-from-cloudflare-pages-to-workers","Migrating a Nuxt site from Cloudflare Pages to Workers","The full path from the cloudflare-pages preset to a cloudflare_module Worker: the config changes, the custom-domain cutover that no deploy step handles for you, and the gotchas that only show up in CI.","2026-07-06T21:00:00.000+05:30",[1120,1131,1160,1133,1185],"devops",{"path":1187,"title":1188,"description":1189,"date":1190,"updatedAt":1124,"category":1191,"tags":1192,"image":1124,"status":1128},"\u002Farticles\u002Ftil-tailwind-v4-webkit-mask-prefix","TIL: Tailwind v4 mask utilities need the webkit prefix in plain CSS","Porting a Tailwind mask-composite utility to scoped CSS quietly breaks in Chrome and Safari unless you also write the -webkit-mask-* properties, because Tailwind was emitting them for you.","2026-07-06T20:15:00.000+05:30","css",[1193,1191,1194],"tailwind","webkit",{"path":856,"title":1196,"description":1197,"date":1198,"updatedAt":1124,"category":1120,"tags":1199,"image":1124,"status":1128},"TIL: Enabling Workers Cache through Nitro without a native option","Nitro forwards arbitrary keys into the generated wrangler config, so you can turn on Cloudflare Workers Cache with a single line even though there is no dedicated Nitro option for it.","2026-07-06T14:30:00.000+05:30",[1120,1131,1133,1160],{"path":1201,"title":1202,"description":1203,"date":1204,"updatedAt":1124,"category":1205,"tags":1206,"image":1124,"status":1128},"\u002Farticles\u002Fbuild-and-publish-vue-3-components-using-rollup","Build & Publish Vue 3 Components using Rollup.js","Let's build a reusable Vue 3 component using rollup","2022-07-04T16:20:09.000+05:30","vue",[1207,1208],"vue 3","rollup",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":1210},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m12 19l-7-7l7-7m7 7H5\"\u002F>",1784010409894]