Engineering
Circuit breakers, graceful degradation, and UIs that stay calm
When TMDB goes down, Horrorometer doesn’t. The resilience patterns that keep an external-API-dependent product from melting.
Horrorometer depends on two external APIs — TMDB for film and TV data, Open Library for books. Both are reliable… most of the time. "Most of the time" is not an availability target you can build a product on.
The day TMDB had an extended outage, Horrorometer kept working. Here’s the stack of patterns that made that possible — in the order I’d recommend implementing them.
1. Timeouts, before anything clever
Every outbound call has a timeout, and it’s shorter than you’d think. A hanging request is worse than a failed one — it holds connections, ties up Workers CPU time, and makes the user wait for an error they could have had five seconds ago.
If the upstream doesn’t respond in time, fail fast and let the fallback layer handle it.
2. Retry with exponential backoff — and a cap
Three attempts, exponential backoff. But retries are a tax you pay to the upstream under stress, so cap them. Three is enough. And never retry non-idempotent calls with the same payload without thinking about what happens if the first attempt actually landed.
3. The circuit breaker: stop hammering a dead service
The core pattern. Horrorometer’s breaker works like this:
- 5 failures within 60 seconds → the circuit opens
- while open, calls fail immediately — no request leaves the building
- after 30 seconds, one trial request goes through
- 2 successes → the circuit closes and traffic resumes
That’s it. No external state, no monitoring dashboard required — a small state machine that answers one question: "should I even try?"
The value isn’t that it saves the user from errors. It’s that when TMDB comes back, Horrorometer recovers within 30 seconds instead of drowning it in a backlog of retries the moment it shows a pulse.
4. Graceful degradation: the UI never shows a hard error
The breaker decides whether to call the upstream. Degradation decides what the user sees when the answer is "no":
- Cached data stays served — a slightly stale movie page beats an error page, every time.
- Empty states are honest but calm: "We’re having trouble reaching our film data right now" — not a red banner, not a crash screen.
- Writes queue where they can, and fail cleanly where they can’t, with a message that tells the user what to do next.
The user-facing rule: never show an error you can’t tell the user how to get past.
5. Fail-open, but only where it’s safe
Counter-intuitive one: Horrorometer’s rate limiting fails open. If the rate-limit store is unreachable, requests pass through, because a database outage should not lock every user out of the site. That’s the right trade for a review platform. It would be the wrong trade for an admin panel — the point is that it’s a deliberate decision, made per subsystem, not a default.
The payoffs you don’t expect
The obvious payoff is uptime. The unexpected one is what this does to your own head: once the failure paths are designed, an upstream outage stops being an emergency. Sentry fires, you glance at the graphs, you get on with your day. The resilience layer is what makes a solo-run product feel boring — and boring is the goal.
The full stack in one sentence: timeout, backoff, breaker, degrade — and make every failure a designed one.
Keep reading
More from the blog.
Accessible by default: how I bake it in from the first sketch
Accessibility isn’t a checklist at the end — it’s a posture. Here’s how I keep it present from wireframe through to production code.
Shipping solo: what I learned building three live products
Horrorometer, Kanaboards, and SENDadvisor all started as itches I wanted to scratch. Some patterns held across all three — and a few surprised me.
Designing for stressful systems: lessons from SENDadvisor
Families navigating SEND services are already overwhelmed. How do you design a directory that feels calm, trustworthy, and genuinely useful?