Opening

Every time your app makes a network call, it is placing a bet. It is betting that the server will respond within a reasonable time. But what if the server does not respond? Without a timeout, your app will wait indefinitely. The coroutine sits there, the thread is occupied, the user stares at a spinner, and nothing happens. Ever. Timeouts and retries are two of the most important reliability patterns in software, and getting them right is surprisingly nuanced. Too aggressive and you create false failures. Too lenient and you create terrible user experiences. Today we are going to nail down exactly how to think about them.

Why Every External Call Needs a Timeout

Here is a rule that has no exceptions: every external call must have a timeout. Network requests. Database queries. File operations. Bluetooth communication. If it leaves your process, it needs a timeout. Without a timeout, a single hung server can bring down your entire app. Imagine your login API stops responding. Without a timeout, the login coroutine waits forever. The user taps the login button again — another coroutine, another forever wait. And again. And again. Each one holds resources. Eventually the app becomes unresponsive. With a timeout, the call fails after a defined period, the user sees an error message, and the app remains functional. A timeout is not just a performance optimization — it is a safety mechanism that prevents cascading failures.

Types of Timeouts

Not all timeouts are equal. OkHttp, which is the standard HTTP client for Android, defines three distinct timeouts, and understanding each one matters. Connect timeout: how long to wait to establish a TCP connection. This is the time to reach the server, before any data is exchanged. If the server is down or unreachable, this is the timeout that fires. Default: ten seconds. Read timeout: how long to wait for data once the connection is established. The connection is open, the request was sent, but the server is slow to respond. Default: ten seconds. Write timeout: how long to wait when sending data to the server — uploading a large request body, for example. Default: ten seconds. Call timeout: the total end-to-end time for the entire request, including connection, writing, server processing, and reading the response. Default: zero, meaning no total limit. I strongly recommend setting a call timeout of thirty to sixty seconds for most API calls.

Choosing the Right Timeout Values

How do you choose timeout values? Start with your user experience requirements. If the user is staring at a loading screen, how long are they willing to wait? Research suggests that after three seconds, users start to feel the delay. After ten seconds, attention drops significantly. So your total call timeout should align with what your UX can tolerate. For most API calls: connect timeout of ten seconds is fine — if you cannot reach the server in ten seconds, something is seriously wrong. Read timeout of thirty seconds gives the server time to process complex requests. Call timeout of sixty seconds as an absolute upper bound. For background sync that the user is not waiting on, you can be more generous — ninety seconds, even two minutes. For real-time features like chat, you want shorter timeouts — five to ten seconds total — because staleness is worse than failure. The key is: different operations deserve different timeouts based on their user-facing impact.

The Retry Decision

When a call fails, should you retry? The answer is not always yes. It depends on two things: is the failure transient, and is the operation safe to repeat? Transient failures are temporary: a network glitch, a momentary server overload, a DNS resolution delay. Retrying a transient failure often succeeds. Permanent failures are not going away: a 404, an invalid request, an authentication failure. Retrying these wastes time and resources — the result will be the same. The second question — is the operation safe to repeat — is about idempotency. We will cover this deeply in the next video. For now, the rule is: only retry operations where repeating them does not cause side effects. A GET request is safe to retry. A POST that creates a payment is not — unless you have idempotency protection.

Exponential Backoff

When you do retry, do not retry immediately. If the server is overloaded and you retry instantly, you are adding to the overload. Instead, use exponential backoff. First retry after one second. Second retry after two seconds. Third retry after four seconds. Each wait is double the previous one. This gives the server breathing room to recover. But pure exponential backoff has a problem: if a thousand clients all start backing off at the same time, they all retry at the same synchronized moments — one second, two seconds, four seconds — creating periodic spikes of traffic. The fix is jitter. Add a random component to each delay. Instead of retrying after exactly two seconds, retry after two seconds plus a random amount between zero and one second. This spreads the retries over time and prevents synchronized thundering herds.

Retry Budget

Even with exponential backoff, you need a maximum retry count. Without it, a permanent failure that looks transient will be retried forever. The standard approach is a retry budget: maximum three retries, then give up and surface the error to the user. Three retries with exponential backoff means the total time is roughly: first try, wait one second, second try, wait two seconds, third try, wait four seconds, final try. That is about seven seconds of waiting plus the request times. This is usually within the user's patience threshold. If three retries with increasing delays do not resolve the issue, more retries are unlikely to help. Surface the error, let the user decide whether to try again, and move on.

Circuit Breaker

Retries handle transient failures well. But what about sustained failures? If a service is down for minutes or hours, retrying every request three times is wasteful. You are tripling your network traffic for no benefit. A circuit breaker addresses this. The pattern has three states. Closed: normal operation, requests go through. Open: after a threshold of consecutive failures (say, five in a row), stop sending requests entirely. All requests fail immediately without making a network call. Half-open: after a cool-down period (say, thirty seconds), allow one request through. If it succeeds, close the circuit — the service is back. If it fails, open the circuit again. Circuit breakers are more common in backend services, but they are useful on mobile too. If your sync service has failed five times in a row, stop hammering it. Wait thirty seconds, try one probe request, and go from there. Your battery and your network will thank you.

OkHttp Interceptor Example

Let me show you how to implement retries with OkHttp. You create an Interceptor that wraps the chain.proceed call in a retry loop. The interceptor catches IOException — which covers network errors — and retries up to a maximum count. Between retries, it delays with exponential backoff and jitter. Here is the shape of the code. You initialize a retry count at zero and a max retries at three. In a loop, you try chain dot proceed. If it throws an IOException and you have retries left, you calculate the delay — two to the power of retry count, times one thousand, plus a random jitter — and you delay for that duration, then increment and try again. If you exhaust retries, you throw the last exception. This interceptor applies to every request going through that OkHttpClient, giving you automatic retry behavior for all network calls.

Coroutine-Level Timeouts

Beyond OkHttp timeouts, you can also set timeouts at the coroutine level using withTimeout or withTimeoutOrNull. This is useful when you want a timeout that covers more than just the network call — maybe you want a timeout for the entire operation including local processing. withTimeout throws a TimeoutCancellationException if the block does not complete in time. withTimeoutOrNull returns null instead of throwing, which is often more convenient. A pattern I like is combining both: OkHttp timeouts protect against slow network calls, and coroutine timeouts protect against the overall operation taking too long. The coroutine timeout should be longer than the OkHttp timeout — it is the outer safety net.

What NOT to Retry

Let me be explicit about what you should not retry. HTTP 400 Bad Request: your request is malformed. Retrying will give the same error. HTTP 401 Unauthorized: your token is invalid. Retry after refreshing the token, not the same request. HTTP 403 Forbidden: you do not have permission. Retrying will not grant permission. HTTP 404 Not Found: the resource does not exist. Retrying will not create it. HTTP 422 Unprocessable Entity: the request data is invalid. Fix the data, do not retry. HTTP 429 Too Many Requests: you are being rate-limited. Retry, but respect the Retry-After header. HTTP 500 and 503: server error — these are good candidates for retry with backoff. The general rule: retry 5xx errors and network failures. Do not retry 4xx errors except for 429 with the Retry-After header and 401 with token refresh.

Closing

Here is the framework I want you to carry forward. Every external call needs a timeout — no exceptions. Set timeouts based on user experience expectations, not arbitrary numbers. When a call fails, ask: is it transient? Is it safe to retry? Use exponential backoff with jitter to avoid thundering herds. Set a retry budget of three attempts maximum. Consider a circuit breaker for sustained failures. And know which errors are worth retrying and which are not. These patterns — timeouts, retries, backoff, jitter, circuit breakers — are the building blocks of resilient software. They are not glamorous, but they are the difference between an app that works on real networks and an app that only works on perfect WiFi. Next, we are tackling idempotency — why repeating an action must be safe, and how to make it so.