# Lazy transaction id assignment

A transaction does not get an xid at BEGIN; it gets one the first time it writes.

> Canonical URL: https://sudohulk.com/writing/lazy-xid-assignment
> Type: note · Stream: Tech
> Maturity: seedling. Just planted. May be wrong.
> Published: 2026-09-22
> Tags: postgresql
> Reading time: 1 min
> Author: Vignesh Kumar A <vignesh@sudohulk.com>
> Licence: CC BY 4.0. Quote freely, cite the canonical URL.

---

`BEGIN` does not hand out a **transaction id** (xid). A transaction that only reads never gets one; it runs under a lightweight **virtual transaction id** made of the backend id and a per-backend counter. The real 32-bit xid is assigned lazily, on the first operation that needs to mark a tuple: an `INSERT`, `UPDATE`, `DELETE`, or a lock that has to be recorded.

Xids are scarce, and they wrap around. Every assigned xid must eventually be frozen by vacuum to avoid wraparound, so handing them to read-only transactions would burn through the space and create work for no reason.

:::gotcha
`txid_current()` forces an xid to be assigned. `pg_current_xact_id_if_assigned()` (PG 13+) tells you whether one exists without creating it.
:::

Related: [[pd_lsn vs record LSN]], and a post on what happens between BEGIN and COMMIT once it is written.
