Slow network and offline testing recipes
A user books a taxi from a train. The app shows a spinner. They wait, tap Book again, and again. Two minutes later the spinner is still turning. Three taxis arrive.
On the office wifi, that request takes 90 milliseconds. Nobody ever saw the spinner, so nobody thought about it.
Slow networks are not an edge case. They are a Tuesday commute, a hotel, a rural road, and a busy conference. These recipes take about an hour and find bugs that never show up on a fast connection.
Short version
- Use Chrome DevTools throttling, not a guess about what slow feels like.
- The worst state is not "slow". It is "the request never finishes".
- Block a single request to test one failure at a time.
- Check double submits, spinners with no end, and what happens when you go offline mid-action.
- A good failure gives the user a message and a way to try again.
Setting up in Chrome DevTools
Everything here uses tools already built into Chrome. Firefox and Edge have close equivalents.
- Press
F12to open DevTools. - Go to the Network tab.
- Find the throttling dropdown. It usually says No throttling.
- Tick Disable cache while you are there, so you test the real request every time.
Throttling means Chrome deliberately slows down network traffic. Two numbers matter:
- Bandwidth: how much data flows per second. This affects big files, such as images.
- Latency: how long before the first byte arrives. This affects everything, and it is the one that hurts.
A request with 2000ms of latency takes two seconds before anything happens, even if the response is tiny.
The four profiles worth using
| Profile | Roughly like | Use it for |
|---|---|---|
| Fast 4G | A good mobile signal | The baseline most of your users have |
| Slow 4G | A moving train or a busy cafe | Your main regression pass on mobile |
| 3G | Rural coverage, older devices | Loading states, timeouts, layout shift |
| Offline | A tunnel, a lift, a dead wifi | Error handling and recovery |
Chrome also lets you add a custom profile. Add one called "Terrible" with download at 50 kb/s, upload at 20 kb/s, and latency at 3000ms. It exaggerates every timing bug and makes them easy to see.
Recipe 1: The spinner that never stops
Goal: find loading states with no exit.
- Set throttling to Offline.
- Reload the page, or click something that loads data.
- Start a timer. Wait 60 seconds.
What you want: an error message within about 30 seconds, in plain language, with a Try again button.
What you often get:
- A spinner that turns forever, with no timeout at all.
- A skeleton screen, those grey placeholder blocks, that never fills in.
- An empty list with the message "No results found", which is a lie. There are results; the app just could not fetch them.
That third one is the most damaging. The user believes their data is gone.
Bad: Empty table with "You have no invoices yet."
Good: "We could not load your invoices. Check your connection and try again." with a retry button.
Run this on every screen that loads data, including menus and dropdowns that fetch on open.
Recipe 2: The double submit
Goal: find actions that can be triggered twice.
- Set throttling to 3G.
- Open a form that creates something: an order, a message, a new user.
- Fill it in and click Submit.
- While the request is still in flight, click Submit again. Then a third time.
- Wait for everything to settle, then reload and count the records.
You should have exactly one record. If you have three, the button was not disabled during the request.
Variations worth running:
- Press
Enterrepeatedly instead of clicking. - Double-click quickly, which some users do out of habit.
- Click Submit, then immediately press the browser back button, then go forward and submit again.
- On a payment form, check with the developer before you test. Never run duplicate-charge tests against a live payment provider.
The fix is usually simple. The bug is usually expensive.
Recipe 3: Block one request
Throttling slows everything down. Blocking kills one specific thing, which is far better for pinpointing a failure.
- Open the Network tab and reload the page.
- Find the request you care about, for example
GET https://staging.example.com/api/user/profile. - Right-click it and choose Block request URL.
- Reload.
Now only that one call fails. Everything else works.
Try these in turn:
- Block the user profile call. Does the app show the header with an empty name, or does it crash with
TypeError: Cannot read properties of undefined (reading 'name')? - Block an image or the logo. Does the layout collapse, or does the alt text appear neatly?
- Block the analytics script. The app must keep working. This is the same failure an ad blocker causes.
- Block a font file. Text should still be readable in a fallback font, not invisible.
- Block one item in a dashboard of six widgets. Five widgets should still load. If one failure blanks the whole page, that is a bug.
- Block the token refresh call. The app should send the user to login, not sit half-broken.
You can also block a whole domain with a pattern such as *cdn.example.com*. Use Block request domain from the same right-click menu.
Recipe 4: Go offline mid-action
Goal: test the moment the connection dies, not the state of being offline.
This is the hardest case for developers to get right, so it is where the bugs are.
- Start with throttling off.
- Begin a slow action: upload a 20 MB file, or submit a long form.
- While it is running, switch throttling to Offline.
- Watch what happens.
- After 20 seconds, switch back to Fast 4G.
Check three things:
- Did the app tell the user something went wrong?
- Did it recover on its own when the connection came back, or does the user have to do something?
- Was the work lost?
For file uploads, check whether the progress bar freezes at 43% forever or reports a failure. A frozen progress bar with no message is one of the most reported "the app is broken" complaints in support queues.
Also test the reverse. Start offline, then come back online. Many apps cache the "you are offline" banner and never remove it.
Recipe 5: Retry behaviour
Retry means the app automatically tries a failed request again. Good retry logic is invisible. Bad retry logic makes things worse.
To test it, use the Network tab and watch the request list.
- Block the API call for a key screen.
- Reload and count how many times the same request appears in the list.
Then judge it:
- No retry at all. One attempt, then an error. Acceptable for actions that change data, weak for simple reads.
- A few retries with a growing gap. Three attempts at 1s, 2s, then 4s, then a clear error. This is what you want.
- Endless retries. The list grows and grows. This drains battery and hammers the server. Report it.
- Retry on a payment or a create action. Dangerous. A retried create can produce duplicates. Check with the team whether the endpoint is safe to repeat.
While you have the network log open, note the status codes. A 504 Gateway Timeout and a net::ERR_INTERNET_DISCONNECTED should both lead to a friendly message, but they often take different code paths and only one is handled.
Recipe 6: Layout under slow loading
Slow loading exposes visual bugs that fast loading hides.
- Set throttling to 3G and reload.
- Watch the page as it builds.
Look for:
- Layout shift, where content jumps down as images arrive and the user clicks the wrong thing.
- Text appearing before its font loads, then changing size.
- A modal that opens before its content arrives, so it flashes at 40 pixels tall and then jumps.
- Buttons that are clickable before the page is ready, firing an action against missing data.
Record a short screen capture here rather than taking a still image. The problem is the movement, and a static screenshot cannot show it.
What to write in the bug report
Network bugs are the classic "works on my machine" ticket. The report has to carry the conditions.
Include:
- The exact throttle profile, named. "Slow 4G" beats "slow connection".
- Whether you blocked a request, and which URL.
- How long you waited before deciding it was stuck.
- The failing request with its status:
GET /api/invoicesstayed pending for 60s, or returned504. - Any console errors, copied as text.
- What the user sees, and what they can do next.
A screen recording plus the network log is worth more than a paragraph of description. If you use a browser-based reporting tool such as Crosscheck, the network requests and console output are captured with the report, so the developer can see the pending call rather than take your word for it.
Frequently asked questions
Is DevTools throttling accurate compared to a real slow connection?
It is close enough to find bugs, and that is its job. Real mobile networks are worse in ways throttling does not copy, such as packet loss and sudden signal drops. Use throttling every day, and test on a real device on real mobile data before a major release.
Which profile should I use for everyday testing?
Slow 4G. It is slow enough to reveal loading states and fast enough that you can still get through a test pass. Save 3G and Offline for targeted recipes.
Does throttling affect requests already in flight?
Yes. Switching to Offline while a request is running is exactly how you test a dropped connection, and it is the most useful trick in this article.
How long should an app wait before showing a timeout error?
There is no fixed rule, but somewhere between 10 and 30 seconds suits most screens. The important part is that a limit exists. A spinner with no timeout is always a bug, whatever the number.
Should the app work fully offline?
Only if your team decided it should. Most web apps do not need offline support. Every web app needs to fail clearly when the network drops, and that is what these recipes check.




