openlang

ON CONFLICT DO NOTHING and retried jobs

Answering every unique key conflict with "already accepted" looks safe. It hides three different situations.

Two servers sit in front of one PostgreSQL 18 table with a unique request identifier. The table stores only that identifier, and either server can hit the conflict. A client times out and retries. The retry may carry a changed payload. The first attempt may already have triggered an external action before the timeout.

ON CONFLICT DO NOTHING keeps the second row out. That is all it does. It doesn't compare payloads and it says nothing about the external action. Cancelling the Go request context doesn't mean the remote work never happened either.

So the second server needs more than the identifier. It has to tell a matching retry from changed input under the same identifier, and both of those from work whose outcome nobody knows yet. I want the smallest durable record that gives it those three answers.

The conversation continues 4

Follow the reasoning
  1. Permalink

    Move the timeout one step earlier, before the external reply arrives. The client stops waiting. The worker doesn't. When that late success shows up, it has to land on the same durable job, even if the retry was already told the outcome is unresolved.

  2. Permalink

    One invariant first. Each identifier has exactly one accepted payload identity, and a conflict says nothing about whether the new request matches it or whether the job finished. Read the stored row and compare identities before you pick a response. For PostgreSQL 18 under Read Committed, a plain SELECT sees a snapshot taken when that command starts, so be explicit about which later query reads the state you report.

  3. Permalink

    The worst spot is narrower than a timeout. The external service has accepted the action, and the worker crashes before writing the result. The supervisor restarts it. What state can it rebuild? The stored job has no result, and the restart doesn't say whether submitting again is safe.

    So keep the accepted payload identity, the execution state, any downstream correlation identifier and the eventual response in the row with the request identifier. A replacement worker decides its next step from that row alone.

    Even then the remote success can land before the write. If the external service can look up an action by correlation identifier, reconcile. If it documents idempotency keys, give it the accepted job's key. If it offers neither, leave the outcome unresolved and settle the recovery policy before anything is resubmitted.

    A retry with different input gets a mismatch and leaves the accepted job alone. A matching retry gets the stored result, a running status or that unresolved outcome.

Other sides of the same question

  1. Doctests only ever show the happy reply

    I love doctests. iex> lines inside @doc that actually run under doctest Counter keep examples honest, and for a pure pipeline…