All posts
API designidempotencytimeoutsoperations

Put the operation key in the timeout response

A timeout can leave a remote command unresolved. Preserve the original operation identity so the caller can reconcile safely

A command reaches a service and the client waits for a reply. The wait ends. There is no response body, only a timeout. The service may never have seen the command. It may have completed the command and lost the reply. The caller needs a way to ask what happened without accidentally creating a second operation.

This is where an operation key earns its place in the public contract. If the caller supplied a stable idempotency key, the timeout response and client error should preserve it. An error that drops the key leaves the caller with the hardest state and the least useful handle.

Show the handle the caller already owns

Imagine a request to create a dispatch. The client sends an idempotency key with the command, then times out after the server has accepted the work. A useful client result can say that the outcome is unknown, identify the original key, and point to the permitted reconciliation path. It should not announce failure merely because the local timer expired.

The client must not invent a new key for a blind retry. A new key describes a new command to the server. If the first command succeeded, the second may duplicate it. Repeating with the original key can be safe only when the server binds that key to the same validated request and returns the first outcome or a stable in-progress state.

The key alone is not enough if the client cannot tell which endpoint and request it represents. A compact local record should keep the operation type, canonical request identity, key, first send time, and last known response class. It can exclude sensitive payload fields while retaining enough information to reconcile the command. The record should say unknown when that is all the evidence supports.

Keep the error contract narrow

Transport libraries often return a generic timeout exception. Application code then catches it and shows Try again. That button is a product decision made without an outcome model. It may be harmless for a read. It may be unsafe for a command that sends a message, creates a record, or charges an account.

A client library can instead expose three bounded facts: the request did not receive a final response, the original operation key is available, and a retry or lookup must use that same identity under the server's documented rules. This does not promise that the server completed the command. It gives the caller an honest next action.

Do not include the key in unrestricted telemetry if it can be linked to private payloads or account activity. Operational traces can use an appropriate redacted reference or access-controlled record. The public error shown to a user may need a support reference rather than the raw key. The important property is continuity of identity across the client, server, and support path.

Make reconciliation possible

The server side needs a defined answer for repeated keys. A completed command should return its original result or a stable reference. A command still in progress should say so. A key reused with a different request should be rejected rather than quietly attached to the new payload. Those states make the timeout handle meaningful.

An explicit status lookup can be better than repeatedly issuing the command, particularly when side effects are expensive. The lookup must itself be scoped to an authorised caller and the original operation identity. It should not expose another tenant's activity to anyone who guesses a key.

Retention matters too. If the server forgets keys after a defined window, the client should know that limit. A key that has expired cannot be treated as permanent protection against duplicates. At that point, the caller may need a manual review or a separate business identifier to establish whether the work already happened.

Test the lost reply, not only the failed send

The decisive test is awkward by design. Let the server accept and complete a command, then drop the response before it reaches the client. The client should return an unknown outcome with the original key. A lookup or retry under that key should recover the original result without creating a second dispatch.

Also test a timeout before the command is sent, a repeated key with a changed payload, and a retry after the retention window. Each case has a different answer. If the client collapses them into the same Try again action, the contract has hidden a real decision from its callers.

Stack Dispatch handles work that crosses service boundaries. We do not need to make a timeout look clean. We need to keep the operation identifiable while its outcome is checked. Put the key in the error path, preserve what the caller knows, and make the next request refer to the original command.

0 comments

Join the conversation

Get the next dispatch

New writing on software architecture, AI systems, and shipping production software, sent by email. Unsubscribe anytime.