[{"data":1,"prerenderedAt":404},["ShallowReactive",2],{"navigation":3,"/blog/journey-github-streak-caching":130,"surround-journey-github-streak-caching":391},[4],{"title":5,"path":6,"stem":7,"children":8,"page":129},"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],{"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},"Disk Management in Linux","/blog/disk-management-in-linux","blog/disk-management-in-linux",{"title":42,"path":43,"stem":44},"Docker Devtools Stage | Powerlevel, OhMyZSH, tmux, fuzzy finder, autocomplete etc","/blog/docker-devtools","blog/docker-devtools",{"title":46,"path":47,"stem":48},"Docker container | Resolving Permission Denied Woes: A Decade-Long Struggle Unraveled","/blog/docker-non-root-user-perms","blog/docker-non-root-user-perms",{"title":50,"path":51,"stem":52},"Git commands | underrated","/blog/git-commands-underrated","blog/git-commands-underrated",{"title":54,"path":55,"stem":56},"Journey: Taming Edge Caching and KV Storage","/blog/journey-github-streak-caching","blog/journey-github-streak-caching",{"title":58,"path":59,"stem":60},"Kubernetes Cluster Setup Guide","/blog/k8s-setup","blog/k8s-setup",{"title":62,"path":63,"stem":64},"Linux WiFi Drivers","/blog/linux-wifi-drivers","blog/linux-wifi-drivers",{"title":66,"path":67,"stem":68},"pfSense basic firewall setup","/blog/pfsense-basic-firewall-setup","blog/pfsense-basic-firewall-setup",{"title":70,"path":71,"stem":72},"pfSense Quickstart","/blog/pfsense-quickstart","blog/pfsense-quickstart",{"title":74,"path":75,"stem":76},"Postmortem: Cloudflare Worker Missing Dependency on Deploy","/blog/postmortem-cloudflare-worker-dependency-deploy","blog/postmortem-cloudflare-worker-dependency-deploy",{"title":78,"path":79,"stem":80},"Postmortem: Serverless HTTP Driver Transaction Failures","/blog/postmortem-neon-http-transactions","blog/postmortem-neon-http-transactions",{"title":82,"path":83,"stem":84},"Postmortem: Nuxt Content 404s on Staging Deployments","/blog/postmortem-nuxt-content-404-staging","blog/postmortem-nuxt-content-404-staging",{"title":86,"path":87,"stem":88},"Postmortem: 500 Internal Server Error on ISR Routes","/blog/postmortem-nuxt-content-payload-extraction","blog/postmortem-nuxt-content-payload-extraction",{"title":90,"path":91,"stem":92},"Post-Mortem: Intrusive Database Driver Refactoring","/blog/postmortem-proxy-re-architecting","blog/postmortem-proxy-re-architecting",{"title":94,"path":95,"stem":96},"Post-Mortem: Cloudflare Worker Connection Hang & Resilient DB Fallbacks","/blog/postmortem-worker-db-connection-hang","blog/postmortem-worker-db-connection-hang",{"title":98,"path":99,"stem":100},"Pro Tip: Sandboxing Cloudflare Workers for AI Agents","/blog/pro-tip-wrangler-local-dev-trick","blog/pro-tip-wrangler-local-dev-trick",{"title":102,"path":103,"stem":104},"Proxmox DNS Management","/blog/proxmox-dns","blog/proxmox-dns",{"title":106,"path":107,"stem":108},"Proxmox Recovery","/blog/proxmox-recovery","blog/proxmox-recovery",{"title":110,"path":111,"stem":112},"Proxmox WiFi Connection Story","/blog/proxmox-wifi","blog/proxmox-wifi",{"title":114,"path":115,"stem":116},"roadauth-rails api jwt cors","/blog/rodauth-jwt","blog/rodauth-jwt",{"title":118,"path":119,"stem":120},"SOCKS5 Proxy Quickstart","/blog/sock5-proxy","blog/sock5-proxy",{"title":122,"path":123,"stem":124},"tmux status bar toggle","/blog/tmux-status","blog/tmux-status",{"title":126,"path":127,"stem":128},"UFW - Cheatsheet","/blog/ufw-cheatsheet","blog/ufw-cheatsheet",false,{"id":131,"title":54,"authors":132,"badge":138,"body":140,"date":380,"description":381,"extension":382,"image":383,"imageComponent":384,"meta":385,"navigation":386,"path":55,"pinned":129,"seo":387,"seoImage":388,"stem":56,"__hash__":390},"posts/blog/journey-github-streak-caching.md",[133],{"name":134,"to":135,"avatar":136},"Rahul Dhole","/",{"src":137},"/profile.png",{"label":139},"Journey",{"type":141,"value":142,"toc":373},"minimark",[143,148,163,171,185,189,197,202,294,299,302,341,345,348,362],[144,145,147],"h2",{"id":146},"_1-the-challenge","1. The Challenge",[149,150,151,152,162],"p",{},"When building the ",[153,154,158],"a",{"href":155,"rel":156},"https://github.com/rahuldhole/github-streak",[157],"nofollow",[159,160,161],"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.",[149,164,165,166,170],{},"Here is the high-level request flow (",[153,167,169],{"href":168},"#fig-1","Figure 1",") that highlighted the need for multiple layers of caching:",[149,172,173,176,181],{},[153,174],{"id":175},"fig-1",[177,178],"mermaid",{":config":179,"code":180},"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%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%3EGitHub%3A%20Fetch%20recent%20data%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%20Fetch%20ALL%20data%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",[182,183,184],"em",{},"Figure 1: Request flow highlighting the dual caching layers (CDN & KV)",[144,186,188],{"id":187},"_2-the-evolution-of-my-caching-strategy","2. The Evolution of My Caching Strategy",[149,190,191,192,196],{},"My caching strategy evolved through several iterations as I learned and adapted. Here's a quick summary of the approaches I tried (",[153,193,195],{"href":194},"#table-1","Table 1","):",[149,198,199],{},[153,200],{"id":201},"table-1",[203,204,205,225],"table",{},[206,207,208],"thead",{},[209,210,211,216,219,222],"tr",{},[212,213,215],"th",{"align":214},"left","Iteration",[212,217,218],{"align":214},"Strategy",[212,220,221],{"align":214},"Pros",[212,223,224],{"align":214},"Cons / Challenges",[226,227,228,246,262,278],"tbody",{},[209,229,230,237,240,243],{},[231,232,233],"td",{"align":214},[234,235,236],"strong",{},"V1",[231,238,239],{"align":214},"Plain CDN caching",[231,241,242],{"align":214},"Easy to implement",[231,244,245],{"align":214},"Cache invalidation issues; users saw outdated SVGs",[209,247,248,253,256,259],{},[231,249,250],{"align":214},[234,251,252],{},"V2",[231,254,255],{"align":214},"Nitro JS SWR Cache",[231,257,258],{"align":214},"Stale-while-revalidate",[231,260,261],{"align":214},"Cached API errors; painfully long cache invalidation",[209,263,264,269,272,275],{},[231,265,266],{"align":214},[234,267,268],{},"V3",[231,270,271],{"align":214},"Versioned Cache Keys",[231,273,274],{"align":214},"Instantly busts stale images on demand",[231,276,277],{"align":214},"N/A",[209,279,280,285,288,291],{},[231,281,282],{"align":214},[234,283,284],{},"V4",[231,286,287],{"align":214},"KV Storage + CDN",[231,289,290],{"align":214},"High performance, avoids recalculations",[231,292,293],{"align":214},"More complex state management",[149,295,296],{},[182,297,298],{},"Table 1: Evolution of the caching strategy over four iterations",[149,300,301],{},"Here is how I navigated through each phase:",[303,304,305,312,318,335],"ol",{},[306,307,308,311],"li",{},[234,309,310],{},"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.",[306,313,314,317],{},[234,315,316],{},"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.",[306,319,320,323,324,327,328,331,332,334],{},[234,321,322],{},"Attempt 3: Manual Version Invalidation:"," I needed a manual override for developer mistakes. I added a version number in ",[159,325,326],{},"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 ",[159,329,330],{},"cacheVersion"," key in ",[159,333,326],{}," to instantly bust the cache across the board.",[306,336,337,340],{},[234,338,339],{},"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.",[144,342,344],{"id":343},"_3-reaching-a-robust-solution","3. Reaching a Robust Solution",[149,346,347],{},"Ultimately, I arrived at a robust architecture utilizing two distinct types of caching:",[303,349,350,356],{},[306,351,352,355],{},[234,353,354],{},"CDN Cache:"," For the final, rendered SVG images.",[306,357,358,361],{},[234,359,360],{},"KV Cache:"," For the raw, historical GitHub contribution data (older than 6 months).",[149,363,364,365,368,369,372],{},"Crucially, both of these caching layers are strictly controlled by cache versioning. They can be busted globally by the developer updating the ",[159,366,367],{},"cacheVersionKey",", or on a per-user basis via a ",[159,370,371],{},"?v="," query parameter. This gives me the perfect balance of lightning-fast performance and total control over cache invalidation.",{"title":374,"searchDepth":375,"depth":375,"links":376},"",2,[377,378,379],{"id":146,"depth":375,"text":147},{"id":187,"depth":375,"text":188},{"id":343,"depth":375,"text":344},"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":139},{},true,{"title":54,"description":381},{"src":389},"https://placehold.co/800x400/0f172a/3b82f6?text=Edge+Caching+Journey","l4PpDvHXpg1BWbhzjh9iCkL4HLO0D80Tv4N_c9QK6lI",[392,398],{"title":50,"path":51,"stem":52,"description":393,"imageComponent":394,"seoImage":396,"children":-1},"Git commands | underrated - Dive into the details of git commands | underrated with this quick guide.",{"type":395},"Git",{"src":397},"https://placehold.co/800x400/0f172a/3b82f6?text=Git+commands+|+underrated",{"title":58,"path":59,"stem":60,"description":399,"imageComponent":400,"seoImage":402,"children":-1},"Kubernetes Cluster Setup Guide - Dive into the details of kubernetes cluster setup guide with this quick guide.",{"type":401},"DevOps",{"src":403},"https://placehold.co/800x400/0f172a/3b82f6?text=Kubernetes+Cluster+Setup+Guide",1784464468315]