A timeout tells a caller that a response did not arrive within its waiting period. It does not prove that the remote operation failed. The request may never have reached the server, may still be executing, may have committed while the response was lost, or may have failed in a way the caller never received.
That uncertainty is central to distributed systems. Reliable integrations need deadlines, selective and bounded retries, idempotent operation handling, and reconciliation for outcomes that remain unknown.
A hypothetical payment sequence
Consider a hypothetical ERP integrating with a payment service. No timings or availability claims are implied by this example.
- The ERP creates local payment attempt
pay_1042with statuspending. - It sends
POST /paymentswith idempotency keypay_1042. - The provider validates the request and commits the payment.
- The response is lost because the connection closes.
- The ERP reaches its timeout.
At step five, the ERP has an uncertain outcome. Marking the payment failed would be inaccurate. Sending a new payment with a new key could duplicate the charge. Treating the timeout as success could record a payment that never happened.
The local state should represent reality: pending or unknown, not an invented conclusion. The next action is to retrieve the result by idempotency key or provider reference, retry safely with the same key if the contract supports it, or reconcile later.
Design a deadline, not isolated timeouts
A caller usually has a total amount of time in which the operation remains useful. That deadline should constrain connection establishment, request execution, retries, and any fallback. If each layer independently applies a generous timeout, the end-to-end request can outlive the user’s patience or the upstream deadline.
Set timeout values from the dependency’s observed behavior and the workflow’s requirements. Include connection setup, TLS negotiation, DNS behavior, queueing, and response reading where relevant. A timeout copied from another system is not evidence.
Propagate the remaining deadline where protocols and libraries support it. Before a retry, confirm that another attempt can finish within the remaining budget. If not, return or persist an honest uncertain state rather than starting work whose result cannot be observed.
Timeouts also protect resources. Without them, blocked calls can consume threads, sockets, connection-pool entries, or memory while the dependency is degraded. The timeout must still allow expected slow-but-valid work; setting it too aggressively can create retries and make an otherwise manageable slowdown worse.
Retry only when the failure and operation permit it
Retries help with transient failures, such as a dropped connection before work begins or a brief service unavailability. They do not repair validation errors, authorization failures, or permanent business rejection. Define retryable conditions explicitly.
Use a small attempt limit, exponential backoff, and jitter. Backoff reduces repeated pressure; jitter prevents many clients from retrying in lockstep. Respect a valid Retry-After response when the contract defines it. A retry policy should also fit the caller’s deadline.
Count retries across layers. If the browser, gateway, application, SDK, and worker each retry independently, one request can multiply into many downstream attempts. Choose the layer with enough context to decide whether retry is safe and useful.
Circuit breakers can limit calls to a repeatedly failing dependency, but they add state and recovery behavior of their own. They are appropriate when supported by evidence and operating needs, not mandatory decoration for every remote call.
Make repeated side effects safe
Idempotency means repeating an operation under the defined contract has the same intended effect as performing it once. HTTP defines methods such as PUT and DELETE as idempotent at the semantic level, but application behavior and side effects still require careful implementation. POST-based business operations often need an explicit idempotency mechanism.
For the hypothetical payment:
- the caller generates a stable key for one logical payment attempt;
- every retry of that attempt uses the same key;
- the receiver stores the key, request identity, status, and eventual result atomically with the operation;
- a repeated request returns the existing result instead of creating another payment;
- reuse of the same key with conflicting request data is rejected.
The key needs scope. Is it unique per tenant, account, or provider? It needs a retention period long enough to cover retries and reconciliation. Expiring the record too soon can make an old retry unsafe; retaining it forever has storage and privacy implications.
Storage atomicity matters. If the side effect commits but the idempotency record does not, duplication remains possible. The implementation may use one database transaction, a provider-supported primitive, or a workflow that can reconcile the gap. Simply accepting an Idempotency-Key header does not make the operation idempotent.
Not every effect can be made perfectly repeatable. Sending an email or invoking a legacy provider may lack a deduplication contract. In those cases, isolate the effect, record attempts, use stable references where possible, and make residual uncertainty visible.
Separate transport status from business status
An HTTP response describes the request as observed by one server at one moment. A 202 Accepted means work was accepted, not completed. A 500 may occur before or after a durable write. A client-side timeout gives no remote business status at all.
Model long-running or failure-prone operations as resources when appropriate:
POST /payment-attempts -> 202 Accepted, attempt id
GET /payment-attempts/:id -> pending | succeeded | failed | unknown
This gives callers a stable way to query progress. Webhooks or events can provide notification, but polling or reconciliation is still useful when notifications can be delayed, duplicated, or lost.
Reconcile what automation cannot prove
A reconciliation process compares local attempts with remote records using stable identifiers. It can move confirmed operations to succeeded, confirmed rejections to failed, and unresolved cases to an operator queue. It should be safe to rerun and should record why a transition occurred.
Operators need enough context to act: local key, remote reference if known, amount and currency where applicable, timestamps, attempt history, and the last observed error. Sensitive data should be limited and protected. Manual actions should be audited, especially if an operator can retry or mark an outcome.
Alerts should distinguish an elevated uncertain-outcome rate from a normal pending backlog. The correct threshold depends on workload and support expectations; derive it from observed behavior rather than inventing a universal number.
These concerns belong in the architecture contract, not only in client code. An architecture review can trace deadlines, retry ownership, side effects, and reconciliation across service boundaries.
Networks delay, drop, duplicate, and reorder communication. A robust design does not pretend those conditions disappear. It limits waiting, retries deliberately, gives repeated work a stable identity, and preserves uncertainty until evidence resolves it.