Opening
Here is something that will change how you think about networking: failure is not an edge case. Failure is the normal state of mobile networking. Networks drop packets. Servers go down. Connections time out. Cell towers hand off. Tunnels happen. And if your app does not handle these failures gracefully, your users will notice. They will see spinners that never stop, errors that make no sense, and data that silently disappears. Today we are going to talk about the three pillars of resilient networking: timeouts, retries with exponential backoff, and knowing when to give up. These are not advanced topics. They are fundamental. Every production app needs them, and understanding why they work the way they do will make you a much better engineer.
Timeouts: How Long to Wait Before Giving Up
A timeout is the simplest form of failure handling — you set a maximum wait time, and if nothing happens within that window, you give up. But OkHttp has three different timeouts, and understanding what each one does matters. The connect timeout is how long OkHttp will wait to establish a TCP connection. If the server is unreachable or the network is down, this fires. Default is 10 seconds. The read timeout is how long OkHttp will wait for the next byte of data once the connection is established. If the server is processing a slow query and not sending anything back, this fires. Also 10 seconds by default. The call timeout covers the entire operation — DNS, connection, request, processing, and response. It is the maximum end-to-end duration. By default it is zero, meaning no limit. You should set it. If you do not, a request that connects fine but receives data at a trickle of one byte per second could keep your coroutine tied up for hours.
Choosing the Right Timeout Values
Choosing timeout values is more art than science, but there are good principles. Start with user expectations. For an interactive request — user taps a button and expects a result — a total timeout of 10 to 15 seconds is reasonable. Beyond that, the user has already assumed something is broken. For background work — sync operations, analytics uploads — you can be more generous, maybe 30 to 60 seconds. But do not set timeouts to infinity. That is how you get threads stuck forever. For the connect timeout specifically, 10 seconds is fine for most cases. If you cannot establish a TCP connection in 10 seconds, the server is either down or the network is completely broken. Retrying might help. Waiting longer will not. For read timeouts, consider the API. If you know a particular endpoint is slow — maybe it generates a report — you might increase the read timeout for that specific call. OkHttp lets you create per-request clients with custom timeout values using the newBuilder method, so you can customize without changing your global defaults.
What Happens After a Timeout
When a timeout fires in OkHttp, it throws an exception. A connect timeout throws a SocketTimeoutException. A read timeout also throws a SocketTimeoutException. A call timeout throws an InterruptedIOException. In your coroutine, this means the suspend function throws, and your try-catch block catches it. But here is the important question: what do you do next? Just showing an error message is a terrible experience. The user does not care that a socket timed out. They care that the thing they asked for did not work. A better approach is to retry. But retrying naively — immediately, over and over — is even worse. That is where backoff comes in.
Exponential Backoff
Exponential backoff is a simple but powerful idea. When a request fails, you wait before retrying. And each time it fails again, you wait longer. The pattern typically looks like this: first retry after 1 second. Second retry after 2 seconds. Third retry after 4 seconds. Fourth retry after 8 seconds. Each wait is double the previous one — hence exponential. Why not just retry immediately? Because the failure probably was not random. If the server is overloaded, hammering it with immediate retries makes the problem worse. You are adding load to an already struggling system. By backing off exponentially, you give the server time to recover. And if the failure was a transient network issue — a brief packet loss, a tower handoff — the brief wait often means the next attempt succeeds. The math here is on your side. If each retry has a 50 percent chance of succeeding, three retries give you a 87.5 percent chance of eventual success. Four retries: 93.75 percent. The exponential wait ensures you do not overwhelm anything in the process.
Jitter: Avoiding the Thundering Herd
There is a subtle but critical addition to exponential backoff: jitter. Jitter means adding a random amount of time to each wait. Instead of retrying after exactly 1 second, you retry after a random time between 0 and 1 second. Instead of exactly 4 seconds, somewhere between 0 and 4 seconds. Why? Imagine your backend goes down for 30 seconds. You have 10,000 users, and their apps all detect the failure at roughly the same time. Without jitter, they all retry after 1 second. Ten thousand requests hit the server simultaneously the moment it comes back up. It immediately goes down again. This is called the thundering herd problem, and it is devastatingly common. With jitter, those 10,000 retries are spread randomly across the wait window. Instead of a spike, the server sees a gradual ramp-up. It can handle that. Jitter is one line of code — multiply your backoff delay by a random value between 0 and 1 — but it can be the difference between a graceful recovery and a cascading failure.
Retry Budget: Knowing When to Give Up
Exponential backoff solves the timing problem, but you also need to decide when to stop retrying. Retrying forever is not an option. Your user is staring at a screen, waiting. The standard approach is to set a retry budget — a maximum number of retries. Three to five retries is typical. With exponential backoff, three retries take about 7 seconds total — 1 plus 2 plus 4. Five retries take 31 seconds — 1 plus 2 plus 4 plus 8 plus 16. That is probably the outer limit for interactive operations. You can also set the budget by total time rather than count. "Retry for up to 30 seconds, then give up." This is often more intuitive because it maps directly to user patience. When you exhaust your retry budget, show a meaningful error. "Could not load your feed. Check your connection and pull to refresh." Not "SocketTimeoutException." Give the user an action they can take.
Idempotent vs Non-Idempotent: What is Safe to Retry
Here is a critical question that many developers overlook: is it safe to retry this request? For GET requests, the answer is almost always yes. Getting user data twice produces the same result. No harm done. But for POST requests — creating a resource, making a payment, sending a message — retrying can be dangerous. Imagine a payment endpoint. The request was sent, the server processed it, and the response was lost on its way back. Your app thinks the request failed and retries. Now the user has been charged twice. This is the idempotency problem. GET, HEAD, PUT, and DELETE are generally considered idempotent — repeating them produces the same result. POST is not. If you need to retry POST requests safely, the server needs to support idempotency keys — a unique identifier you send with each request. If the server sees the same key twice, it returns the original response instead of processing the request again. Stripe, for example, does this with an Idempotency-Key header. Your retry logic should be aware of HTTP methods. Retry GETs freely. Retry POSTs only with idempotency guarantees.
Implementing Retries with OkHttp Interceptors
OkHttp's interceptor chain is the perfect place to implement retry logic. You can create an interceptor that catches failures and retries with backoff. The structure is straightforward. You intercept the request. You try to execute it. If it throws an IOException — timeout, connection reset, whatever — you check whether the request is retryable, calculate the backoff delay, sleep, and try again. You keep a counter and stop when you hit your retry budget. The beauty of doing this in an interceptor is that it applies to all requests automatically. You can also make it configurable — different retry budgets for different endpoints, different backoff strategies. OkHttp actually has some built-in retry logic — it will transparently retry on connection failures and redirect responses. But it does not do exponential backoff. For that, you need your own interceptor. The key implementation detail: use Thread.sleep in the interceptor since it runs on OkHttp's thread pool, not your coroutine dispatcher. Do not use delay — you are not in a coroutine context inside the interceptor.
The Circuit Breaker Pattern
There is one more pattern worth knowing: the circuit breaker. It comes from electrical engineering — when too much current flows, the breaker trips and cuts the circuit, preventing damage. In software, a circuit breaker monitors failures. When the failure rate exceeds a threshold — say, 50 percent of requests to a particular server fail in the last minute — the circuit breaker "opens." While open, it immediately fails all requests without even trying. No network calls, no waiting for timeouts. This sounds harsh, but it serves two purposes. First, it protects the server. If it is already struggling, sending more requests makes things worse. Second, it protects your user. Instead of waiting 10 seconds for a timeout on every request, they get an instant failure and can try again later. After a cooldown period, the circuit breaker enters a "half-open" state — it lets one request through to test if the server has recovered. If it succeeds, the breaker closes and normal traffic resumes. If it fails, the breaker opens again. While Android apps do not typically implement full circuit breakers, the concept is powerful. Even a simple "if the last three requests to this endpoint failed, show cached data instead" is a mini circuit breaker.
Putting It All Together
Let me give you the complete picture of resilient networking. Set appropriate timeouts — connect, read, and call — so you never wait forever. When a request fails, retry with exponential backoff and jitter. Limit retries with a budget — either by count or total time. Only retry idempotent operations, or use idempotency keys for non-idempotent ones. Consider a circuit breaker for persistent failures. And always, always have a fallback — cached data, a meaningful error message, an offline mode. This is not optional complexity. This is what separates a professional Android app from a toy. Your users are on trains, in elevators, in basements, in countries with unreliable infrastructure. They deserve an app that handles failure as gracefully as it handles success.
Recap and What is Next
Let us recap. Timeouts prevent infinite waits — use connect, read, and call timeouts together. Exponential backoff spaces out retries: 1, 2, 4, 8 seconds. Jitter adds randomness to prevent thundering herds. A retry budget limits total attempts — three to five is typical. Idempotency determines what is safe to retry — GETs yes, POSTs only with keys. OkHttp interceptors are the right place to implement retry logic. And circuit breakers protect against sustained failures. In the next video, we are going to shift gears and talk about what happens to data once it arrives — serialization. How your app converts JSON text into Kotlin objects, why it is more expensive than you think, and when it becomes a bottleneck. See you there.