Last week I repeated my favorite mistake: I trusted a cache more than I trusted the generative model behind it. After the retry storm incident, I told myself the model would be the weak link in any free AI stack. I spent 48 hours caching free model output through MonkeyCode's free server option, hoping to turn a slow, occasionally drifting service into a fast, stable one. The model drifted less than I expected. The cache drifted more, and it did so silently.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The setup: why I added a cache
My target was small, and small targets are where I make the largest assumptions. A free model classified support tickets into one of five categories and returned JSON. The cost was not money; the cost was waiting for the model and watching the same prompt return different labels across hours. A cache looked like the obvious fix because the prompts were small and the categories were stable. I thought I was protecting users from latency; I was actually giving the system a place to store opinions. That distinction became painful within the first few requests.
Failure one: the cache key ignored context
The first incident felt like a model problem until I checked the logs. A user edited a ticket after it had been classified, and my cache returned the old category. The cache key was built only from the prompt text, so it could not know that the input context had changed. The ticket text was different, the category should have been reevaluated, and the hash did not care. In a deterministic function, the same input means the same output; in an LLM service, the same prompt can have different acceptable answers. What good is a cache if it returns a confident, outdated answer?
Failure two: validation ran on the write, not on the read
My second mistake was more subtle. I validated the model response before writing it to storage, but I never re-validated the value when reading it back. One malformed JSON object slipped past because the validation result and the stored value did not share the same lifetime. A poisoned entry then remained readable for hours, and every consumer got the same broken shape. I felt clever for catching the bad write; the read path had no such guard. Why would you trust a cached value more than a fresh model output?
Failure three: one TTL for every behavior
The model did not change uniformly. Some prompt patterns stayed stable for long stretches, while others shifted their semantics much faster. A single TTL made stable outputs expire before they needed to and unstable outputs live too long. The worst part was that nothing in the cache told me which case I was in. A cache is a memory, but a bad TTL turns it into an assumption. The next question was: how do I make this assumption visible?
The artifact: a cache that fails loud
Here is the pattern I would run again. It is deliberately simple, and it works for any model client, free or paid. I call it safe_get because the only thing worse than no cache is a cache that lies.
def safe_get(session, prompt, context, ttl_seconds):
key = stable_hash({
'prompt': prompt,
'context': context,
'schema_version': '2026.09',
'model_family': 'free',
})
hit = storage.get(key)
if hit and not expired(hit['created_at'], ttl_seconds):
if validator(hit['value']):
return hit['value']
storage.delete(key)
log.warning('Removed poisoned cache entry: %s', key)
value = session.complete(prompt)
if not validator(value):
raise ValueError('Model output failed validation; nothing was cached.')
storage.set(key, {'value': value, 'created_at': now()})
return value
The important details are in the small decisions. The key contains the context and a schema version, so an edit or a schema change can never hit an old entry. The validator runs on both paths, and it deletes a bad hit instead of returning it. Invalid output is never cached at all, because raising an error is better than repeating a bad answer. Three rules made the difference:
-
schema_versionis not optional; model output changes shape, and old JSON becomes invalid. -
contextis not optional; when the user edits the source text, a new key must be created. -
validatorruns on both paths; write-time validation prevents bad data, and read-time validation prevents stale bad data.
When I would cache, and when I would not
Here is the decision table I wish I had written before the experiment. The common thread is intent: what are you willing to repeat?
- Immutable input, stable schema, non-critical output: cache with a long TTL and read-time validation.
- User-editable content, summaries, or labels: shorten the TTL or skip the cache entirely.
- Output that triggers transactions, payments, or moderation actions: do not cache; treat every call as a fresh decision.
- Experimental prompts that still change often: cache by hand only, with a manual purge button.
What held up
I should say what did not break. The free model answered consistently enough for the core classification task, and the free server option handled the workload without me touching infrastructure. Neither the model nor the server caused the worst failures; my application logic did. That is the field note I keep forgetting: most AI stack failures are not AI failures. The free parts of the stack earned their place; the code I added around them did not.
Limitations, because these are field notes
This was a 48-hour run with one workload, not a benchmark. I did not compare providers, measure latency under load, or test every prompt shape. I also did not measure the cost of cache misses versus model calls with enough precision to make an economic claim. The pattern above worked for JSON classification on a small set, so test it on your own traffic before trusting it. Cache invalidation remains a hard problem, and a validator is a guard, not a guarantee.
What I would repeat
The most dangerous part of a free LLM stack is often the layer you add to protect yourself from the LLM. Cache with respect: version your keys, validate on read, and expire with intent. Before you wrap a free model in another cache, write the read-time validator first. That is the step I almost skipped, and the one I would repeat.
Top comments (0)