TOAST, FSM & Visibility Map
Storing large values and tracking free space
Heap Storage fixes every page at 8KB. That's fine
for a numeric or a short text value, but what happens when you insert a
50KB JSON blob into a single column? Postgres has to move it somewhere else
— and it needs to track, per page, how much room is left and which pages
are safe to skip during a scan. Three mechanisms handle this: TOAST, the
Free Space Map, and the Visibility Map.
TOAST
TOAST (The Oversized-Attribute Storage Technique) kicks in automatically
for variable-length columns — text, jsonb, bytea, arrays — once a row
would exceed roughly 2KB (Postgres targets fitting at least 4 tuples per
page). The oversized value is compressed and, if still too big, sliced into
chunks stored in a hidden companion table:
-- every table with a toastable column gets one of these automatically
SELECT reltoastrelid::regclass FROM pg_class WHERE relname = 'accounts';
-- pg_toast.pg_toast_16391Each column has a storage strategy controlling this behavior:
PLAIN— never compressed or moved out-of-line (used for fixed-size types likeinteger)EXTENDED— compress first, then move out-of-line if still too large (the default fortext/jsonb)EXTERNAL— move out-of-line without compressing (faster substring access, larger storage)MAIN— compress, but avoid moving out-of-line unless there's no other choice
A wide jsonb column means every SELECT * — even one that never touches
that column in its WHERE clause — can pay for a TOAST fetch if the
column is included in the result. Select only the columns you need on hot
paths.
Free Space Map (FSM)
Every relation keeps a Free Space Map — a compact side-file recording
roughly how much free space each page has. When you INSERT, Postgres
consults the FSM to find a page with room instead of scanning the whole
table or always appending to the end. It's what makes space freed by
VACUUM reusable rather than wasted.
Visibility Map (VM)
The Visibility Map is a bitmap, one bit (actually two) per page, tracking whether a page is:
- all-visible — every tuple on the page is visible to all current and future transactions, so no MVCC visibility check is needed
- all-frozen — every tuple's
xminis old enough that it no longer needs to be considered during transaction-ID wraparound freezing
The VM matters for two very different reasons:
Index-only scans. If a query only needs indexed columns, Postgres can answer entirely from the index — skipping the heap — but only for pages the VM marks all-visible. A table with a lot of recent write activity has a "colder" VM and falls back to the heap more often, even for index-covered queries.
Vacuum skipping. Autovacuum uses the VM to skip pages it already knows are all-visible/all-frozen, which is why a mostly-read, rarely-written table vacuums fast even when it's huge — most of its pages are never revisited.
Both maps live in small companion files next to the heap (_fsm, _vm) and
are updated incrementally as you write and vacuum. Up next: the mechanism
that actually decides which tuples are visible in the first place —
MVCC.