TOAST
The Oversized-Attribute Storage Technique. PostgreSQL's quiet mechanism for compressing and storing large field values outside the main table — working precisely as designed when no one is watching.
TOAST — The Oversized-Attribute Storage Technique — is perhaps the most whimsically named serious mechanism in all of PostgreSQL. It handles values that do not fit in a single data page. PostgreSQL stores rows in 8 KB pages, and a single row cannot span multiple pages. When a column value pushes a row beyond roughly 2 KB, PostgreSQL automatically compresses the value, moves it to a separate TOAST table, or both. This happens transparently — your queries read and write the column normally, and PostgreSQL manages the storage mechanics entirely behind the scenes. Most databases use TOAST constantly without anyone noticing. I have always appreciated infrastructure that does its finest work in silence.
What TOAST is
PostgreSQL has a hard constraint: a single row must fit within one 8 KB page. The page also contains header information and potentially other rows, so the practical threshold is lower. When any individual field value exceeds roughly 2 KB, TOAST activates — quietly and without consulting you.
TOAST can do two things with an oversized value: compress it in-place using a fast compression algorithm, and move it out-of-line to a separate TOAST table. These operations can be combined — compress first, then move if the compressed value is still too large — or applied independently, depending on the column's storage strategy.
Every table with variable-length columns has an associated TOAST table, created automatically in the pg_toast schema. You never interact with this table directly — nor should you need to. When you insert a large text or jsonb value, PostgreSQL silently breaks it into ~2 KB chunks, stores those chunks in the TOAST table, and replaces the original value in the main row with a small pointer. When you read the value back, PostgreSQL reassembles the chunks and returns the full value. The entire affair is handled without your involvement, which is rather the hallmark of good service.
How TOAST works
Each column has a TOAST strategy that controls how PostgreSQL handles oversized values. The strategy is set by the data type's default and can be changed per-column. The defaults are well chosen — most workloads will never need to adjust them.
- PLAIN — no TOAST at all. The value must fit in a page as-is. Used by fixed-length types like
integer,bigint, andbooleanthat are never large enough to need TOAST. - EXTENDED — compress first, then move out-of-line if still too large. This is the default for
text,jsonb, andbytea. It provides the best balance of space savings and access speed for most workloads. - EXTERNAL — move out-of-line without compressing. Useful when the data is not compressible (already compressed, binary formats) or when you need fast substring access — PostgreSQL can read individual chunks without decompressing the entire value.
- MAIN — compress, but try to keep in-line. Only moves out-of-line as a last resort when the row still exceeds the page size after compression. Keeps data closer to the main row for faster access, at the cost of larger main table pages.
-- Check the TOAST strategy for each column in a table
SELECT
attname AS column,
atttypid::regtype AS type,
CASE attstorage
WHEN 'p' THEN 'PLAIN — no TOAST (fixed-length types)'
WHEN 'e' THEN 'EXTERNAL — out-of-line, no compression'
WHEN 'x' THEN 'EXTENDED — compress, then out-of-line if still large'
WHEN 'm' THEN 'MAIN — compress, avoid out-of-line'
END AS strategy
FROM pg_attribute
WHERE attrelid = 'my_table'::regclass
AND attnum > 0
AND NOT attisdropped
ORDER BY attnum; -- Change a column's TOAST strategy
-- EXTERNAL is useful when you read large values frequently
-- and compression/decompression overhead is not worth it
ALTER TABLE my_table ALTER COLUMN payload SET STORAGE EXTERNAL;
-- MAIN keeps data in-line when possible (compress, avoid moving out)
ALTER TABLE my_table ALTER COLUMN description SET STORAGE MAIN; Changing a column's storage strategy only affects future writes. Existing rows retain their current storage format until they are updated.
TOAST and performance
TOAST is designed to be invisible, and for most workloads it succeeds admirably. But it has performance characteristics that reward understanding.
Detoasting cost on read. When PostgreSQL reads a TOASTed value, it fetches the chunks from the TOAST table and reassembles them. If the value was compressed, it decompresses. This adds I/O and CPU proportional to the value size. A 1 KB compressed text field adds negligible overhead. A 50 MB jsonb document adds real latency.
Sequential scans skip TOAST data. This is the upside. When PostgreSQL performs a sequential scan on a table, it reads only the main table pages. TOASTed values are stored separately, so they do not inflate the main table's physical size. A table with a million rows and a large jsonb column per row may have a 200 MB main table and a 40 GB TOAST table. A sequential scan that filters rows reads 200 MB, not 40 GB. The TOAST data is only fetched for rows that survive the filter and include the TOASTed column in the output.
Avoid SELECT * on tables with large columns. If I may be direct, this is the single most actionable advice for TOAST performance. If you only need three columns from a ten-column table, name those three columns. SELECT * forces PostgreSQL to detoast every large column for every returned row, even if your application ignores most of them. Explicit column lists let PostgreSQL skip TOAST table access entirely for columns you did not request. Asking for everything and discarding most of it is not merely wasteful — it is asking your database to do unnecessary work on your behalf, and one ought to know better.
Inspecting TOAST
Every table with TOASTable columns has an associated TOAST table. You can find it through pg_class.reltoastrelid and check its size independently from the main table. It is worth the occasional look — a proper inspection of the premises, if you will.
-- Find the TOAST table for a given table
SELECT
c.relname AS table_name,
t.relname AS toast_table,
pg_size_pretty(pg_relation_size(t.oid)) AS toast_size
FROM pg_class c
JOIN pg_class t ON c.reltoastrelid = t.oid
WHERE c.relname = 'my_table';
-- Compare main table size vs TOAST size
SELECT
pg_size_pretty(pg_relation_size('my_table')) AS main_size,
pg_size_pretty(pg_relation_size(
(SELECT reltoastrelid FROM pg_class WHERE relname = 'my_table')
)) AS toast_size,
pg_size_pretty(pg_total_relation_size('my_table')) AS total_size; If a table's total size is much larger than its main relation size, the difference is mostly TOAST data. This is normal for tables with text, jsonb, or bytea columns that store large values.
For deeper diagnostics, you can query the TOAST table directly. TOAST tables live in the pg_toast schema and are named pg_toast_<oid>, where the OID is the parent table's OID. Each row in the TOAST table is a single chunk of a single value.
-- Look at individual TOAST chunks (for diagnostics)
-- Each chunk is ~2KB; large values span multiple chunks
SELECT
chunk_id,
chunk_seq,
length(chunk_data) AS chunk_bytes
FROM pg_toast.pg_toast_<oid>
WHERE chunk_id = <toast_pointer_value>
ORDER BY chunk_seq; How Gold Lapel relates
Gold Lapel operates at the query level, intercepting SQL between your application and PostgreSQL. TOAST is transparent to queries — Gold Lapel sees the same column values your application does, fully assembled and decompressed. It does not interact with TOAST tables directly, nor does it need to.
Where the connection matters is in query analysis. Gold Lapel can identify patterns where SELECT * on tables with large TOASTed columns is causing unnecessary I/O — the kind of habit that accumulates quietly until someone examines the bill. By surfacing which queries would benefit from explicit column lists, Gold Lapel helps your application avoid detoasting overhead for columns it never uses. TOAST does its job well. It simply does its job best when queries only ask for what they need.