go 22 lines · 1 tab

Retry Postgres serialization failures with bounded attempts

Leah Thompson Jan 2026
1 tab
package dbutil

import (
  "context"

  "github.com/jackc/pgconn"
)

func RetrySerialization(ctx context.Context, max int, fn func(context.Context) error) error {
  var last error
  for i := 0; i < max; i++ {
    if err := fn(ctx); err != nil {
      last = err
      if pgErr, ok := err.(*pgconn.PgError); ok && pgErr.Code == "40001" {
        continue
      }
      return err
    }
    return nil
  }
  return last
}
1 file · go Explain with highlit

For workloads that run at SERIALIZABLE isolation (or that hit serialization conflicts under load), retries are part of the contract. The important part is to retry only the safe errors (typically SQLSTATE 40001) and to keep the loop bounded so you don’t create runaway contention. I like to wrap the transaction function in a small retry helper that re-runs the unit of work on serialization failures only. In production, I also add jittered backoff to reduce thundering herds when many transactions collide. This approach keeps call sites clean and ensures you don’t accidentally “retry everything”, which can hide real bugs. When you combine this with good observability (attempt count, conflict rate), you can tune the system rather than guessing. It’s one of those boring patterns that saves you during peak load.


Related snips

Share this code

Here's the card — post it anywhere.

Retry Postgres serialization failures with bounded attempts — share card
Link copied