PostgreSQL TOAST Internals: Out-of-Line Compression, JSONB Payload Sizing, and Disk I/O Penalties

Demystify PostgreSQL's Oversized-Attribute Storage Technique (TOAST). Compare EXTENDED vs EXTERNAL storage strategies, benchmark LZ4 compression, and prevent write amplification on large JSONB columns.

The 8KB Page Constraint and the Birth of TOAST

PostgreSQL manages all table data in fixed-size blocks called pages, which default to 8KB (8,192 bytes) in standard compilations. Because tuple data cannot span across multiple physical pages in the main heap, a fundamental architectural dilemma emerges: how can PostgreSQL store large records—such as 100KB JSONB documents, high-resolution vector embeddings, or multi-paragraph HTML bodies—without violating the 8KB page boundary?

The solution is The Oversized-Attribute Storage Technique (TOAST). Whenever a tuple's serialized byte length exceeds the threshold of approximately 2KB (defined as TOAST_TUPLE_THRESHOLD = BLCKSZ / 4, or ~2,048 bytes), PostgreSQL’s storage engine automatically triggers TOAST mechanics to compress and offload oversized attributes.

1. Storage Strategies: PLAIN, EXTENDED, EXTERNAL, and MAIN

Every column in a PostgreSQL table is assigned one of four physical storage strategies based on its data type:

Strategy Compression Allowed? Out-of-Line Storage? Default Data Types & Behavior
PLAIN No No Fixed-length types (integer, boolean, uuid, timestamp). Always stored inline.
EXTENDED Yes (First line of defense) Yes (If compression is insufficient) Default for variable-length types (text, varchar, jsonb, bytea).
EXTERNAL No Yes Disables compression entirely; moves oversized fields directly out-of-line. Fast for substring operations.
MAIN Yes Only as last resort Tries aggressively to keep compressed data inline inside the main heap page.

2. The Lifecycle of an Out-of-Line Attribute

When an update or insert introduces a large JSONB document into an EXTENDED column:

  1. Inline Compression: PostgreSQL attempts to compress the field using the configured compression algorithm. If the compressed value fits within the target page threshold (TOAST_TUPLE_TARGET = ~2KB), it remains directly in the heap page.
  2. Out-of-Line Offloading: If compression is insufficient, PostgreSQL creates a synthetic 18-byte pointer (a varlena header) in the main table row. The actual data payload is sliced into 2KB chunks and stored in a companion physical table named pg_toast_<OID>.
  3. B-Tree Indexing on TOAST Chunks: The companion TOAST table possesses its own primary B-Tree index structured over (chunk_id, chunk_seq) to ensure rapid chunk re-assembly when read.

3. The Hidden Performance Penalties: JSONB and Sequential Scans

While TOAST enables storing arbitrarily large documents, it introduces severe, often misunderstood performance penalties in production workloads:

A. Write Amplification on Partial JSONB Updates

Many developers assume that executing UPDATE users SET profile = jsonb_set(profile, '{last_seen}', '12345') updates only the modified key. It does not. Under PostgreSQL's MVCC, the entire TOASTed document must be decompressed, updated in memory, re-compressed, and written as completely new chunks in the TOAST table. Updating a 200KB JSONB document to change a single integer generates over 200KB of new write-ahead log (WAL) data and triggers heavy disk I/O.

B. Memory Footprint and De-TOASTing Overhead

When executing an analytical query (e.g., SELECT COUNT(*) FROM orders WHERE status = 'COMPLETED'), if your query accidentally uses SELECT *, PostgreSQL is forced to follow pointers into the TOAST table, load every chunk block into shared_buffers, decompress the raw data in CPU memory, and discard it. This causes severe cache pollution, flushing useful index pages out of RAM.

4. Production Benchmarking: Modernizing with LZ4 Compression

Historically, PostgreSQL relied on a proprietary compression algorithm called pglz. While effective for simple text, pglz imposes heavy CPU serialization bottlenecks on large JSONB structures. Starting in PostgreSQL 14, native LZ4 compression was introduced:

-- Modernize TOAST compression algorithm for JSONB columns
ALTER TABLE event_logs ALTER COLUMN payload SET COMPRESSION lz4;

-- Audit TOAST table sizes across production relations
SELECT 
    c.relname AS base_table,
    pg_size_pretty(pg_relation_size(c.oid)) AS heap_size,
    pg_size_pretty(pg_total_relation_size(c.reltoastrelid)) AS toast_size,
    ROUND((pg_total_relation_size(c.reltoastrelid)::numeric / 
           NULLIF(pg_total_relation_size(c.oid), 0)::numeric) * 100, 2) AS toast_pct
FROM pg_class c
WHERE c.relkind = 'r' AND c.reltoastrelid != 0
ORDER BY pg_total_relation_size(c.reltoastrelid) DESC
LIMIT 10;

In high-throughput benchmarks, migrating high-write JSONB columns from pglz to lz4 delivers a 3.2x speedup in write throughput and reduces CPU compression overhead by over 55%.

5. Architectural Best Practices for Production Systems

  • Split Hot Metadata from Cold JSONB Blobs: If an entity requires frequent status updates alongside a massive metadata JSONB document, decouple them into two distinct relational tables: a lean, fast-updating primary table and a 1-to-1 companion table for the TOASTed payload.
  • Never SELECT * on TOASTed Tables: Always project only the specific relational columns required by the application view. Keeping wide JSONB columns out of the projection list eliminates all TOAST chunk lookups and decompression overhead.
  • Consider EXTERNAL for Pre-Compressed Payloads: If storing binary assets that are already compressed (e.g. gzip archives, WebP images, or encrypted envelopes), set the storage strategy to EXTERNAL to prevent PostgreSQL from burning CPU cycles attempting futile re-compression.

Understanding and tuning TOAST internals is fundamental to maintaining sub-millisecond query execution and preventing disk bloat on modern database clusters.

Interactive PostgreSQL Memory & Tuning Calculator

// Real-Time Production Memory Allocator
PostgreSQL 14 / 15 / 16 / 17

Adjust your server resources below to calculate optimized postgresql.conf memory thresholds, autovacuum scale factors, and cost weights.

16 GB
2 GB 64 GB 256 GB
8 Cores
2 16 64
100
20 200 1,000
generated-postgresql.conf
INI
# Memory Allocations
shared_buffers = 4GB
work_mem = 40MB
maintenance_work_mem = 1GB
effective_cache_size = 12GB

# Concurrency & Background Workers
max_connections = 100
max_worker_processes = 8
max_parallel_workers_per_gather = 4
max_parallel_workers = 8

# Autovacuum Tuning (Prevent Bloat)
autovacuum_max_workers = 4
autovacuum_vacuum_scale_factor = 0.05
autovacuum_analyze_scale_factor = 0.02
autovacuum_vacuum_cost_limit = 1000

# Planner Cost Constants (NVMe SSD)
random_page_cost = 1.1
effective_io_concurrency = 200
Need hands-on database profiling? We analyze query execution plans, resolve lock trees, and eliminate replication lag.
Book Database Audit (30m)
// Production Systems Architecture • Database Diagnostic Audit

Diagnosing Production PostgreSQL Bloat, Lock Contention, or Replication Lag?

Theoretical tuning only goes so far. We provide hands-on architectural reviews of query execution plans, autovacuum parameters, connection pools, and read-replica lag for high-concurrency systems.

All Insights
Chat on WhatsApp