Duplicate Orders After an API Timeout? Check CDN Origin Retries, Client Resends, and Idempotency
Create Time:2026-11-03 10:33:57
浏览量
1090

Two retry paths carry matching orders toward a database, where an idempotency check accepts only one creation.

A customer submits an order, waits, and sees a timeout. They try again—and discover two orders. The dangerous assumption is that receiving no success response means the server did nothing. The order may already have committed while the response was lost or arrived after a proxy stopped waiting.

Investigate proxy behavior, client retries, and business-level writes together. A CDN adds another layer to check; duplicate requests alone do not prove that the CDN replayed them. The following examples describe troubleshooting and design, not tests performed in your production environment.

Separate a timeout from the operation's outcome

A write request can reach the origin, create an order, commit a transaction, and then send its response. Failures at different stages can look identical to the client: a network error or 504. A disconnected client does not guarantee that a database transaction rolled back. That depends on application, driver, and transaction behavior.

Use business order identifiers, trace identifiers, and server records to establish the result before clicking repeatedly or replaying a production creation endpoint. For payments, reconcile the merchant order with the payment provider's records. A browser error is not sufficient justification for charging again.

HTTP defines idempotent method semantics and restricts automatic retries of non-idempotent requests. POST should not be treated as safe to repeat simply because a network failure occurred. An application can support retries through an explicit idempotency contract.[1]

Use separate operation and attempt identifiers

Keep a stable identifier for one business operation and a distinct request or trace identifier for each transport attempt. Retrying the same order creation preserves the operation identifier but may produce several attempt identifiers. This distinguishes one operation sent three times from three genuinely separate orders.

Logs can include a redacted user identifier, operation identifier, attempt identifier, ingress time, upstream address, status, duration, and resulting order number. Do not put complete Authorization values, cookies, payment details, or request bodies in ordinary access logs. Avoid personal information in operation identifiers too.

Correlate the client, CDN, load balancer, gateway, and application timelines. Multiple origin requests might result from proxy retries, a frontend SDK, mobile reconnection, double-clicking, or repeated job delivery. Duplicate execution by an event consumer can occur after an otherwise normal HTTP interaction.

Check the actual product's retry conditions

CDNs do not all behave alike. Determine whether a connection failure before sending can trigger another attempt, whether a request already sent can be retried, which methods are eligible, which status codes trigger retries, and how the total timeout budget is calculated.

Nginx exposes upstream switching conditions through proxy_next_upstream. Its documentation gives additional rules for non-idempotent methods: POST, LOCK, and PATCH are normally not passed to another upstream once sent, unless non_idempotent is explicitly enabled. Do not add that option merely to reduce visible errors.[2]

CloudFront also distinguishes connection behavior from origin response timeouts and treats methods differently. Its documentation states that POST, PUT, PATCH, DELETE, and OPTIONS requests are not resent after an origin response timeout in the same way that some read requests can be.[3] This does not rule out retries elsewhere or establish identical behavior for every connection-failure scenario.

If a configuration may be enabling replay, export the current rules and compare them with logs before changing anything. Do not disable retries globally without assessing read traffic that relies on failover. Scope changes to relevant write paths and methods, and define rollback conditions.

Preventing double-clicks is only an entry-point safeguard

Disabling the submit button and displaying a processing state can reduce duplicate clicks. It cannot cover refreshes, device changes, SDK retries, or disconnections. When the page recovers, query the existing operation instead of unconditionally creating a new one.

For a single operation, generate and persist a high-entropy identifier before the first attempt. Reuse it for retries. A new key on every retry tells the server that these are different operations. Conversely, a genuinely new order must not accidentally reuse the previous order's key.

Idempotency-Key is a common API convention, not a mechanism that works automatically when added to any request. The backend must implement its meaning and proxies must forward it as configured. Cross-origin browser clients also need the custom header allowed by CORS preflight. Key format, scope, and retention belong in the API contract.

Coordinate deduplication with business writes

A basic design uses a unique combination of tenant or user scope, operation type, and idempotency key. Store a digest of the agreed normalized request fields, processing state, and business result. Reusing a key with different products or amounts should produce the documented conflict behavior, not an unrelated old order.

Concurrency is critical. Two requests must not both observe that no record exists and then each create an order. Use database uniqueness, transactions, or a reliable atomic mechanism to ensure only one attempt acquires processing rights. This is a conceptual flow, not production-ready code:

Determine key scope from the authenticated caller and endpoint
Validate input and compute a digest of agreed request fields
Atomically acquire processing rights for the idempotency record
  Completed, matching digest: return the agreed saved result
  In progress: return processing status or direct the caller to query later
  Same key, different digest: reject reuse
Acquired: coordinate the order write and idempotency result storage
Query and reconcile uncertain external execution results

A separate cache lookup followed by a cache write does not usually prevent races. Expired or lost cache entries can admit another execution. Persist the necessary order-level uniqueness guarantees in business data rather than relying only on a temporary lock. Multi-instance and multi-region deployments need explicit coordination boundaries.

A local database transaction cannot automatically cover an external payment or inventory service. Pass a stable business identifier to downstream services that support idempotency, and provide status queries, compensation, and reconciliation. A transactional outbox can coordinate local data and event publication, but consumers still need to tolerate duplicate messages.

Define which results are retained

Success, validation failure, processing-in-progress, internal failure, and an unknown outcome should not blindly share one replay rule. Specify which states are recorded, what repeat requests receive, and when the client should query status instead of retrying.

Stripe provides a concrete example: its documented idempotency behavior saves the first execution's status code and response body, including some failure results, and checks that repeated parameters match. Validation failures and conflicts with concurrently executing requests have documented exceptions.[4] These are Stripe's semantics, not an automatic property of a custom API.

Retain records long enough for the permitted retry and recovery window, while assessing storage costs. Once a record has been removed, an old request may be treated as new. Persistent business uniqueness and reconciliation remain important. An arbitrary one-day cache does not guarantee that duplicates can never occur.

Recheck caller identity and access rights before returning a saved result. Different users choosing the same key must not see each other's orders. Responses containing personal data should not enter a shared CDN cache. Application idempotency storage and edge response caching are different mechanisms.

Design timeout budgets and retries together

Clients, CDNs, gateways, and downstream services each impose waiting limits. Where possible, let the application return a meaningful outcome before the outer deadline. Alternatively, use a controlled asynchronous operation: return an operation identifier and let the client query progress. Accepted for processing does not mean an order or payment has succeeded.

Extending every timeout increases connection and resource occupancy without eliminating lost responses. Automatic retries need an attempt limit, a total time budget, and backoff, and must stay within the API's retry contract. For writes with an uncertain outcome, querying status is often safer than creating a replacement operation immediately.

Test the failure points that matter

Use test accounts, test orders, and payment sandboxes. Cover requests that never reach the origin, failures before a write, a disconnected response after commit, concurrent requests with one key, mismatched parameters with one key, record cleanup, and uncertain downstream outcomes. Do not inject failures into a production charging endpoint.

Acceptance must inspect actual order counts, payment records, stock changes, and event processing—not just HTTP success. Repeated transmission of one operation should meet the promised single-business-effect contract. Two intentionally separate user operations must not be merged accidentally.

Back up proxy rules and application settings before rollout. Deploy gradually and monitor duplicate orders, processing backlogs, conflicts, and timeout distributions. Reverting a retry setting is relatively straightforward; deleting idempotency records may admit old requests, and removing database constraints may permit new duplicates. Neither is ordinary cache cleanup.

Handle existing duplicates through review and reconciliation rather than bulk deletion. Identify who resent the request, when the original write committed, and why a second execution was accepted. Those findings determine whether the fix belongs in the proxy, the client, or the business write path.

References

Source review date: November 3, 2026. Provider-specific behavior depends on the cited documentation and the deployed version.

  1. RFC 9110: Idempotent Methods

  2. Nginx: proxy_next_upstream

  3. CloudFront: Origin response timeout

  4. Stripe: Idempotent requests