DEV Community

Cover image for Your API Timed Out. Did the Payment Still Go Through?
Robert Adamson
Robert Adamson

Posted on

Your API Timed Out. Did the Payment Still Go Through?

A customer clicks Pay.

The screen keeps loading. Then an error appears:

“Request timed out. Please try again.”

They click again.

Now there are two requests—and potentially two charges.

Disabling the button helps with accidental clicks. But what happens when the network fails, the page reloads, or a background job retries?

Every developer building payments, bookings, or orders should understand this problem.

1. A timeout doesn’t mean the operation failed

Imagine this sequence:

  1. Your server receives a payment request.
  2. The payment provider processes it.
  3. The response gets lost.
  4. Your server reports a timeout.

The customer may already have been charged.

A timeout means you didn’t receive a response in time. The outcome may still be unknown. AWS explicitly highlights this distinction in its guidance on retries.

Immediately starting a new payment could repeat an action that already succeeded.

2. Give each operation an identity

This is where idempotency helps.

In plain English:

Repeating the same operation should have the same intended effect as performing it once.

An idempotency key identifies a particular operation:

POST /payments
Idempotency-Key: 8d5f8ba2-41c9-4a82-a4a2-820897f483f1
Enter fullscreen mode Exit fullscreen mode

If that request times out, retry the same operation with the same key.

Generate a new key for a genuinely new operation.

The key is an identifier. Your backend—or the API you call—must implement the behavior that makes retries safe.

3. Handle duplicates on the server

A typical implementation needs to:

  • Associate the key with the authenticated customer and operation.
  • Store a fingerprint of the request parameters.
  • Claim the key atomically so simultaneous requests cannot both start the work.
  • Track whether the operation is processing or completed.
  • Return the stored outcome when a completed operation is retried.

If the same key arrives with different parameters, reject it.

If the original operation is still processing, return a documented processing response or conflict instead of starting it again.

A “check whether it exists, then insert” sequence alone can race. Use database constraints and appropriate transactions to protect local changes.

4. Protect the payment provider call too

Your database transaction cannot roll back a charge made by an external provider.

Suppose the provider processes the payment, but your server crashes before saving the result. Your local record may still say “processing.”

Use the provider’s idempotency support with a stable key for that payment attempt. Stripe, for example, supports retrying requests using the same idempotency key. Its exact behavior and retention rules are documented in its API reference.

Also provide a recovery path: retrieve the payment’s status or reconcile it through verified webhook events.

While the outcome is uncertain, show “Checking payment status” instead of inviting the customer to start another payment.

5. Retry carefully

Even a safe retry can add load to a struggling service.

For retryable failures:

  • Limit the number of attempts.
  • Increase the delay between attempts.
  • Add randomness to spread requests out.
  • Follow the provider’s retry guidance.

This is called exponential backoff with jitter. It helps prevent many clients from retrying at the same moment.

Don’t blindly retry every error. Invalid input needs correction.

6. Test the uncomfortable cases

Before shipping, test:

  • Two simultaneous requests with the same key.
  • A successful operation whose response is lost.
  • A server crash after the provider succeeds.
  • The same key with a different amount.
  • A repeated webhook event.

For a payment, verify that retries produce one intended charge and a consistent local record.

Apply the same thinking to orders, reservations, credit deductions, and other actions that should not happen twice.

The question to ask before adding a retry

“If the first attempt succeeded but I never received the response, what will this second attempt do?”

If the answer is “charge again,” “book again,” or “deduct again,” the retry needs more work.

A reliable API needs a clear answer for both success and uncertainty.

Have you ever dealt with a duplicate payment, order, or background job? What caused it?

Top comments (0)