go 19 lines · 1 tab

Row-level locking with SELECT ... FOR UPDATE in a transaction

Leah Thompson Jan 2026
1 tab
package store

import (
  "context"

  "github.com/jackc/pgx/v5"
)

func ConsumeToken(ctx context.Context, tx pgx.Tx, token string) error {
  var used bool
  if err := tx.QueryRow(ctx, `SELECT used FROM tokens WHERE token=$1 FOR UPDATE`, token).Scan(&used); err != nil {
    return err
  }
  if used {
    return nil
  }
  _, err := tx.Exec(ctx, `UPDATE tokens SET used=true WHERE token=$1`, token)
  return err
}
1 file · go Explain with highlit

Optimistic locking is great for most user edits, but sometimes you need strict serialization—like decrementing inventory or consuming a one-time token. In those cases I use SELECT ... FOR UPDATE inside a transaction. The lock is scoped to the transaction, which means your code must be disciplined about ctx deadlines so locks don’t hang forever. The other key is ordering: if you lock multiple rows, lock them in a consistent order to avoid deadlocks. This pattern also pairs well with idempotency keys: acquire the row lock, check whether the work was already done, then perform the write. When used carefully, it’s a reliable way to enforce invariants without building distributed locks. The database is very good at this; let it do the hard work.


Related snips

Share this code

Here's the card — post it anywhere.

Row-level locking with SELECT ... FOR UPDATE in a transaction — share card
Link copied