The 200ms target on bad networks
What you can guarantee on 2G, what you cannot, and why most performance budgets are wishful thinking.
Loading essay…
What you can guarantee on 2G, what you cannot, and why most performance budgets are wishful thinking.
Loading essay…
"User actions feel instant when they complete in under 200 milliseconds." This number gets quoted in every performance writeup. What nobody tells you is that on real consumer networks, 200 ms is gone before your code runs. Here is what you can guarantee, what you cannot, and why most performance budgets are wishful thinking.
Boot up a 4G phone with a typical (not best-case) cell signal, issue a request to a server in a different country, measure where the time lives:
TLS handshake (cold) 180 ms
DNS lookup (cold) 70 ms
TCP three-way 40 ms (rural cellular)
HTTP request body ~5 ms
Server processing varies
Response body ~10 ms (small)
Render parse + paint ~16 ms (one frame)
That is ~315 ms before your code does anything, on a cold connection. Even a warm connection (TLS resumption, DNS cached, TCP held open) costs 60-90 ms of round-trip on real cellular. The 200 ms budget is gone before the request reaches your server.
This is not a tale of woe. It is a precondition. Performance work on networked actions is mostly about avoiding the round-trip, not about speeding it up.
Three patterns produce instant-feeling actions on bad networks. None of them involve making the network faster.
The action visibly takes effect before the round-trip. The user taps "send"; the message appears in the chat with a "sending" indicator; the round-trip resolves it to "sent" or "failed."
This works because the user's mental model is that they sent the message. The truth (the server hasn't acknowledged) is implementation detail. The "sending" indicator is the honest part: we tell them it is in flight; they trust the system to resolve it.
The data on the screen is read from a local database, not the network. The network is only used to keep the local database fresh, in the background, when convenient. We covered this pattern in the offline-first Flutter article; it is the structural reason apps can feel fast on bad networks.
If you know the user is about to need data X (because they are on the screen that lists Xs and the next obvious tap is "open one"), fetch it ahead of time. Latency hidden behind a tap that has not happened yet is latency that is not in the critical path.
The dangerous form of this pattern is "pre-fetch everything." It costs the user data plan and battery. Be specific: pre-fetch only the items that have a > 30 % click-through rate from the current screen.
Three SLOs we offer:
| Metric | 4G typical | 2G | Notes |
|---|---|---|---|
| Tap-to-acknowledged | < 50 ms | < 50 ms | Optimistic local update |
| Tap-to-server-confirmed | < 1.2 s | < 5 s | Round-trip |
| Sync-after-reconnect | < 8 s | < 30 s | Catch-up window |
| First meaningful paint | < 800 ms | < 1.5 s | Cold app start |
| First paint after bg→fg | < 200 ms | < 200 ms | Local data, no round-trip |
The "tap-to-acknowledged" row is the only one where 200 ms applies, and that is because it is local. The "tap-to-server-confirmed" row is realistic for a network round-trip and we do not pretend otherwise.
The honest framing for users: actions feel instant; sync is in the background; if something fails, we tell you, with a way to retry.
Some things just are slow on bad networks. Be honest about them instead of papering over.
A search across a tenant's full dataset. A report. A "what is the current invoice total." Round-trip cost is unavoidable. Show a skeleton, don't stall on a spinner.
Maps. External authentication providers. Embedded video. Each one is its own DNS + TLS + request cycle, and they add up. If you can avoid the third party, do; if you can't, lazy-load below the fold and show the rest of the page.
Websockets, SSE, push notifications, all of these degrade when the network is congested. The fallback is not "make it work harder;" the fallback is polling on a longer interval and accepting the latency.
Two places:
Every key action emits a single event with start/end timestamps and the network type at the time. We aggregate to p50 / p95 / p99 per network type. The interesting one is p95 on cellular; p50 on wifi flatters the numbers.
Every PR runs a synthetic test against the staging server with network throttled to "slow 3G" via DevTools profile. Any action that takes longer than its budget on slow 3G blocks the PR. This catches regressions that look fine on a fast home network.
Bad networks make the 200 ms target unreachable for any action that involves the network. The honest path forward is to design actions that do not involve the network on the critical path: optimistic local writes, local-first reads, predictive prefetch. Then state clearly what the actual round-trip budget is, and meet it.
Most apps are dishonest about their performance budgets because they were written for the home wifi of the engineering team. The first time you ship to a customer in a basement on 3G, you find out which budget was real.
The next post is on indexing strategies for analytical queries. Or browse the writing index for other directions.