A request to another service times out. Should the application try again? For a read, the answer is often straightforward. For an action that creates something, sends something or moves a workflow forward, the timeout leaves an uncomfortable gap: the remote service may have completed the work before the response was lost.
Reliable retry behavior starts by acknowledging that gap. “We did not receive a response” and “nothing happened” are different statements.
Classify the action
Before adding a retry loop, record what the operation does and what makes repeating it safe. Reading an invoice is different from creating one. Sending an email has a visible side effect even if the stored data barely changes.
Some APIs support an idempotency key that identifies one intended operation across repeated requests. When supported, the application should preserve that key for retries of the same action. Generating a new key on every attempt defeats the purpose. Other integrations require a lookup or reconciliation step to discover what actually happened.
The important property is the provider’s documented behavior. An HTTP method alone is not enough to establish the full contract of a business operation.
Bound the uncertainty
Retry policies need a maximum effort, a delay strategy and a way to stop. Repeated immediate requests can make a struggling service’s situation worse. A rate-limit response may provide a Retry-After value that should inform the decision.
In the go-invoice-ninja codebase, retry handling distinguishes methods and rate-limit responses rather than treating every failure equally. Its changelog also distinguishes unreleased adjustments from earlier versions. Applications should review the behavior of the version they actually use, particularly around operations with side effects.
Give people a useful state
The interface matters too. If an operation’s outcome is unknown, inviting someone to press the same button repeatedly can create more uncertainty. A processing state, an operation reference and a reconciliation path are more useful than an optimistic success message or a generic failure.
An integration test should cover the awkward case: the remote service accepts the action, then the connection drops. Verify that the next step does not blindly create a second operation. Also test cancellation, exhausted retries and a response that asks the client to wait longer than it is prepared to wait.
The aim is not to eliminate all failures. It is to preserve the meaning of a person’s original action while the network behaves imperfectly. A retry is part of that meaning, not merely a convenience around an HTTP client.