{"slug": "why-my-cache-has-no-ttl", "title": "Why My Cache Has No TTL", "summary": "A developer behind the Repospec service explains why its cache layer has no TTL, opting to store spec files directly in PostgreSQL for search rather than using a traditional cache like Redis. The decision prioritizes search consistency over cache eviction, ensuring files remain searchable indefinitely.", "body_md": "Last time, I wrote about the dilemma of feeding specs scattered across multiple repositories to AI ([Index Everything, or Read Everything?](https://dev.to/shunya_shida/index-everything-or-read-everything-the-dilemma-of-feeding-specs-to-ai-in-multi-repo-development-52h7)). This post is a devlog for the caching layer of the service I mentioned there ([Repospec](https://repospec.dev)).\n\nMy answer in that post was \"outsource just the search\" — which means the spec files live in PostgreSQL.\n\nIn other words, PostgreSQL holds a cache of files that have already been pushed to GitHub. And that cache has **no TTL**. Once a file goes in, it stays. Indefinitely, by design.\n\nIf you've ever built a cache, this probably makes your skin crawl. It made mine crawl too — the first version absolutely had a TTL. This post is my personal record of how I talked myself out of it.\n\nI'll be honest: a lot of this was figured out as I went, not planned up front.\n\nThe one thing I knew from the start was that fetching from GitHub on every request was a bad idea, purely for speed (the previous post covers the broader reasoning). So caching was always part of the plan. And my thought process for choosing the store was, in its entirety: \"It's a cache, so... Redis, right?\" Not my proudest engineering decision, but I suspect it's the same reflex most of us have.\n\nThe TTL went in just as automatically. Files fetched from GitHub would expire after a fixed window. Why? Because that's what caches do. That was the whole reason.\n\nHere's the embarrassing part. The entire point of this service is *\"find the spec you can't remember where you wrote.\"*\n\nSearch is not a feature — it's the product.\n\nAnd yet there I was, picking a cache store before thinking about how search would work at all. I got absorbed in the architecture and drifted away from the actual problem.\n\nOnce search finally got my attention, the answer more or less picked itself: query PostgreSQL directly. Redis is excellent at being fast and excellent at being a cache, but for the kind of fine-grained text search I needed, Postgres was simply the better fit for this use case.\n\nWith the store settled, I looked at the TTL again. And I stalled.\n\n**Why am I deleting this data at all?**\n\nI tried putting into words what this cache actually needed to guarantee, and it came out like this:\n\nWhat matters is staying in sync with the latest data on GitHub — not that cache entries get deleted on some appropriate schedule.\n\nA TTL doesn't guarantee freshness. It just guarantees deletion. Sure, the entry gets re-fetched afterward and ends up fresh — but \"eventually re-fetched\" is not the same thing as \"in sync.\" Worse, in this service, a file that expires from the cache *also disappears from search results*. \"I searched for it and it wasn't there\" is the single failure mode I refuse to accept.\n\nSo the TTL went.\n\nThere's a flip side, though. Removing the TTL means holding onto files from people's private repositories indefinitely. So I also decided what *not* to hold: the search logs store no spec content and no snippets — queries only. I'd call it less a privacy stance and more a \"this is confidential material and I should act like it\" stance.\n\nOf course I considered the two-store setup: Redis for caching, PostgreSQL for search. It's a perfectly reasonable architecture. I just couldn't find a benefit that applied *here*.\n\nThe thing is, **this cache doesn't behave like a network cache**. A network cache grows without bound unless you evict — eviction is load-bearing. This cache has a natural ceiling: it can never hold more than the spec files users have registered. Nothing else ever gets in. The thing a TTL usually protects you from simply doesn't exist here.\n\nThe second reason is plumbing. With two stores, every GitHub update has to be synced to *both* Redis and Postgres. Two sync targets means two ways to drift. I wanted exactly one.\n\nDoes Postgres lose to Redis on raw speed? Yes, clearly. But Postgres offered a much wider range of search capabilities, and this whole thing runs behind MCP — an AI agent reading spec files. On that path, a few tens or hundreds of milliseconds are effectively invisible. Shaving milliseconds off a route where the consumer is an LLM buys you nothing.\n\nWith the TTL gone, freshness has exactly one owner: GitHub's push webhook.\n\nThis was a design commitment from day one: **keeping the cache fresh must never be a human's job.** Imagine pushing a commit, then having to open a dashboard and click a \"re-sync\" button. That's real friction, and I know myself — I wouldn't do it. With webhooks, there's a small lag, but the chore simply doesn't exist.\n\nThat said, the implementation turned out to be a lot more than \"receive notification, update row\"...\n\nThat story deserves its own post.\n\nThe webhook wasn't the only bill that arrived later. The other one was chunking.\n\nThe first version stuffed each spec file into a single column, whole. That fit my usual approach — get something working, then improve it — so in it went. But a column holding a massive blob of text is not a healthy thing to run search queries against. The files needed to be split into reasonably sized pieces.\n\nThen the questions started. What granularity actually gives the best search precision and usability? What about files without clean structural boundaries, unlike Markdown? Turns out \"just split the text\" is not a thing — I only learned that by trying. This rabbit hole goes as deep as you're willing to dig, so for now the chunk size is a hardcoded guess. There's a very real chance this ends up delegated to something like Elasticsearch eventually, but I'd rather tune it against real usage than against my imagination.\n\nLet me be clear about where this setup does *not* belong.\n\nIt only works because two preconditions happen to hold:\n\nIf either one fails for you, just use Redis. It's the right default for a reason.\n\nAnd to be clear, this post says nothing about network-level caching in general — there are far better articles on that. Everything here only applies because the data had an unusual shape *and* MCP made latency nearly irrelevant.\n\n\"Cache means Redis\" is a reflex most of us carry. It's a good reflex. But depending on what you're actually protecting, a setup like this one can be enough.\n\nI'd genuinely like to hear how others have reasoned about this. Have you ever dropped a TTL — or deliberately kept one where it wasn't strictly needed? And if you've dealt with chunking for search, I'd love to know where you landed on granularity. That part is still a guess on my side.\n\nOriginally written in Japanese ([https://zenn.dev/shida_dev/articles/a4dfcec6c08ad5](https://zenn.dev/shida_dev/articles/a4dfcec6c08ad5)). Translated with AI assistance and reviewed by the author.", "url": "https://wpnews.pro/news/why-my-cache-has-no-ttl", "canonical_source": "https://dev.to/shunya_shida/why-my-cache-has-no-ttl-oo7", "published_at": "2026-08-03 14:14:35+00:00", "updated_at": "2026-08-03 14:46:54.925771+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["Repospec", "PostgreSQL", "Redis", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/why-my-cache-has-no-ttl", "markdown": "https://wpnews.pro/news/why-my-cache-has-no-ttl.md", "text": "https://wpnews.pro/news/why-my-cache-has-no-ttl.txt", "jsonld": "https://wpnews.pro/news/why-my-cache-has-no-ttl.jsonld"}}