Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[Confoo 2026] Avoiding Déjà Vu - Building Res...

Avatar for Paul Conroy Paul Conroy
February 25, 2026

[Confoo 2026] Avoiding Déjà Vu - Building Resilient APIs with Idempotency

Repeated API requests happen more often than we'd like - double clicks, flaky networks, retries, and proxies can all replay calls unexpectedly. Without idempotency, this can lead to duplicate charges, broken state, and confused users.

This talk explores what really goes wrong when idempotency is missing, how companies like Stripe and Shopify design for retries in practice, and why implementing idempotency is often far simpler than we expect!

Avatar for Paul Conroy

Paul Conroy

February 25, 2026
Tweet

More Decks by Paul Conroy

Other Decks in Technology

Transcript

  1. From Dublin, Ireland Started playing with the web 30+ years

    ago (Notepad, Frontpage & Geocities!) CTO at Square1 conroyp.com / @conroyp Paul Conroy 👴 🌍 🇮🇪
  2. 💸 Double charges 📦 Duplicate orders 🐛 Data integrity issues

    How does Déjà Vu affect our API servers? 🤬 Angry customers
  3. 💸 Double charges 📦 Duplicate orders 🐛 Data integrity issues

    How does Déjà Vu affect our API servers? 🤬 Angry customers 🗣 Higher support costs
  4. Reasons for retries • Impatient users • Mobile apps retrying

    failed connections • Load balancer failovers • CDN fallbacks
  5. 💸 Double charges 📦 Duplicate orders 🐛 Data integrity issues

    🤬 Angry customers 🗣 Higher support costs Consequences of missing idempotency
  6. Naturally idempotent: • GET • HEAD • OPTIONS Non-idempotent verbs:

    • POST • PATCH HTTP Verbs • PUT • DELETE
  7. Naturally idempotent: • GET • HEAD • OPTIONS Non-idempotent verbs:

    • POST • PATCH HTTP Verbs • PUT • DELETE
  8. Naturally idempotent: • GET • HEAD • OPTIONS Non-idempotent verbs:

    • POST • PATCH HTTP Verbs • PUT • DELETE
  9. Naturally idempotent: • GET • HEAD • OPTIONS Non-idempotent verbs:

    • POST • PATCH HTTP Verbs • PUT • DELETE Observable side-effects
  10. Naturally idempotent: • GET • HEAD • OPTIONS Non-idempotent verbs:

    • POST • PATCH HTTP Verbs • PUT • DELETE Observable side-effects
  11. 🧑💻 Seen the request before? Retrieve response from cache Generate

    response Save response to cache Return response Yes No
  12. Use a unique hash per request, storing it as a

    cache key. But where do we get the hash from? How do we know if we’ve seen a request before?
  13. Request Body Build up hash using all parameters contained in

    the request order 🧑💻 🧑💻 🍔🍟 🍔🍟
  14. Request Body Build up hash using all parameters contained in

    the request order 🧑💻 🧑💻 🍔🍟 🍔🍟 🏢
  15. IP Address Include user’s public IP address in hash 🧑💻

    🧑💻 🍔🍟 🍔🍟 🏢 104.17.3.109 104.17.3.109
  16. user_id: 3792 User ID Logged-in user’s ID 🧑💻 🧑💻 🍔🍟

    🍔🍟 🏢 user_id: 2552 user_id: null user_id: null
  17. How do we know if we’ve seen a request before?

    Use a unique hash per request, storing it as a cache key. But where do we get the hash from?
  18. How do we know if we’ve seen a request before?

    • Request body - but duplicate orders! Use a unique hash per request, storing it as a cache key. But where do we get the hash from?
  19. How do we know if we’ve seen a request before?

    • Request body - but duplicate orders! • IP address - but shared public IPs! Use a unique hash per request, storing it as a cache key. But where do we get the hash from?
  20. How do we know if we’ve seen a request before?

    • Request body - but duplicate orders! • IP address - but shared public IPs! • User ID - but guest checkouts! Use a unique hash per request, storing it as a cache key. But where do we get the hash from?
  21. How do we know if we’ve seen a request before?

    • Request body - but duplicate orders! • IP address - but shared public IPs! • User ID - but guest checkouts! Make the client do some work! Use a unique hash per request, storing it as a cache key. But where do we get the hash from?
  22. 🧑💻Idempotency-Key: 1 • Make the client pass a key with

    each idempotent request • Use this as the basis for our server-side cache • Typically use UUIDs to avoid collisions • Client SDKs help with key generation
  23. 🧑💻 Seen the request before? Generate response Save response to

    cache Return response No Idempotency-Key: 1
  24. 🧑💻 Seen the request before? Retrieve response from cache Generate

    response Save response to cache Return response Yes No Idempotency-Key: 1
  25. Choose a TTL based on retry behaviour, business impact, and

    storage constraints How long should we cache for? ⏳ Short TTL (Mins - Hours) ⏱ Medium TTL (Hours to Days) 📅 Long TTL (Days to Weeks) 🔒 Infinite TTL (Persistent Storage)
  26. Scenario: Mobile app order processing (Food delivery application) User Behaviour:

    Users place orders on their phones while outside. Retry Pattern: The mobile app automatically retries failed requests up to 3 times within a 5-minute window. Key Selection Strategy: UUIDs generally sufficient. ⏳ Short TTL (Mins - Hours) • Most connectivity issues resolve within minutes • Retries typically happen almost immediately or within a few minutes • After this window, a new order attempt is likely genuinely new
  27. Scenario: Batch payment processing (B2B SaaS platform) User Behaviour: Businesses

    run scheduled payment batch jobs that process hundreds of transactions. Retry Pattern: Failed batches are retried same day or the next morning. Key Selection Strategy: UUIDs still generally sufficient - user/session identifiers may be helpful for multi-day caches. ⏱ Medium TTL (Hours to Days) • Business hours and operational patterns dictate retry windows • System failures may take several hours to resolve • Next-day retries are common in business workflows
  28. Scenario: Subscription Management System (B2B SaaS platform handling subscription payments)

    User Behaviour: Users change subscription tiers, add features, etc. Retry Pattern: Support teams need to reprocess failed changes days later. Key Selection Strategy: Consider additional business context added to the key. 📅 Long TTL (Days to Weeks) • Customer service tickets often take days to resolve • Subscription changes have billing cycle implications • Retry attempts may happen after delays with customer communication
  29. Scenario: Regulatory Compliance Reporting User Behaviour: System submits mandatory reports

    to govt agencies. Retry Pattern: Failed submissions retried indefinitely until successful, but must never be duplicated. Key Selection Strategy: Additional metadata about requester. 🔒 Infinite TTL (Persistent Storage) • Regulatory requirements prohibit both missed and duplicate reports • Legal penalties for non-compliance are severe • The reporting requirement never expires
  30. Should errors be cached? Should we allow retries? What about

    errors? https://docs.stripe.com/api/idempotent_requests
  31. Should errors be cached? Should we allow retries? What about

    errors? https://docs.stripe.com/api/idempotent_requests
  32. Should errors be cached? Should we allow retries? What about

    errors? https://docs.stripe.com/api/idempotent_requests
  33. Cache locking Using a lock for the process allows us

    to ensure only one process handles the request.
 • Try to get a cache lock • If we can, process the request then release the lock • If we can’t, the same request is being processed by another process. Wait until it’s done
  34. Implementing Idempotency • Decide on the endpoints which need it

    • Select appropriate key cache TTLs • Document idempotent operations in your API • Allow your users to trust an operation is idempotent
  35. Implementing Idempotency • Decide on the endpoints which need it

    • Select appropriate key cache TTLs • Document idempotent operations in your API • Allow your users to trust an operation is idempotent • Decide on the endpoints which need it • Select appropriate key cache TTLs • Document idempotent operations in your API
  36. Registered As Canonical Username User ID Bigbird bigbird 123 ᴮᴵᴳᴮᴵᴿᴰ

    BIGBIRD u'\u1d2e\u1d35\u1d33\u1d2e\u1d35\u1d3f\u1d30'
  37. Registered As Canonical Username User ID Bigbird bigbird 123 ᴮᴵᴳᴮᴵᴿᴰ

    BIGBIRD u'\u1d2e\u1d35\u1d33\u1d2e\u1d35\u1d3f\u1d30' 456
  38. • getCanonicalUsername relied on nodeprep.prepare • Input string not being

    valid Unicode 3.2 meant nodeprep.prepare was no longer idempotent!
 • Fixed by double-checking username, and subsequently a library update What went wrong?
  39. • Relying on server side hashes alone to identify repeat

    requests is risky • Make the client do the work! • Cache TTL appropriate to your use case • Don’t worry about verbs that are already idempotent • But don’t forget about DELETE! • Replay behaviour - document your choice! Takeaways
  40. • Making retries safe with idempotent APIs (AWS)
 https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/ •

    Designing robust and predictable APIs with idempotency (Stripe) https://stripe.com/blog/idempotency • Creative usernames and Spotify account hijacking (Spotify) https://engineering.atspotify.com/2013/06/creative-usernames/ • Idempotency - what is it, and how can it help our Laravel APIs? https://www.conroyp.com/articles/what-is-idempotency-add-to-laravel-apis • Laravel Idempotency Package https://github.com/square1-io/laravel-idempotency Further reading