DEV Community

Payneteasy
Payneteasy

Posted on

A client timeout is not a decline

Our HTTP client timeout for the charge call was 10 seconds. The gateway's upstream timeout to the issuer was 30. Most auths come back in 1-2 seconds, so nobody noticed the gap.

Then one issuer's processor started taking 12-15 seconds on a bad afternoon. Our client gave up at 10, logged "payment failed", and showed the customer a retry button. The auth was still in flight and succeeded at second 13. The customer clicked retry, the new attempt got a fresh order ID, and we ended up with two approved authorizations on one cart. A cleanup job voided one of them hours later, but the cardholder's available balance stayed held twice for days.

The mistake was treating a timeout as a decline. A timeout is a third state: unknown. The fix was small:

  • On timeout, mark the payment pending_unknown, not failed
  • Poll the gateway by our merchant order ID for up to 60 seconds before allowing a new attempt
  • Reuse the same order ID and idempotency key on retry, so the gateway returns the original result
  • Set the client timeout above the gateway's upstream timeout, not below it

Duplicate holds on slow-issuer days dropped to zero over the next two weeks.

How do you handle the unknown state in your checkout? Do you block the retry button, or let the idempotency key sort it out?

Top comments (1)

Collapse
 
fintechrailsdigest profile image
Fintech Rails Digest •

The double-hold on the cardholder's balance is the detail that makes this worth writing down, because it turns an engineering mistake into a support ticket and sometimes a chargeback. We landed on the same third state eventually, but the hard part was not the code, it was the UI: 'pending_unknown' is an awkward thing to show a customer, and the temptation to render it as failed is exactly what produced the retry button in the first place. Setting the client timeout above the gateway's upstream timeout is the cheap half of the fix and worth doing even before any polling exists.