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