← All notes
Product & Platform Engineering5 min

A 429 is not a toast

Most of the platforms we integrate — Shopify Admin, vision models for invoice lines, PrintNode after a quiet hour — will eventually answer with 429. On the server that is a quota signal. In the product it is often collapsed into a generic failure toast and a blind retry. Those two choices turn a temporary budget into a trust problem: the user sees "broken," refreshes, and makes the herd worse. Treat throttled as its own state, the same way you treat pending entitlements as neither allowed nor denied.

A toast hides the only useful fact

A rate limit response usually carries the recovery hint: Retry-After, remaining quota headers, or a reset timestamp. If the client maps every non-2xx to "Something went wrong," that hint dies in the network layer. The person waiting on a PO parse or a delivery ticket never learns whether to wait twenty seconds or open a ticket.

We have seen this on field and admin surfaces that share one upstream budget. The OCR worker backs off correctly in logs while the UI still paints a red banner. Operators retry from the browser. The worker and the browser then compete for the same remaining tokens. The toast did not fail to notify — it notified the wrong story.

Throttled is not offline and not denied

Three product states get mashed together: the network is gone, the account is not allowed, and the account is allowed but the budget is spent for a window. Offline wants reconnect UX. Denied wants an entitlement or plan explanation. Throttled wants a clock and a promise about what the app is doing.

If you reuse the entitlement upsell for a 429, entitled users think they lost access. If you reuse the offline empty state, they toggle airplane mode. Name the state in copy the way you name it in code: "We're waiting on the provider — next try at 15:04 UTC" beats both.

Respect Retry-After; stop inventing your own cadence

Exponential backoff with jitter is the right default when the server is silent about timing. When Retry-After is present, it is the schedule. Ignoring it to "be helpful" with a faster poll is how interactive clicks steal budget from the job that was already mid-flight.

On multi-surface products the rule has to be shared. BuilderHelp's admin UI, field app, and OCR worker are three callers that can look independent and still share one vendor ceiling. A client-side retry policy that only knows about its own tab will happily recreate the thundering herd the server just refused.

Protect the live path in the interface too

Server notes already cover catch-up losing to live traffic. The UI needs the same priority. A "Retry all failed syncs" control that fires unbounded requests is a product footgun after an outage. Prefer a queued recovery with a visible budget — how many are left, that live actions still work, that bulk retry is paced.

For writes, pair the throttle state with idempotency the user cannot see: if they click again because the button looked dead, the second submit must not create a second side effect when the first one eventually lands.

What this is not

This is not an argument against rate limits, and it is not the same problem as structuring API error types for partners. A stable problem code helps machines branch; a throttle state helps humans wait without thrashing. You usually need both.

It is also not entitlement flash. Pending plan status should not render an upsell. Pending quota should not render a fatal error. Different unknowns, different placeholders — both are correctness bugs when you pick the wrong confident UI too early.

Questions

Why shouldn't a 429 show the same toast as other errors?
A rate limit usually includes recovery timing. A generic failure toast drops that signal, invites immediate retries, and teaches users the product is broken when it is only budgeted for a window.
What should the UI show while throttled?
Which action paused, when the window resets, whether the app will retry, and prior results still on screen. Disable stampede controls until the budget reopens.
How is this different from catch-up rate limiting on the server?
Server catch-up pacing protects live traffic in workers and queues. Throttle UI protects the same budget from human retries and makes the wait legible. Both layers have to agree on priority.

Sources

  1. MDN — 429 Too Many Requests
  2. RFC 6585 — Additional HTTP Status Codes (429)
  3. IETF RateLimit header fields draftUseful vocabulary for communicating limit, remaining, and reset on success paths too.
  4. AWS Architecture Blog — exponential backoff and jitter

Have something to build?

Tell us what you're working on and we'll tell you honestly whether we're the right fit.

Work with us