[{"data":1,"prerenderedAt":457},["ShallowReactive",2],{"navigation":3,"/blog/journey-github-streak-caching":154,"surround-journey-github-streak-caching":444},[4],{"title":5,"path":6,"stem":7,"children":8,"page":153},"Blog","/blog","blog",[9,13,17,21,25,29,33,37,41,45,49,53,57,61,65,69,73,77,81,85,89,93,97,101,105,109,113,117,121,125,129,133,137,141,145,149],{"title":10,"path":11,"stem":12},"How to Increase Free Tier Memory on AWS EC2","/blog/aws-ec2-ram","blog/aws-ec2-ram",{"title":14,"path":15,"stem":16},"Blooper: The Disappearing Cloudflare Variables","/blog/blooper-cloudflare-env-vars","blog/blooper-cloudflare-env-vars",{"title":18,"path":19,"stem":20},"Blooper: Hardcoded Content Links cause 404s","/blog/blooper-hardcoded-nuxt-content-links","blog/blooper-hardcoded-nuxt-content-links",{"title":22,"path":23,"stem":24},"Blooper: The Invisible SEO","/blog/blooper-nuxt-seo-env","blog/blooper-nuxt-seo-env",{"title":26,"path":27,"stem":28},"Blooper: The SSR Crash of Doom","/blog/blooper-ssr-feedback-form","blog/blooper-ssr-feedback-form",{"title":30,"path":31,"stem":32},"Blooper: The YouTube Crawl Budget Killer","/blog/blooper-video-crawl-budget","blog/blooper-video-crawl-budget",{"title":34,"path":35,"stem":36},"Blooper: PWA Redirecting SPA Routes","/blog/blooper-vite-pwa-ssr-redirects","blog/blooper-vite-pwa-ssr-redirects",{"title":38,"path":39,"stem":40},"Git Branching in 2026: Code State, Environment Isolation, and Modern CD","/blog/branch-theater-staging-branches","blog/branch-theater-staging-branches",{"title":42,"path":43,"stem":44},"The Developer's Guide to Avoiding Cloud Billing Traps in 2026","/blog/cloud-billing-traps-2026","blog/cloud-billing-traps-2026",{"title":46,"path":47,"stem":48},"Disk Management in Linux","/blog/disk-management-in-linux","blog/disk-management-in-linux",{"title":50,"path":51,"stem":52},"Docker Devtools Stage | Powerlevel, OhMyZSH, tmux, fuzzy finder, autocomplete etc","/blog/docker-devtools","blog/docker-devtools",{"title":54,"path":55,"stem":56},"Docker container | Resolving Permission Denied Woes: A Decade-Long Struggle Unraveled","/blog/docker-non-root-user-perms","blog/docker-non-root-user-perms",{"title":58,"path":59,"stem":60},"The Ultimate Guide to Free Tier Hosting Platforms (Serverless & Containers)","/blog/free-hosting-platforms-guide","blog/free-hosting-platforms-guide",{"title":62,"path":63,"stem":64},"Git commands | underrated","/blog/git-commands-underrated","blog/git-commands-underrated",{"title":66,"path":67,"stem":68},"Journey: Taming Edge Caching and KV Storage","/blog/journey-github-streak-caching","blog/journey-github-streak-caching",{"title":70,"path":71,"stem":72},"Kubernetes Cluster Setup Guide","/blog/k8s-setup","blog/k8s-setup",{"title":74,"path":75,"stem":76},"Linux WiFi Drivers","/blog/linux-wifi-drivers","blog/linux-wifi-drivers",{"title":78,"path":79,"stem":80},"4 Common Routing Traps When Migrating to Nuxt i18n","/blog/nuxt-i18n-routing","blog/nuxt-i18n-routing",{"title":82,"path":83,"stem":84},"Patch the CVE, Don't Trigger an Accidental Refactor: Surviving PHP2Shell on Docker","/blog/patching-php2shell-docker-playbook","blog/patching-php2shell-docker-playbook",{"title":86,"path":87,"stem":88},"pfSense basic firewall setup","/blog/pfsense-basic-firewall-setup","blog/pfsense-basic-firewall-setup",{"title":90,"path":91,"stem":92},"pfSense Quickstart","/blog/pfsense-quickstart","blog/pfsense-quickstart",{"title":94,"path":95,"stem":96},"Postmortem: Cloudflare Worker Missing Dependency on Deploy","/blog/postmortem-cloudflare-worker-dependency-deploy","blog/postmortem-cloudflare-worker-dependency-deploy",{"title":98,"path":99,"stem":100},"Postmortem: Serverless HTTP Driver Transaction Failures","/blog/postmortem-neon-http-transactions","blog/postmortem-neon-http-transactions",{"title":102,"path":103,"stem":104},"Postmortem: The Ghost in the Chrome Cache (Nuxt & Cloudflare Pages)","/blog/postmortem-nuxt-cloudflare-chunk-error","blog/postmortem-nuxt-cloudflare-chunk-error",{"title":106,"path":107,"stem":108},"Postmortem: Nuxt Content 404s on Staging Deployments","/blog/postmortem-nuxt-content-404-staging","blog/postmortem-nuxt-content-404-staging",{"title":110,"path":111,"stem":112},"Postmortem: 500 Internal Server Error on ISR Routes","/blog/postmortem-nuxt-content-payload-extraction","blog/postmortem-nuxt-content-payload-extraction",{"title":114,"path":115,"stem":116},"Post-Mortem: Intrusive Database Driver Refactoring","/blog/postmortem-proxy-re-architecting","blog/postmortem-proxy-re-architecting",{"title":118,"path":119,"stem":120},"Post-Mortem: Cloudflare Worker Connection Hang & Resilient DB Fallbacks","/blog/postmortem-worker-db-connection-hang","blog/postmortem-worker-db-connection-hang",{"title":122,"path":123,"stem":124},"Pro Tip: Sandboxing Cloudflare Workers for AI Agents","/blog/pro-tip-wrangler-local-dev-trick","blog/pro-tip-wrangler-local-dev-trick",{"title":126,"path":127,"stem":128},"Proxmox DNS Management","/blog/proxmox-dns","blog/proxmox-dns",{"title":130,"path":131,"stem":132},"Proxmox Recovery","/blog/proxmox-recovery","blog/proxmox-recovery",{"title":134,"path":135,"stem":136},"Proxmox WiFi Connection Story","/blog/proxmox-wifi","blog/proxmox-wifi",{"title":138,"path":139,"stem":140},"roadauth-rails api jwt cors","/blog/rodauth-jwt","blog/rodauth-jwt",{"title":142,"path":143,"stem":144},"SOCKS5 Proxy Quickstart","/blog/sock5-proxy","blog/sock5-proxy",{"title":146,"path":147,"stem":148},"tmux status bar toggle","/blog/tmux-status","blog/tmux-status",{"title":150,"path":151,"stem":152},"UFW - Cheatsheet","/blog/ufw-cheatsheet","blog/ufw-cheatsheet",false,{"id":155,"title":66,"authors":156,"badge":162,"body":164,"date":433,"description":434,"extension":435,"image":436,"imageComponent":437,"meta":438,"navigation":439,"path":67,"pinned":153,"seo":440,"seoImage":441,"stem":68,"__hash__":443},"posts/blog/journey-github-streak-caching.md",[157],{"name":158,"to":159,"avatar":160},"Rahul Dhole","/",{"src":161},"/profile.png",{"label":163},"Journey",{"type":165,"value":166,"toc":426},"minimark",[167,172,187,195,209,213,221,226,334,339,342,395,399,402,416],[168,169,171],"h2",{"id":170},"_1-the-challenge","1. The Challenge",[173,174,175,176,186],"p",{},"When building the ",[177,178,182],"a",{"href":179,"rel":180},"https://github.com/rahuldhole/github-streak",[181],"nofollow",[183,184,185],"code",{},"🔥 github-streak"," project, one of the immediate hurdles I faced was performance and rate-limiting. Rendering GitHub streak SVGs requires making expensive calls to the GitHub API. Without aggressive caching, I risked quickly burning through my API rate limits and serving slow responses to end-users.",[173,188,189,190,194],{},"Here is the high-level request flow (",[177,191,193],{"href":192},"#fig-1","Figure 1",") that highlighted the need for multiple layers of caching:",[173,196,197,200,205],{},[177,198],{"id":199},"fig-1",[201,202],"mermaid",{":config":203,"code":204},"config","sequenceDiagram%0A%20%20%20%20participant%20User%0A%20%20%20%20participant%20CDN%20as%20Edge%20CDN%0A%20%20%20%20participant%20Edge%20as%20Edge%20Server%0A%20%20%20%20participant%20KV%20as%20KV%20Storage%0A%20%20%20%20participant%20GitHub%20as%20GitHub%20API%0A%0A%20%20%20%20User-%3E%3ECDN%3A%20Request%20SVG%20Image%0A%20%20%20%20alt%20CDN%20Hit%0A%20%20%20%20%20%20%20%20CDN--%3E%3EUser%3A%20Return%20Cached%20SVG%0A%20%20%20%20else%20CDN%20Miss%20%2F%20SWR%0A%20%20%20%20%20%20%20%20CDN-%3E%3EEdge%3A%20Execute%20Render%20Logic%0A%20%20%20%20%20%20%20%20Edge-%3E%3EKV%3A%20Check%20historical%20data%0A%20%20%20%20%20%20%20%20alt%20KV%20Hit%0A%20%20%20%20%20%20%20%20%20%20%20%20KV--%3E%3EEdge%3A%20Return%206%2B%20months%20data%0A%20%20%20%20%20%20%20%20%20%20%20%20Edge-%3E%3EKV%3A%20Check%20recent%20data%20staleness%0A%20%20%20%20%20%20%20%20%20%20%20%20alt%20Cache%20%3E%2060%20mins%20(Slow%20Lane)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Edge-)GitHub%3A%20Background%20Fetch%20(Primary%20Token)%0A%20%20%20%20%20%20%20%20%20%20%20%20else%20Cache%20%3E%205%20mins%20(Fast%20Lane)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20Edge-)GitHub%3A%20Background%20Fetch%20(Secondary%20Token)%0A%20%20%20%20%20%20%20%20%20%20%20%20end%0A%20%20%20%20%20%20%20%20else%20KV%20Miss%0A%20%20%20%20%20%20%20%20%20%20%20%20Edge-%3E%3EGitHub%3A%20Sync%20Fetch%20ALL%20data%20(Primary%20Token)%0A%20%20%20%20%20%20%20%20%20%20%20%20GitHub--%3E%3EEdge%3A%20Return%20Data%0A%20%20%20%20%20%20%20%20%20%20%20%20Edge-%3E%3EKV%3A%20Store%206%2B%20months%20data%0A%20%20%20%20%20%20%20%20end%0A%20%20%20%20%20%20%20%20Edge--%3E%3ECDN%3A%20Render%20%26%20Save%20SVG%0A%20%20%20%20%20%20%20%20CDN--%3E%3EUser%3A%20Return%20SVG%0A%20%20%20%20end",[206,207,208],"em",{},"Figure 1: Request flow highlighting the dual caching layers (CDN & KV) and dual-token refresh lanes.",[168,210,212],{"id":211},"_2-the-evolution-of-my-caching-strategy","2. The Evolution of My Caching Strategy",[173,214,215,216,220],{},"My caching strategy evolved through several iterations as I learned and adapted. Here's a quick summary of the approaches I tried (",[177,217,219],{"href":218},"#table-1","Table 1","):",[173,222,223],{},[177,224],{"id":225},"table-1",[227,228,229,249],"table",{},[230,231,232],"thead",{},[233,234,235,240,243,246],"tr",{},[236,237,239],"th",{"align":238},"left","Iteration",[236,241,242],{"align":238},"Strategy",[236,244,245],{"align":238},"Pros",[236,247,248],{"align":238},"Cons / Challenges",[250,251,252,270,286,302,318],"tbody",{},[233,253,254,261,264,267],{},[255,256,257],"td",{"align":238},[258,259,260],"strong",{},"V1",[255,262,263],{"align":238},"Plain CDN caching",[255,265,266],{"align":238},"Easy to implement",[255,268,269],{"align":238},"Cache invalidation issues; users saw outdated SVGs",[233,271,272,277,280,283],{},[255,273,274],{"align":238},[258,275,276],{},"V2",[255,278,279],{"align":238},"Nitro JS SWR Cache",[255,281,282],{"align":238},"Stale-while-revalidate",[255,284,285],{"align":238},"Cached API errors; painfully long cache invalidation",[233,287,288,293,296,299],{},[255,289,290],{"align":238},[258,291,292],{},"V3",[255,294,295],{"align":238},"Versioned Cache Keys",[255,297,298],{"align":238},"Instantly busts stale images on demand",[255,300,301],{"align":238},"N/A",[233,303,304,309,312,315],{},[255,305,306],{"align":238},[258,307,308],{},"V4",[255,310,311],{"align":238},"KV Storage + CDN",[255,313,314],{"align":238},"High performance, avoids recalculations",[255,316,317],{"align":238},"More complex state management",[233,319,320,325,328,331],{},[255,321,322],{"align":238},[258,323,324],{},"V5",[255,326,327],{"align":238},"Dual-Token Fast Lane",[255,329,330],{"align":238},"Near real-time updates (5m TTL), protects primary quota",[255,332,333],{"align":238},"Requires a secondary GitHub bot account/token",[173,335,336],{},[206,337,338],{},"Table 1: Evolution of the caching strategy over five iterations",[173,340,341],{},"Here is how I navigated through each phase:",[343,344,345,352,358,375,381],"ol",{},[346,347,348,351],"li",{},[258,349,350],{},"Attempt 1: Plain CDN Caching:"," I initially started with plain SVG caching directly on the CDN. This seemed fine at first, but I quickly ran into a severe cache invalidation problem. The SVG update was controlled entirely by the edge CDN, which meant it was showing the same exact SVG even after a user's GitHub contributions were updated.",[346,353,354,357],{},[258,355,356],{},"Attempt 2: Nitro JS SWR:"," To fix the staleness, I tried using the Nitro JS SWR (stale-while-revalidate) cache. It sounded perfect, but it had a fatal flaw: it started caching the errors too! If the GitHub API hiccuped, users would see an error SVG for hours, and I had to wait painfully long for its cache invalidation to naturally expire.",[346,359,360,363,364,367,368,371,372,374],{},[258,361,362],{},"Attempt 3: Manual Version Invalidation:"," I needed a manual override for developer mistakes. I added a version number in ",[183,365,366],{},"package.json"," to force invalidate the cache. Even if an error or outdated image was stubbornly cached in the CDN, I could just update the ",[183,369,370],{},"cacheVersion"," key in ",[183,373,366],{}," to instantly bust the cache across the board.",[346,376,377,380],{},[258,378,379],{},"Attempt 4: KV Storage for Performance:"," Even with the invalidation fixed, the underlying performance problem remained. Fetching a user's entire history on every cache miss was slow. To solve this, I started caching older GitHub contributions (anything older than 6 months) in KV storage for a month. This way, I don't have to recalculate historical contributions on the fly—I just stitch the historical KV data with the recent live data, and then cache the final rendered SVG on the simple CDN.",[346,382,383,386,387,390,391,394],{},[258,384,385],{},"Attempt 5: Dual-Token \"Fast Lane\" Architecture:"," While V4 was fast, caching recent contributions for a whole hour meant users didn't see their SVGs update immediately after committing. Fetching more frequently risked blowing through the 5,000 requests/hour GitHub API limit. To solve this, I implemented a ",[258,388,389],{},"Fast Lane / Slow Lane"," dual-token system. A primary GitHub token is strictly reserved for a reliable 1-hour background refresh. A secondary token (from a bot account) handles aggressive 5-minute background refreshes and forced user invalidations (",[183,392,393],{},"?no-cache=true","). If a traffic spike burns out the secondary token, it fails silently, and the app gracefully falls back to the unbreakable 1-hour primary cache!",[168,396,398],{"id":397},"_3-reaching-a-robust-solution","3. Reaching a Robust Solution",[173,400,401],{},"Ultimately, I arrived at a robust architecture utilising two distinct types of caching and dynamic token routing:",[343,403,404,410],{},[346,405,406,409],{},[258,407,408],{},"CDN Cache:"," For the final, rendered SVG images.",[346,411,412,415],{},[258,413,414],{},"KV Cache:"," For the raw, historical GitHub contribution data (older than 6 months).",[173,417,418,419,422,423,425],{},"Crucially, both of these caching layers are strictly controlled by cache versioning, and our background refresh mechanisms are fully shielded by the dual-token rate limit isolation. Caches can be busted globally by the developer updating the ",[183,420,421],{},"cacheVersionKey",", or on a per-user basis via a ",[183,424,393],{}," query parameter (which safely routes through the secondary token). This gives me the perfect balance of lightning-fast performance, near real-time visual updates, and total control over cache invalidation.",{"title":427,"searchDepth":428,"depth":428,"links":429},"",2,[430,431,432],{"id":170,"depth":428,"text":171},{"id":211,"depth":428,"text":212},{"id":397,"depth":428,"text":398},"2026-07-19","The evolution of caching in the github-streak worker: from simple CDN headers to a dual-layer KV and Edge caching architecture.","md",null,{"type":163},{},true,{"title":66,"description":434},{"src":442},"https://placehold.co/800x400/0f172a/3b82f6?text=Edge+Caching+Journey","b-27RWdHWHfrxyT_AHbvtgQrALKzgvZdm6QNVC7clys",[445,451],{"title":62,"path":63,"stem":64,"description":446,"imageComponent":447,"seoImage":449,"children":-1},"Git commands | underrated - Dive into the details of git commands | underrated with this quick guide.",{"type":448},"Git",{"src":450},"https://placehold.co/800x400/0f172a/3b82f6?text=Git+commands+|+underrated",{"title":70,"path":71,"stem":72,"description":452,"imageComponent":453,"seoImage":455,"children":-1},"Kubernetes Cluster Setup Guide - Dive into the details of kubernetes cluster setup guide with this quick guide.",{"type":454},"DevOps",{"src":456},"https://placehold.co/800x400/0f172a/3b82f6?text=Kubernetes+Cluster+Setup+Guide",1792752997593]