A PostgreSQL transaction can call nextval, roll back every row change it made, and still leave the allocated sequence value consumed. The row state returns to its earlier transactional form; the sequence allocation does not. This asymmetry is intentional and places sequence generators outside the rollback semantics developers often associate with database writes.
That boundary matters whenever a generated identifier is treated as more than an opaque key. A sequence provides concurrent value allocation with atomic nextval calls. It does not provide a gapless ledger, a count of committed rows, or a transactionally reversible numbering stream.
Allocation and row visibility follow different rules
Consider a table backed by an identity column:
CREATE TABLE event_log (
event_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
payload text NOT NULL
);An insert normally obtains a value from the sequence associated with event_id. The surrounding transaction can still fail:
BEGIN;
INSERT INTO event_log (payload)
VALUES ('temporary');
ROLLBACK;The inserted row is no longer visible after the rollback. The sequence value obtained while forming that row is not returned to the pool. A later insert can therefore receive a larger identifier with a numeric gap between committed rows.
This behavior separates two state machines. Table changes participate in transaction commit and rollback. Sequence advancement performed by nextval is not reclaimed when the calling transaction aborts. Treating both as one atomic state transition gives the wrong model.
The distinction also means a missing identifier does not imply that a committed row was deleted. The value may have been allocated by a transaction that never committed, or consumed by another execution path that did not ultimately insert a row.
Conflict handling can consume a value without inserting
A transaction does not need to abort for a gap to appear. PostgreSQL can evaluate a row’s default expressions before it knows that the row will not be inserted.
A representative case is INSERT ... ON CONFLICT:
CREATE TABLE account (
account_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE
);
INSERT INTO account (email)
VALUES ('a@example.test');
INSERT INTO account (email)
VALUES ('a@example.test')
ON CONFLICT (email) DO NOTHING;The second statement can obtain the next identity value while constructing its candidate tuple, then detect the uniqueness conflict and take the DO NOTHING path. No second account row is stored, yet the allocated sequence value remains consumed.
The observable gap follows evaluation order and sequence semantics, not a failed durability guarantee. Code that flags every missing numeric identifier as evidence of data loss will therefore produce false alarms.
The same boundary applies to application code that explicitly calls nextval before deciding whether to write. Allocation itself is the irreversible sequence event for this purpose; row insertion is a separate event.
Atomic allocation does not imply commit order
nextval is safe for concurrent use: sessions obtaining values from one sequence receive distinct allocations. That property does not make the numeric order a serialization of transaction commits.
Two transactions can interleave:
transaction A: nextval -> 501
transaction B: nextval -> 502
transaction B: COMMIT
transaction A: COMMITThe row carrying 502 can become committed before the row carrying 501. If transaction A later aborts instead, 501 can remain absent while 502 is present.
Sequence order therefore records allocation order only within the limits of the sequence mechanism and its configuration. It is not a database-wide causal clock and not a reliable substitute for commit ordering.
This distinction becomes important in consumers that paginate, replicate application events, or infer recency from an identifier. A larger sequence value often correlates with later allocation, but that correlation does not establish that the corresponding transaction committed later or became visible later.
Sequence caching widens the gap between allocation and observation
PostgreSQL sequences can cache values. With a cache greater than one, a session can reserve a block of sequence values and serve later nextval calls from that reserved block.
Caching reduces shared sequence access, but it further weakens interpretations based on dense or globally observed numbering. Reserved values can remain unused when a session ends. Concurrent sessions can hold different reserved blocks, so values returned across sessions need not form the simple alternating pattern that a single uncached counter might suggest.
For example, one session can reserve an earlier block while another reserves a later block. The later block can yield values while the first session still has unused values from its earlier reservation. Distinctness remains the central property; globally strict return order is not the contract to build on when caching is involved.
The last_value field also needs care in operational tooling. With caching, sequence state can reflect a reserved range rather than the highest identifier already stored in a table. Comparing last_value directly with max(id) can reveal useful state, but the difference is not automatically an anomaly.
A sequence is not a gapless business counter
Invoice numbers, regulated document numbers, ticket positions, and other domain counters can carry requirements that differ from surrogate keys. If a requirement says every issued number must correspond to a committed business record with no gaps, a normal PostgreSQL sequence does not satisfy that requirement.
The mismatch is semantic rather than cosmetic. Sequence allocation deliberately avoids reclaiming values after rollback. Reusing an abandoned value would require coordination around transaction outcome and concurrent allocators, changing the mechanism that makes sequence allocation inexpensive and contention-resistant for common identifier use.
A gapless counter can be modeled with transactional table state and appropriate locking, but that design has a different concurrency profile. If every writer must serialize through one counter row, that row becomes a coordination point. The requirement should justify that cost rather than emerge from an assumption that identifiers ought to look consecutive.
For surrogate primary keys, gaps usually have no relational meaning. Foreign keys reference existing values, not every integer between the minimum and maximum. Dense numbering is therefore an additional property, not a consequence of key integrity.
Rollback does not restore external observations
Sequence values sometimes leave the database before the transaction that uses them commits. An application might allocate an identifier, place it in a log message, construct an object-storage key, or send it to another service.
If the database transaction then aborts, the external observation can remain while the database row does not. The sequence behavior does not create distributed atomicity between those systems. It only means the identifier allocation itself will not be reclaimed by transaction rollback.
This can be useful when an identifier needs to be reserved before a row exists, but it also creates an explicit consistency boundary. A caller that requires an externally published identifier to correspond to committed database state needs a publication protocol tied to commit outcome, rather than relying on sequence semantics to bridge the systems.
PostgreSQL documentation also notes a crash-related boundary for sequence state used outside the database: a sequence state change from an uncommitted transaction may not yet be durable on storage when the cluster crashes. External use that depends on the allocation surviving a crash should occur only after the transaction containing the sequence call has committed.
currval is session state, not shared sequence inspection
After a session has called nextval for a sequence, currval returns the value most recently obtained for that sequence in the same session. Other sessions advancing the sequence do not change that session-local result.
That makes currval materially different from reading shared sequence state. It is safe from a race in which another session calls nextval between an insert and the caller retrieving its own generated value, provided the same database session is used and nextval has already run there.
Modern client code often uses INSERT ... RETURNING instead:
INSERT INTO event_log (payload)
VALUES ('accepted')
RETURNING event_id;RETURNING ties the observed identifier directly to the row produced by that statement and avoids a separate sequence lookup. The sequence still has the same rollback and gap semantics; only the method of obtaining the generated row value changes.
Sequence gaps are evidence of allocation, not row history
A sequence is a concurrent allocator with its own state transitions. Its values can be consumed by aborted transactions, conflict paths, explicit allocation calls, cached reservations, and sessions that never produce a durable row for every reserved number.
Those conditions make gaps normal output from the mechanism. They also place a firm limit on what an identifier can prove. Presence of a value on a committed row proves that row received that identifier. Absence of a nearby value does not establish deletion, failed durability, or a missing commit.
Systems remain simpler when sequence-backed identifiers stay identifiers. Commit order belongs to transaction metadata or an explicitly designed ordering mechanism. Gapless business numbering belongs to a transactional domain model that accepts the required coordination. PostgreSQL sequences solve a narrower problem: distinct, concurrent value allocation without making rollback reclaim those values.