# Why My Cache Has No TTL

> Source: <https://dev.to/shunya_shida/why-my-cache-has-no-ttl-oo7>
> Published: 2026-08-03 14:14:35+00:00

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)).

My answer in that post was "outsource just the search" — which means the spec files live in PostgreSQL.

In 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.

If 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.

I'll be honest: a lot of this was figured out as I went, not planned up front.

The 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.

The 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.

Here's the embarrassing part. The entire point of this service is *"find the spec you can't remember where you wrote."*

Search is not a feature — it's the product.

And 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.

Once 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.

With the store settled, I looked at the TTL again. And I stalled.

**Why am I deleting this data at all?**

I tried putting into words what this cache actually needed to guarantee, and it came out like this:

What matters is staying in sync with the latest data on GitHub — not that cache entries get deleted on some appropriate schedule.

A 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.

So the TTL went.

There'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.

Of 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*.

The 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.

The 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.

Does 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.

With the TTL gone, freshness has exactly one owner: GitHub's push webhook.

This 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.

That said, the implementation turned out to be a lot more than "receive notification, update row"...

That story deserves its own post.

The webhook wasn't the only bill that arrived later. The other one was chunking.

The 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.

Then 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.

Let me be clear about where this setup does *not* belong.

It only works because two preconditions happen to hold:

If either one fails for you, just use Redis. It's the right default for a reason.

And 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.

"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.

I'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.

Originally 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.
