The API Was Slow. Redis Was Not the Fix
Tracing one request removed the bottleneck without adding a distributed system to operate.
An API was slow, and my first solution was familiar:
- add Redis
- add EC2 auto scaling
- spread the load
The tools were reasonable.
The diagnosis was not.
After tracing one request, I found that the service loaded and parsed the same account rules several times inside the same request.
The system did not need shared caching or more servers yet.
It needed to stop repeating the same work.
A request-local cache removed the bottleneck without adding a network dependency, credentials, invalidation rules, deployment changes, or another component to monitor.
Trace the Request Before Changing the Architecture
The slow path looked like this:
def handle_request(items: list[Item]) -> list[Price]:
results = []
for item in items:
rules = load_and_parse_rules(item.account_id)
results.append(apply_rules(item, rules))
return results
Several items belonged to the same account. The rules were stable during the request, but the service reloaded and reparsed them for every item.
The measured problem was:
Repeated work inside one request.
Redis solves a different problem:
Shared cached state across processes or machines.
Auto scaling solves another:
Remaining load exceeds the capacity of current instances.
Neither matched the actual bottleneck.
Fix the Smallest Boundary
The smallest sufficient fix was request-local reuse:
def handle_request(items: list[Item]) -> list[Price]:
rules_by_account: dict[str, Rules] = {}
results = []
for item in items:
rules = rules_by_account.get(item.account_id)
if rules is None:
rules = load_and_parse_rules(item.account_id)
rules_by_account[item.account_id] = rules
results.append(apply_rules(item, rules))
return results
The change had one narrow effect:
Stable rules are loaded once per account per request.
The cache disappeared at the end of the request. That gave it useful properties:
- no stale state across requests
- no invalidation mechanism
- no shared credentials
- no network failure
- no extra deployment
- no memory that grows indefinitely
- no new on-call dependency
It solved the measured waste at the boundary where the waste existed.
Infrastructure Has an Operating Cost
Each caching boundary buys something and creates obligations:
| Mechanism | Useful when | New obligations |
|---|---|---|
| Request-local cache | Work repeats inside one request | Minimal code complexity |
| In-process cache | Work repeats across requests in one process | Size limits, expiry, staleness |
| Redis | Multiple processes need shared cached state | Network dependency, credentials, capacity, availability, invalidation |
| Auto scaling | Efficient work still exceeds current capacity | Cold starts, deployment behavior, cost variability, observability |
Adding Redis early would have widened the system before proving that shared state was necessary.
Adding more instances would have multiplied the inefficient request path across more machines.
Separate Efficiency From Capacity
These problems are often confused:
- Efficiency: one request performs unnecessary work
- Capacity: the optimized workload still exceeds available compute
The correct sequence is:
measure
-> remove redundant work
-> measure again
-> add shared infrastructure only if required
-> add capacity only if optimized load exceeds capacity
Scaling before improving efficiency can hide the root cause while increasing cost.
Know When the Cache Must Move Outward
A request-local cache is not always enough.
Move to a longer-lived boundary only when measurement proves the need.
An in-process cache may be justified when:
- the same stable data is repeatedly loaded across requests
- temporary staleness is acceptable
- one process can own the cache
A shared cache may be justified when:
- many processes need the same expensive result
- recomputation cost remains material after local fixes
- the application can tolerate or survive cache failure
- invalidation and access rules are explicit
At each step, ask what new failure mode the cache introduces:
- What happens when it is wrong?
- What happens when it is empty?
- What happens when it is full?
- What happens when it is unavailable?
- How stale may the value become?
- Who can read or modify it?
The wider the cache boundary, the higher the operational burden.
The Useful Rule
Do not begin with the infrastructure associated with a class of problem.
Begin with the measured constraint inside the actual request path.
For this API, the fix was not “make it scalable.”
It was:
Stop recomputing stable data inside one request.
That smaller statement produced the smaller and more reliable solution.
Performance Checklist
Before adding infrastructure to a slow system, ask:
- Which operation is actually slow?
- Is the work repeated unnecessarily?
- Does the result need to outlive one request?
- Must cached state be shared across processes?
- How stale may it become?
- What invalidates it?
- Can the application continue when the cache fails?
- Is the remaining problem efficiency or capacity?
- What new operational dependency will the proposed fix create?
Remove waste first.
Add infrastructure when the remaining problem genuinely requires infrastructure.
Comments