Something is broken and you think it's the network. Maybe a request is hanging, a screen loads blank on slower connections, or users are reporting failures you can't reproduce on your office WiFi. Before you start rewriting your networking layer, you need to confirm the problem is actually network-related — and then figure out exactly what kind of network condition triggers it.
This guide walks through how to isolate, reproduce, and fix network-related bugs in macOS and iOS apps. The goal is to turn a vague "it doesn't work on bad connections" into a specific, reproducible issue you can fix and verify.
Not every bug that looks network-related is. Before you go down the network debugging path, rule out the more common causes first:
A quick way to check: open your network inspector (Xcode's Instruments, Safari's Web Inspector, or a proxy tool) and watch the actual requests. If the response comes back fine but the UI is wrong, it's not a network bug. If the request never completes, takes too long, or fails — keep reading.
"Slow network" isn't a single condition. Different types of degradation cause different symptoms, and knowing which one you're dealing with points you toward the right fix.
Every request takes longer to start, but once data starts flowing it arrives at normal speed. Symptoms: initial page loads feel slow, API calls take seconds before the first byte, but large downloads complete at a reasonable pace once they begin. Chatty protocols that make many small requests (like a screen that fires 10 API calls on load) suffer the most.
Data flows slowly. Symptoms: large payloads (images, file uploads, big JSON responses) take a long time. Small requests might feel fine. Progressive loading or streaming responses stall partway through. A 5 MB image that loads instantly on WiFi takes 40 seconds on Edge.
Some packets never arrive and need to be retransmitted. Symptoms: intermittent failures, requests that sometimes work and sometimes don't, connections that stall and then resume, TCP retransmission delays that add seconds of unpredictable latency. Packet loss is often the cause when a bug is hard to reproduce — it's inherently random.
The hostname lookup takes longer than expected. Symptoms: the very first request to a new domain is slow, but subsequent requests to the same domain are fine (because the DNS result is cached). If your app talks to multiple domains on a single screen, DNS delays compound. Users on networks with slow or unreliable DNS servers hit this more than you'd expect.
Knowing which condition triggers the bug matters because the fix is different for each. A timeout issue caused by high latency needs a longer timeout or a smarter retry strategy. A rendering bug caused by packet loss needs better handling of partial or interrupted responses. Treating them all as "slow network" leads to vague fixes that don't address the root cause.
Once you know the type of network condition causing the problem, look at how your code handles it. Here are the most common things that break:
The default timeout for URLSession is 60 seconds. That's a long time for a user to stare at a spinner. But setting it too low causes false failures on legitimately slow connections.
// Too aggressive — will fail on 3G
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 5
// More realistic — gives slow connections a chance
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 120 Consider using different timeouts for different request types. A quick metadata fetch can be more aggressive than a large file download.
Not all failures are permanent. A request that fails due to packet loss might succeed on retry. But retrying blindly can make things worse — hammering a slow server with repeated requests isn't going to help.
// Simple exponential backoff
func fetchWithRetry(url: URL, attempt: Int = 0) async throws -> Data {
do {
let (data, _) = try await URLSession.shared.data(from: url)
return data
} catch {
guard attempt < 3 else { throw error }
try await Task.sleep(for: .seconds(pow(2, Double(attempt))))
return try await fetchWithRetry(url: url, attempt: attempt + 1)
}
} Retry on transient errors (timeouts, connection resets). Don't retry on 4xx responses — those won't suddenly start working.
The most common network UX bug isn't a crash — it's a blank screen. If a request takes 8 seconds on a slow connection, does the user see a loading indicator? Does it show up immediately, or only after a delay?
Check that your loading states cover the full duration of the request, including retries. A spinner that disappears after the first attempt fails — while a retry is still in progress — is worse than no spinner at all.
What happens when the network is completely gone? Does the app show a useful error, or does it silently fail? Can the user retry without navigating away and back?
// Check reachability before showing a generic error
if let urlError = error as? URLError {
switch urlError.code {
case .notConnectedToInternet:
showOfflineMessage()
case .timedOut:
showRetryOption()
default:
showGenericError()
}
} Distinguish between "offline," "slow," and "server error." Each deserves a different message and a different recovery path.
A network bug you can't reproduce is a network bug you can't fix — or at least, one you can't verify is fixed. The key is documenting the exact conditions that trigger the problem.
When you file a bug (or document it for yourself), include:
With specific conditions documented, you can apply those exact conditions during development, confirm you see the bug, apply the fix, and verify it's resolved — all without guessing.
To reproduce a network bug, you need to apply the exact conditions that trigger it. There are a few options on macOS:
sudo. There's no GUI and no easy way to switch between profiles.The specific tool matters less than the workflow: identify the condition, apply it, reproduce the bug, fix it, verify the fix under the same condition. If you're doing this regularly — and if you're shipping a networked app, you should be — pick whatever tool has the least friction for your workflow.
You might also find this useful: How to Simulate a Slow Network on Mac for Testing