Here is the truth nobody wants to hear: your network is lying to you. It promises delivery, ordering, and speed. It delivers dropped packets, latency spikes, and partitions. Most software written in the last decade assumes the opposite, and that assumption costs real money, real time, and real sanity.

The Network Is Not a Pipe
Peter Deutsch and his colleagues at Sun Microsystems laid out the Fallacies of Distributed Computing decades ago. The first one â “the network is reliable” â remains the most ignored lesson in software engineering. Developers still write code as if send() guarantees delivery. It does not. The network is a best-effort system, and your code needs to accept that reality from the start.
Consider what actually happens when a service calls another service over HTTP. The packet leaves the application, hits the kernel, traverses the NIC, goes through switches, routers, load balancers, possibly a CDN, and then reverses the journey on the response. Any of those hops can silently drop your data. Any of those devices can introduce 10ms or 10000ms of latency with zero warning. TCP retransmissions will paper over some of this, but at a cost your metrics barely capture.
What “Unreliable” Actually Means
Unreliable does not mean “down.” That would be simple. Unreliable means:
- Partial delivery: Some packets arrive. Others do not. The connection appears alive but is functionally broken.
- Reordering: Packets arrive out of sequence. Your application-level protocol may not handle this.
- Duplication: Retransmissions cause the same data to arrive more than once. If your handler is not idempotent, you double-charge, double-create, or double-delete.
- Partition: Two parts of your system cannot talk to each other, but each part works fine internally. This is the hardest case and the one most architectures pretend cannot happen.
If you have not designed for all four of these failure modes, you have not designed for production. You have designed for your local machine with Docker Compose and a fast loopback interface. That is not the same thing.
Timeouts: Your First and Worst Defense
Every network call needs a timeout. This is non-negotiable. But most timeout values are garbage â picked because someone saw a blog post recommending 30 seconds, or because the default in the HTTP client was 60 seconds and nobody changed it.
A timeout should be derived from your service-level objectives. If your p99 latency target is 200ms, a 30-second timeout is not “generous.” It is irresponsible. By the time 30 seconds pass, your caller has already failed or timed out themselves. You have consumed a thread, a connection, and memory for nothing. You have made the outage worse.
Set timeouts at every layer. Database queries, HTTP calls, gRPC stubs, message queue publishes â all of them. And make those timeouts configurable at runtime, because the right value today is the wrong value tomorrow when traffic patterns shift.
The Danger of Single Timeout Values
A single timeout conflates two different concerns: how long you are willing to wait, and how long the operation should take. These are not the same. You should track the actual latency distribution of your dependencies and set timeouts relative to that distribution. If a downstream service normally responds in 50ms, a 5-second timeout is absurd. Set it at 500ms, instrument the violations, and respond when the violations increase.

Retries: The Failure Amplifier
Retries feel like a safety net. They are actually a loaded weapon pointed at your own system. Here is the pattern that kills production services every week:
- A downstream service starts responding slowly (not failing, just slow).
- Every caller’s timeout fires.
- Every caller retries.
- The downstream service, already struggling, now receives 2x or 3x its normal load.
- It gets slower. More timeouts fire. More retries launch.
- The service collapses under retry amplification.
This is not theoretical. It has happened at every scale, at every major company, repeatedly. The fix is not “retry less.” The fix is a set of disciplined practices.
Exponential Backoff with Jitter
Retrying immediately is wasteful. Retrying at fixed intervals creates thundering herds. The correct approach is exponential backoff with random jitter. Wait 100ms, then 200ms, then 400ms, with a random ±50% on each interval. This spreads retries across time and avoids synchronized retry storms.
But even with good backoff, you must cap the number of retries. If an operation has failed three times, it is probably not going to succeed on the fourth. Fail fast, report the error, let the calling context decide what to do. Blind retries are a denial-of-service tool aimed at yourself.
Idempotency: Not Optional
If your API endpoints are not idempotent, retries will corrupt your data. This is a guarantee, not a risk. A retry is a re-execution of a request the caller believes might not have been processed. If processing it twice changes state in a way that processing it once does not, you have a bug.
Idempotency keys are the standard solution. The client generates a unique key per operation and sends it with the request. The server checks whether it has already processed that key. If yes, it returns the previous result. If no, it processes the request and records the key.
This requires storage â a cache, a database table, something. Yes, it adds complexity. No, you cannot skip it. The alternative is explaining to your finance team why a customer was charged three times for one purchase because the payment gateway was slow and your code retried twice.
Circuit Breakers: Stop Hitting the Broken Thing
A circuit breaker is a state machine with three states: closed, open, and half-open. In closed state, requests flow normally. When failures exceed a threshold, the breaker opens and all requests fail immediately without hitting the downstream service. After a timeout, the breaker enters half-open and lets a small number of requests through. If they succeed, the breaker closes. If they fail, it opens again.
This pattern exists because hammering a failing service does not help anyone. When a dependency is down, the fastest thing you can do is fail immediately. Your caller gets a fast failure, can degrade gracefully, and the downstream service gets a chance to recover because you stopped sending it traffic.
Implement circuit breakers at every boundary where your code calls an external system. Database connections, HTTP APIs, message queues â all of them. Libraries like Hystrix (now in maintenance mode), Resilience4j, or Polly make this straightforward. There is no excuse for not using them.

The CAP Theorem Is Not a Suggestion
Brewer’s theorem states that a distributed system can provide at most two of three guarantees: Consistency, Availability, and Partition tolerance. Since partitions are a reality of networks, you are choosing between consistency and availability when a partition occurs.
Most engineers nod at this and then build systems that try to provide both. This does not work. You must decide, in advance, what your system does during a partition. Do you refuse writes to maintain consistency? Do you accept writes and reconcile later? This decision should be explicit, documented, and tested â not discovered at 2 AM during an outage.
Testing Partitions
Speaking of testing: if you are not injecting network failures into your test and staging environments, you are not testing your network code. Tools like Chaos Monkey, Toxiproxy, and tc (traffic control on Linux) let you simulate packet loss, latency, and partitions. Use them. Run them in CI. Break your system on purpose and verify that it degrades the way you designed it to.
If your first network partition happens in production, you are running an untested system. That is not engineering. That is hope.
Observability: You Cannot Fix What You Cannot See
When a network issue hits production, you need answers fast. Which service is slow? Which requests are failing? Is it a timeout, a circuit breaker trip, or a retry storm? Without proper instrumentation, you are guessing.
Every outbound network call should emit metrics for: latency (p50, p95, p99), error rate, timeout count, and circuit breaker state. Trace IDs should propagate across service boundaries so you can follow a request from ingress to database and back. Logs should contain enough context to diagnose a failure without reproducing it.
This is not gold-plating. This is the minimum bar for operating distributed software. If you cannot answer “which dependency is causing the timeouts?” in under 60 seconds, your observability is insufficient.
Practical Checklist
Here is what I expect from any service that makes network calls:
- Timeouts at every layer, derived from SLOs, configurable at runtime.
- Retries with exponential backoff and jitter, capped at a reasonable count.
- Idempotency keys on all state-mutating endpoints.
- Circuit breakers on all outbound calls to external systems.
- Bulkheads â isolate resources per dependency so one slow service cannot consume all your threads or connections.
- Failure injection tests in CI and staging.
- Metrics, traces, and structured logs for every network call.
Missing any of these? You have a gap that will become a production incident. The only question is when.
FAQ
What if my downstream service does not support idempotency keys?
Implement idempotency on your side. Before calling the downstream service, record the idempotency key and the intended operation in your own database. After the call completes, record the result. If a retry is needed, check your database first. This adds latency and complexity, but it is the only way to guarantee correctness when the downstream system cannot help you.
How do I choose timeout values when I do not have latency data yet?
Start conservative. Set timeouts low â 2x or 3x the expected latency â and instrument aggressively. Let the metrics tell you what the real distribution looks like, then adjust. A too-short timeout that causes visible errors is better than a too-long timeout that silently consumes resources. You can always relax a timeout. You cannot always recover the time you wasted waiting.
Are message queues a substitute for handling network failures directly?
No. Message queues shift the failure point, they do not eliminate it. Your producer must still write to the queue over the network. Your consumer must still process the message and call other services over the network. Queues add a durability layer and decouple timing, which helps â but they introduce their own failure modes (backlog, poison messages, ordering issues). You still need timeouts, retries, circuit breakers, and idempotency at every network boundary, including the one between your service and the queue.
There is no shortcut. The network is unreliable, and your software must be built for that reality from the first commit, not patched after the first outage.











