PostgreSQL JSONB Size Limits: The `256MB` Cap, TOAST Storage, and Indexing Pitfalls
PostgreSQL enforces a strict `268,435,455-byte` (approximately 256 megabytes) limit on individual JSONB values, determined by internal 28-bit length masks in the varlena container header. While generic text fields can expand to 1 gigabyte via TOAST, large JSONB trees suffer from severe GIN index degradation and write amplification. Architectures managing massive document payloads or agentic workflows must understand these storage mechanics and consider external intelligent storage layers.
Why PostgreSQL Caps JSONB at 268,435,455 Bytes: Varlena Headers and Container Limits
The PostgreSQL JSONB size limit is a hard cap of 268,435,455 bytes (approximately 256 megabytes) on any single jsonb value, determined by the internal 4-byte header structure used in PostgreSQL varlena format. Attempting to insert, aggregate, or update a JSONB value beyond this threshold triggers an immediate engine exception: ERROR: total size of jsonb array elements exceeds the maximum of 268435455 bytes (SQLSTATE 54000).
Understanding why this boundary exists requires examining the internal storage structures of PostgreSQL. In relational database design, developers often treat JSONB as an unbounded document store. However, PostgreSQL manages JSONB not as arbitrary text, but as a parsed, decomposed binary tree engineered for rapid key extraction, containment queries, and indexing. That performance optimization introduces strict structural ceilings that do not apply to raw strings.
How the 28-Bit JEntry Header Caps JSONB
Inside the PostgreSQL source code (specifically in src/include/utils/jsonb.h and src/backend/utils/adt/jsonb_util.c), every JSONB container is stored as a variable-length attribute (varlena). A JSONB container consists of a 4-byte header describing the container type (array or object) and element count, followed by an array of 4-byte headers called JEntry structures for each element, and finally the raw binary data.
Each 32-bit JEntry word encodes two distinct pieces of information:
- Type Tags (4 bits): Flags indicating whether the item is a string, number, boolean true, boolean false, null, or a nested container.
- Offset or Length (28 bits): The byte offset or length of the item data within the container buffer.
Because the offset and length fields are masked by JENTRY_OFFLENMASK (0x0FFFFFFF), the maximum addressable offset within any single JSONB container is exactly 2^28 - 1, which equals 268,435,455 bytes. The engine cannot address byte offsets beyond this boundary. If an incoming JSON string or document tree serializes to a binary payload exceeding 268,435,455 bytes, PostgreSQL rejects the datum with a PROGRAM_LIMIT_EXCEEDED error code.
This 256MB cap applies per value, meaning per table cell in a single row. It is not a limit on table size or the sum of all rows. However, because developers often store large arrays, nested event traces, or AI document corpora inside a single column, hitting this internal ceiling in production is a common failure mode.
The Critical Distinction: JSONB Container vs. 1GB TOAST Ceiling
A frequent point of confusion among database administrators and backend engineers is the difference between PostgreSQL's general field size limit and the JSONB container cap. Many technical articles claim that PostgreSQL supports up to 1GB per JSON field. That claim is only half true.
PostgreSQL's underlying variable-length storage engine allows individual fields of types like text, varchar, and bytea to reach up to 1GB (2^30 - 1 bytes, or 1,073,741,823 bytes). In those types, the 4-byte varlena header dedicates 30 bits to length tracking and 2 bits to storage flags. The plain json data type (as opposed to jsonb) stores data as an unparsed, literal text string. Because plain json does not construct a JEntry binary structure, a plain json column can store documents up to 1GB.
When you use jsonb, however, the binary parser decomposes the JSON structure upon write. The 28-bit JEntry offset mask lowers the ceiling from 1GB down to 256MB. Teams migrating from json or text to jsonb to take advantage of indexing frequently run into this hard barrier when handling payloads between 256MB and 1GB.
All technical specifications reflect PostgreSQL 14 through PostgreSQL 18 storage implementations.
Related guides
- Llama.cpp Context Size: How to Set -c, Calculate KV Cache, and Optimize VRAMIn llama.cpp, context size is the maximum sequence length in tokens allocated for prompt and response processing,...
- DynamoDB Item Size Limit: The 400 KB Cap, ValidationException, and Storage PatternsThe DynamoDB item size limit is a strict maximum of 400 KB per item, calculated by summing the byte lengths of all...
- gRPC Message Size Limit: 4MB Default Cap & RESOURCE_EXHAUSTED FixesThe default gRPC message size limit is 4 MB (4,194,304 bytes) for inbound messages, throwing a...
- Kafka Message Size Limits: Broker Caps, Configuration, and the Claim-Check PatternApache Kafka enforces a default message size limit of 1,048,576 bytes (1 MB) across broker, producer, and consumer...
- Vertex AI Token Limits: Model Context Windows, Output Caps & Storage WorkaroundsVertex AI token limits define the maximum input context window, capped at 2,097,152 tokens for Gemini 1.5 Pro, and...
- AWS Bedrock Token Limits: Model Output Caps, Converse API Parameters, and QuotasAn AWS Bedrock token limit refers to the parameter-enforced boundaries on input prompt length and maximum generated...
More on this subject: AI Agents: General Guides (172 guides)
How TOAST Manages Large JSONB Documents on Disk and in Memory
PostgreSQL stores table data in fixed-size blocks called heap pages, which default to 8 KB (BLCKSZ = 8192 bytes). A single table row cannot span multiple pages. To accommodate attributes larger than an 8 KB page, PostgreSQL relies on TOAST (The Oversized-Attribute Storage Technique). Understanding how TOAST handles JSONB columns reveals why large documents create silent performance bottlenecks long before reaching the 256MB limit.
The Mechanics of Out-of-Line Storage
When a row containing variable-length data exceeds TOAST_TUPLE_THRESHOLD (normally 2 KB), the database storage engine evaluates the row for compression and out-of-line storage. The storage engine recognizes four storage strategies:
- EXTENDED: The default strategy for
jsonb,text, andbytea. The system first attempts to compress the value. If the row remains wider thanTOAST_TUPLE_TARGET(normally2 KB), it moves the data out-of-line into an auxiliary TOAST table. - EXTERNAL: Moves wide values out-of-line into the TOAST table without attempting compression.
- MAIN: Compresses the value inline but avoids out-of-line storage unless no other option allows the row to fit inside an
8 KBpage. - PLAIN: Prevents both compression and out-of-line storage. Only fixed-width or short data types support PLAIN.
When a JSONB document is moved out-of-line, PostgreSQL chunks the payload into segments of at most TOAST_MAX_CHUNK_SIZE (approximately 2000 bytes, allowing four chunks per 8 KB page). These chunks are written as individual rows in the table's dedicated TOAST relation (pg_toast_<table_oid>). In the main table's heap page, the original column value is replaced by an 18-byte on-disk pointer containing the TOAST table OID, the chunk ID, the logical uncompressed size, and the physical stored size.
The 256MB Decompression Trap
Because TOAST compresses data using pglz or lz4 before moving it out-of-line, a repetitive JSON document of 280MB might compress down to 40MB on disk. A developer might assume that because the stored data fits well within PostgreSQL disk blocks, the operation is safe.
This is where the decompression trap snaps shut. PostgreSQL enforces the 268,435,455-byte limit on the uncompressed logical representation of the JSONB container, not on its compressed on-disk footprint. When an application executes an update or query that requires PostgreSQL to inspect or reconstitute the JSONB container, the engine must decompress the chunks into memory. If the uncompressed binary data exceeds 268,435,455 bytes, the query fails instantly.
Detoasting Latency and Memory Overhead
Even when JSONB values stay below 256MB (for instance, documents between 5MB and 50MB), TOAST storage introduces substantial overhead during query execution:
- Full Retrieval Penalty: When an application queries a specific key from a large JSONB column (such as
SELECT data->'metadata'->>'status' FROM records WHERE id = 42;), PostgreSQL cannot seek directly to that offset within the TOAST table. The storage engine must fetch every out-of-line chunk from disk, reassemble the compressed byte stream, decompress the full payload in backend memory, and then traverse the binary tree to extract the single requested string. - Memory Churn in Backend Processes: Decompressing and constructing large JSONB containers consumes significant RAM in backend PostgreSQL processes (
palloc). A table scan over a few dozen rows containing20MBJSONB documents can allocate gigabytes of transient memory, evicting active database pages from the PostgreSQL buffer pool (shared_buffers) and the operating system page cache. - No Partial Detoasting for JSONB: Unlike the
textdata type withEXTERNALstorage, where PostgreSQL can perform optimized substring reads on out-of-line chunks without reading the entire document, JSONB always requires complete container detoasting and parsing.
GIN Indexing Pitfalls and Write Amplification on Deep JSONB Trees
The primary reason developers choose jsonb over plain json is indexing. PostgreSQL provides Generalized Inverted Index (GIN) support, enabling index-accelerated searches across arbitrary keys, containment expressions, and JSONPath queries. However, creating a GIN index over multi-megabyte JSONB documents is one of the most common causes of catastrophic database performance degradation.
How GIN Indexes Explode on Large Documents
A standard B-Tree index maps a single column value to a tuple identifier (TID). In contrast, a GIN index is an inverted index: it extracts multiple component keys from each indexed datum and maps each individual key to a list of rows containing that key (a posting list or posting tree).
When you create a default GIN index on a JSONB column (CREATE INDEX idx_data ON events USING GIN (payload);), PostgreSQL uses the default jsonb_ops operator class. This operator class creates an index entry for:
- Every single key in every object across the entire hierarchy.
- Every single scalar value associated with every key.
- Every element in every array.
If a table contains small documents of 1 KB, a document might yield 15 to 30 index entries. But if you insert a 10MB or 50MB JSONB tree representing an AI agent's execution history, complex AST metadata, or scraped web structures, that single document can contain hundreds of thousands of distinct paths, keys, and values.
Inserting that single row forces PostgreSQL to generate hundreds of thousands of index postings. The resulting GIN index quickly balloons to three to ten times the size of the original table heap, consuming tens of gigabytes of disk space and memory.
Write Latency Spikes and gin_pending_list_limit
To prevent every write from performing expensive random writes across the GIN tree, PostgreSQL uses a fast-update buffer known as the pending list (fastupdate = on). Incoming index keys are appended to this pending list in sequential order.
When the pending list exceeds gin_pending_list_limit (which defaults to 4MB), the inserting transaction is forced to pause and execute gin_clean_pending_list, sorting and merging the pending entries into the main GIN balanced tree.
On tables containing large JSONB documents, a single INSERT statement can generate more than 4MB of index keys on its own. Consequently, every write operation breaches gin_pending_list_limit, forcing synchronous in-line merges on every transaction. What should be a 5-millisecond write turns into a multi-second stall, blocking application worker threads and causing connection pool exhaustion.
MVCC and the Immutability Write Penalty
PostgreSQL uses Multi-Version Concurrency Control (MVCC) to provide transactional isolation. In PostgreSQL, updates are never performed in place on existing heap tuples. When an application updates a single field inside a JSONB column, PostgreSQL must:
- Write an entirely new version of the row tuple in the main table heap.
- Re-compress and duplicate the entire JSONB payload in the TOAST table.
- Mark the old row version and its TOAST chunks as dead.
- Insert an entirely new set of index entries into the GIN index for the new document version.
If your application updates a 20MB JSONB document to change a single boolean flag from false to true, PostgreSQL writes 20MB of new data to TOAST, writes corresponding write-ahead log (WAL) records, and generates thousands of new GIN index postings. The old 20MB document remains on disk until VACUUM processes the dead tuple. Under moderate write traffic, this behavior causes massive WAL volume, replica replication lag, and severe disk bloat.
Operator Classes: jsonb_ops vs. jsonb_path_ops vs. Expression Indexes
When JSONB indexing is required, choosing the correct index strategy dramatically reduces bloat and write overhead.
-- Default: jsonb_ops (Indexes keys and values separately)
-- Supports ?, ?|, ?&, @>, @?, @@ operators
CREATE INDEX idx_events_ops ON events USING GIN (payload);
-- Optimized: jsonb_path_ops (Hashes path + value together)
-- Supports @>, @?, @@ operators only; three to six times smaller index size
CREATE INDEX idx_events_path ON events USING GIN (payload jsonb_path_ops);
-- Best Practice: Targeted B-Tree Expression Index on hot fields
-- Extremely compact, zero full-document indexing overhead
CREATE INDEX idx_events_user_id ON events (((payload->>'user_id')::uuid));
The technical difference between a jsonb_ops and a jsonb_path_ops GIN index is that jsonb_ops creates independent index items for each key and value in the data, while jsonb_path_ops creates index items only for each value in the data, hashing the full key path leading to that value into a single 32-bit hash. A jsonb_path_ops index is typically three to six times smaller than a jsonb_ops index on the same data and executes containment queries (@>) faster.
However, for high-throughput production systems, the most reliable pattern is avoiding full-document GIN indexes entirely. Define B-Tree expression indexes on the specific extracted paths your application queries frequently.
Stop Forcing Multi-Megabyte Documents and State into PostgreSQL JSONB
Store dynamic prompt libraries, agent memory traces, and large document corpora in persistent Fast.io workspaces. Your applications and AI agents search indexed files through MCP without hitting the `256MB` container cap or stalling database queries. Monthly plans start with a 14-day free trial, which requires a credit card.
When to Offload Large Payloads from Relational Tables: Architectural Patterns
The hard 256MB container cap, TOAST detoasting latency, and GIN index amplification highlight a clear operational rule: keep individual PostgreSQL JSONB documents under 100 KB.
When JSON payloads exceed 100 KB, they cease to function like relational attributes and start behaving like unstructured binary files. Engineering teams handling large payloads, complex configuration catalogs, or AI agent context can adopt three architectural alternatives to protect relational database performance.
Pattern 1: Relational Normalization
The most direct solution for large JSONB arrays is relational normalization. If a JSONB document hits the 256MB limit because it contains an array of hundreds of thousands of event objects, those objects belong in a separate relational table:
-- Anti-pattern: hundreds of thousands of events in one JSONB array
CREATE TABLE execution_runs (
run_id uuid PRIMARY KEY,
events jsonb -- Breaches `256MB` limit on long runs
);
-- Normalized: Relational one-to-many model
CREATE TABLE execution_runs (
run_id uuid PRIMARY KEY,
started_at timestamptz NOT NULL,
status text NOT NULL
);
CREATE TABLE execution_events (
event_id bigserial PRIMARY KEY,
run_id uuid REFERENCES execution_runs(run_id) ON DELETE CASCADE,
event_type text NOT NULL,
payload jsonb -- Compact, < 5 KB per event
);
CREATE INDEX idx_exec_events_run_id ON execution_events (run_id);
Normalizing arrays into child rows eliminates the 256MB ceiling, enables standard pagination and streaming with LIMIT and OFFSET, and allows index updates to affect only the specific modified row rather than a monolithic document.
Pattern 2: Database Pointers with Object Storage
When documents cannot be cleanly normalized, such as raw JSON dumps from external APIs, full-page web scrapes, or compiled execution logs, store only metadata in PostgreSQL and write the payload to an external object store:
CREATE TABLE raw_ingestions (
ingestion_id uuid PRIMARY KEY,
source text NOT NULL,
content_type text NOT NULL,
byte_size bigint NOT NULL,
checksum text NOT NULL,
storage_uri text NOT NULL, -- s3://bucket/path or workspace path
created_at timestamptz DEFAULT now()
);
PostgreSQL handles transactional queries, foreign keys, and access controls, while the file storage layer handles multi-megabyte payloads without TOAST overhead or buffer pool starvation.
Pattern 3: Offloading to Intelligent Workspaces for Agentic Workflows
For autonomous AI agents and engineering teams building with LLMs, the problem is intensified. Agents produce and consume multi-megabyte context corpora: complete tool execution traces, documentation repositories, customer interaction archives, and prompt template libraries.
Attempting to store these corpora inside PostgreSQL JSONB columns leads straight to the 256MB cap, GIN index stalls, and high database maintenance costs. Moving them to commodity object storage (like AWS S3 or Google Cloud Storage) solves the raw byte problem but creates a retrieval gap: the files remain inert, requiring custom vector databases, chunking pipelines, and synchronization scripts to make them searchable.
A more effective pattern is placing the agent corpus into an intelligent workspace platform like Fast.io:
- Automatic Hybrid Indexing via Intelligence Mode: In Fast.io, files stored in an org-owned workspace are automatically indexed for hybrid search, combining full-text keyword matching with semantic vector retrieval. An agent searching a
100MBcollection of API specifications or technical manuals does not need the entire file loaded into its context window or stored in a database column. The agent queries the workspace and retrieves only the precise excerpts needed for its turn. - Structured Extraction with Metadata Views: For teams that need structured relational querying across semi-structured files, Fast.io's Metadata Views turn documents into live, queryable tables. Users describe fields in plain language, and the system populates typed schemas (Text, Integer, Decimal, Boolean, URL, JSON, Date & Time) across PDFs, spreadsheets, and Word documents without OCR templates or manual parsing rules.
- Direct MCP Integration for AI Workflows: Fast.io provides remote Model Context Protocol (MCP) server endpoints running over Streamable HTTP. Coding agents connect via
https://mcp.fast.io/mcp/code, while Claude and general agents connect viahttps://mcp.fast.io/mcp/tools(refer to the setup documentation for configuration instructions). Agents read, write, and search persistent workspace files directly through standard tool calls. - Per-File Version History and Audit Logs: Every file update retains full version history, protecting against concurrent overwrite conflicts when multiple agents collaborate on a shared project. The append-only audit log records every action for full operational visibility.
Monthly plans start with a 14-day free trial, which requires a credit card. Subscription tiers scale across Starter ($9.99/mo for 3 seats, 250 GB storage, 5 workspaces, and 100,000 credits), Business ($49.99/mo for 10 seats, 5 TB storage, 50 workspaces, and 600,000 credits), and Enterprise ($199.99/mo for 30 seats included, 25 TB storage, 200 workspaces, and 3,000,000 credits) on the pricing page. Credits meter AI operations; overage is $10 per 100,000 credits. Additional storage is billed at 1.5 cents per GB a month and extra bandwidth at 4 cents per GB.
How to Audit and Optimize PostgreSQL JSONB Tables in Production
If your application currently stores large JSONB documents in PostgreSQL, running an audit of your storage footprint and query performance will identify bloat and prevent out-of-memory errors before hitting the 256MB cap.
1. Identify Bloated JSONB Rows
Use PostgreSQL's built-in pg_column_size() function to evaluate the physical stored byte size of your JSONB column across the table:
SELECT
count(*) AS total_rows,
pg_size_pretty(avg(pg_column_size(payload))::bigint) AS avg_size,
pg_size_pretty(max(pg_column_size(payload))) AS max_size,
count(*) FILTER (WHERE pg_column_size(payload) > 1048576) AS rows_over_1mb,
count(*) FILTER (WHERE pg_column_size(payload) > 10485760) AS rows_over_10mb,
count(*) FILTER (WHERE pg_column_size(payload) > 104857600) AS rows_over_100mb
FROM app_data;
If max_size approaches 200MB, or if thousands of rows exceed 1MB, your table is experiencing heavy TOAST reliance and is at risk of breaching the 256MB container cap during uncompressed serialization.
2. Measure Table, TOAST, and Index Disk Footprint
Inspect how much physical storage is consumed by the table heap, its auxiliary TOAST table, and associated indexes:
SELECT
c.relname AS table_name,
pg_size_pretty(pg_relation_size(c.oid)) AS heap_size,
pg_size_pretty(pg_relation_size(c.reltoastrelid)) AS toast_size,
pg_size_pretty(pg_total_relation_size(c.reltoastrelid)) AS total_toast_with_index,
pg_size_pretty(pg_indexes_size(c.oid)) AS index_size,
pg_size_pretty(pg_total_relation_size(c.oid)) AS grand_total
FROM pg_class c
WHERE c.relname = 'app_data';
In tables with bloated JSONB columns, toast_size and index_size frequently account for the vast majority of grand_total.
3. Switch TOAST Compression to LZ4 (PostgreSQL 14+)
Starting with PostgreSQL 14, administrators can configure LZ4 compression for TOAST-able columns. LZ4 provides substantially faster compression and decompression speeds compared to PostgreSQL's default pglz algorithm, substantially reducing CPU detoasting overhead during query execution:
-- Alter existing column to use LZ4 compression
ALTER TABLE app_data ALTER COLUMN payload SET COMPRESSION lz4;
-- Existing rows remain compressed with pglz until updated.
-- To rewrite existing data using LZ4:
VACUUM FULL app_data;
4. Replace Full-Document GIN Indexes with Targeted Expressions
If your table has an unconstrained GIN index on a large JSONB column, drop it and replace it with targeted B-Tree indexes on the specific fields your application queries:
-- Drop the bloated full-document GIN index
DROP INDEX CONCURRENTLY IF EXISTS idx_app_data_payload_gin;
-- Add targeted B-Tree indexes for specific lookup fields
CREATE INDEX CONCURRENTLY idx_app_data_customer_id
ON app_data (((payload->>'customer_id')::uuid));
CREATE INDEX CONCURRENTLY idx_app_data_status
ON app_data (((payload->>'status')::text))
WHERE (payload->>'status') IS NOT NULL;
Targeted B-Tree expression indexes consume a tiny fraction of the disk space of a full GIN index, update in microseconds during writes, and completely avoid the gin_pending_list_limit write stalls.
Sources
References used to verify factual claims in this guide.
-
PostgreSQL TOAST limits the logical size of any value of a TOAST-able data type to 1 GB.
-
A jsonb_ops GIN index creates independent index items for each key and value in the data, while jsonb_path_ops creates items only for each value.
Frequently Asked Questions
What is the maximum size of a JSONB column in PostgreSQL?
The maximum size of a single JSONB value in PostgreSQL is 268,435,455 bytes (approximately `256MB`). This limit is enforced by the internal 28-bit length and offset mask (JENTRY_OFFLENMASK) within PostgreSQL's binary JSONB container header.
Can PostgreSQL JSONB exceed `256MB`?
No, an individual JSONB value cannot exceed `268,435,455 bytes` decompressed. Attempting to store or aggregate a JSONB document beyond this boundary raises a PROGRAM_LIMIT_EXCEEDED error. For larger payloads up to `1GB`, you must use plain text JSON, text, bytea, or external file storage.
How does PostgreSQL TOAST handle large JSONB documents?
When a table row exceeds `2 KB`, PostgreSQL's TOAST storage engine compresses the JSONB value and moves it out-of-line into an auxiliary TOAST table, splitting the payload into chunks of roughly 2000 bytes. The main table row retains only an 18-byte pointer to the out-of-line data.
Why does PostgreSQL allow `1GB` for text columns but only `256MB` for JSONB?
PostgreSQL variable-length storage (varlena) supports up to `1GB` by allocating 30 bits of its 4-byte header to length tracking. While text and plain JSON store raw characters using that 30-bit word, JSONB parses data into a binary tree whose JEntry headers allocate only 28 bits to offsets and lengths.
What error occurs when a JSONB document exceeds `256MB` in PostgreSQL?
When a JSONB document exceeds `268,435,455 bytes`, PostgreSQL rejects the query with the error message: 'ERROR: total size of jsonb array elements exceeds the maximum of `268,435,455 bytes`' or 'ERROR: string too long to represent as jsonb string' with SQLSTATE 54000.
How does GIN indexing affect write performance on large JSONB columns?
Default GIN indexes (jsonb_ops) extract every key and value from a JSONB document into individual index items. On multi-megabyte documents, a single write generates hundreds of thousands of postings, rapidly blowing past the `4MB` gin_pending_list_limit and causing multi-second write stalls.
What is the difference between jsonb_ops and jsonb_path_ops for large documents?
The jsonb_ops operator class indexes each key and value independently, supporting key existence and containment operators. The jsonb_path_ops operator class hashes the full key path and value together into a 32-bit hash, producing an index that is three to six times smaller and much faster for containment queries.
What is the recommended maximum operational size for a JSONB column?
Database engineering best practices recommend keeping individual JSONB documents below `100 KB` in PostgreSQL. Documents exceeding `100 KB` incur heavy TOAST decompression penalties, increase memory churn in backend worker processes, and cause severe write amplification during updates.
Related Resources
Stop Forcing Multi-Megabyte Documents and State into PostgreSQL JSONB
Store dynamic prompt libraries, agent memory traces, and large document corpora in persistent Fast.io workspaces. Your applications and AI agents search indexed files through MCP without hitting the `256MB` container cap or stalling database queries. Monthly plans start with a 14-day free trial, which requires a credit card.