You are on call, and the dashboard turns red. The free-tier model endpoint is returning HTTP 429 for everyone, but only during office hours. Your first move is to blame the shared server. Your second move is to open the billing page. Both moves miss a cheaper failure: your app is asking the same question twice.
Let's fix that before you touch billing. This guide shows you how to audit your request logs, decide when caching makes sense, and add a small quota-aware cache in front of a model API.
Read the 429s Before Blaming the Provider
Rate-limit responses have a pattern. If they arrive in bursts after a quiet period, the likely cause is duplicate traffic:
- A user clicks 'Generate' twice because the button has no loading state.
- A client-side retry fires after a network timeout, and the original request also succeeds.
- A background job re-processes the same queue item because there is no idempotency key.
- A dashboard widget polls the same prompt every few seconds.
All of these are cacheable. None of them require a larger quota.
A Decision Table for Cacheable Model Calls
Before writing code, ask whether your response is deterministic, idempotent, and non-personalized. Use this table as a filter.
| Situation | Cache? | Why |
|---|---|---|
| Same user retries the exact same prompt | Yes | The response is identical, and the duplicate is pure waste. |
| Same intent with slightly different wording | Sometimes | Only if you add normalization or a similarity check. |
| Response includes the user's name or context | No | Caching risks leaking private data between users. |
| Live prices, weather, or status updates | No | A stale response is worse than a 429. |
| Streaming completion where tokens arrive over time | No | Cache the finished result, not the stream. |
Use this table before you design anything. It will save you from caching the wrong layer.
Measure First: A 15-Minute Request-Log Audit
Do not build a cache because you feel slow. Build it because your logs show waste. Export one day of model requests as JSONL, with at least a prompt field per record. Then run this script:
import collections
import json
import re
import sys
def normalize(text: str) -> str:
return ' '.join(re.findall(r'[a-z0-9]+', text.lower()))
def main(path: str) -> None:
counts = collections.Counter()
total = 0
with open(path, 'r', encoding='utf-8') as file:
for line in file:
record = json.loads(line)
counts[normalize(record['prompt'])] += 1
total += 1
duplicate_requests = sum(count - 1 for count in counts.values())
ratio = duplicate_requests / total if total else 0.0
print(f'total requests: {total}')
print(f'duplicate requests: {duplicate_requests} ({ratio:.1%})')
print(f'unique normalized prompts: {len(counts)}')
if __name__ == '__main__':
main(sys.argv[1])
The normalize function strips punctuation, lowercases, and keeps word order. That is enough to turn 'Cache the response!' and 'cache the response' into the same key.
A Quota-Aware Cache in About 60 Lines
Here is a small prototype you can run before buying a cache service. It adds time-to-live, LRU eviction, and hit-ratio tracking:
import time
from collections import OrderedDict
class QuotaAwareCache:
def __init__(self, ttl_seconds=300, max_items=256):
self.ttl_seconds = ttl_seconds
self.max_items = max_items
self._entries = OrderedDict()
self.hits = 0
self.misses = 0
def _is_fresh(self, entry: dict) -> bool:
return time.monotonic() - entry['stored_at'] < self.ttl_seconds
def get(self, key: str):
entry = self._entries.get(key)
if entry is None:
self.misses += 1
return None
if not self._is_fresh(entry):
del self._entries[key]
self.misses += 1
return None
self.hits += 1
self._entries.move_to_end(key)
return entry['response']
def put(self, key: str, response: str) -> None:
self._entries[key] = {'response': response, 'stored_at': time.monotonic()}
self._entries.move_to_end(key)
while len(self._entries) > self.max_items:
self._entries.popitem(last=False)
def hit_ratio(self) -> float:
total = self.hits + self.misses
return self.hits / total if total else 0.0
Then connect it to your model call:
cache = QuotaAwareCache(ttl_seconds=60, max_items=256)
def cached_generate(prompt: str) -> str:
key = normalize(prompt)
cached = cache.get(key)
if cached is not None:
return cached
response = call_free_model(prompt) # placeholder
cache.put(key, response)
return response
This is not a semantic cache. It only catches exact or near-exact duplicates after normalization. That is enough to remove the first wave of waste.
Where to Put the Cache
The cache belongs in front of the retry loop, not behind it. Place it at the service boundary after authentication, so the key does not include a user token. Use fixed TTLs between 30 seconds and 15 minutes depending on how fresh the response must be. Most importantly, export hit_ratio to your metrics dashboard. If the ratio drops below 10%, you are paying cache complexity for nothing.
Validating It on Free Models and a Free Server
To validate this pattern without spending money, you can use MonkeyCode's free model access and free server option. The shared nature of free infrastructure makes it a reasonable place to observe 429s and duplicate traffic. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Run the audit on a real request log first. Then add the cache and compare three numbers: 429 count, p95 latency, and cache hit ratio. In a duplicate-heavy workload, the cache turns those duplicate requests into local hits and keeps your quota for genuinely new prompts.
Limitations: When the Cache Is the Wrong Tool
Do not use this pattern for streaming responses, real-time data, or personalized responses. Streaming should pass through untouched. Live data should fail fast instead of returning a stale answer. Caching a personalized response is a data-leak risk even if you include the user ID in the key.
Also skip this if your normalized duplicate ratio is below 5%. A near-zero duplicate rate means the bottleneck is elsewhere, and a cache will only add a second failure mode.
Your Next 30 Minutes
- Export one day of model request logs.
- Run the audit script and record the duplicate ratio.
- If duplicates exceed 20%, add the prototype cache.
- If duplicates are below 5%, profile the network path instead.
- Expose
hit_ratioand monitor it for a week before touching a paid plan.
Your 429s are trying to tell you something. Listen to the logs before you listen to the billing page.
Top comments (0)