Summary
Writing to an Iceberg table whose partition spec produces many partition tuples per statement (e.g. partition_by = 'source_id, day(created_at), bucket(32, entity_id)') fails with:
ERROR: exceeded maxAllocatedDescs (NNN) while trying to open file "..."
The root cause is a mismatch between pg_lake's flush guard and PostgreSQL's per-backend transient file descriptor budget:
- The partitioned dest receiver keeps one open CSV staging file per distinct partition tuple in the statement, and only flushes a writer when the number of active subreceivers exceeds
pg_lake_table.max_open_files_for_partitioned_write, whose default is 5000 (partitioned_dest_receiver.c#L108, GUC defined at pg_lake_table/src/init.c#L290). Each staging writer holds a descriptor obtained via AllocateFile() (csv_writer.c#L394).
- PostgreSQL caps the number of concurrently allocated transient descriptors per backend at
max_safe_fds / 3 (reserveAllocatedDesc() in fd.c#L2508, cap applied at fd.c#L2543-L2548). max_safe_fds is derived from max_files_per_process at postmaster start, and per the PostgreSQL docs on max_files_per_process: "The default is one thousand files." With the default, max_safe_fds ends up around ~960, so the transient descriptor cap is ~320 per backend.
Since 5000 ≫ ~320, the pg_lake flush guard can never trigger before PostgreSQL raises the error. On any stock-configured PostgreSQL, a single partitioned INSERT/COPY touching more than ~300 partition tuples fails.
Reproduction
CREATE TABLE events (source_id int, entity_id bigint, created_at timestamptz, payload text)
USING iceberg
WITH (partition_by = 'source_id, day(created_at), bucket(32, entity_id)');
-- one statement spanning ~10+ sources x 32 buckets => >320 partition tuples
INSERT INTO events SELECT s, e, now(), 'x'
FROM generate_series(1, 15) s, generate_series(1, 10000) e;
With default max_files_per_process = 1000, this reliably fails with exceeded maxAllocatedDescs. Reducing the batch size does not help unless the batch is also sliced by partition columns, because the open-file count depends on distinct partition tuples per statement, not row count.
A practical consequence worth noting: users work around this by micro-batching their loads (slicing per partition column value + small row batches), which multiplies Iceberg commits/snapshots by orders of magnitude and creates severe downstream metadata bloat and VACUUM (snapshot expiry) load.
Suggested fix
Any of:
- Lower the default of
pg_lake_table.max_open_files_for_partitioned_write to a value that is safe under PostgreSQL defaults (e.g. 200–250).
- Derive the effective cap at runtime, e.g.
Min(MaxOpenFilesForPartitionedWrite, max_safe_fds / 3 - headroom), so the flush-largest-partition path engages instead of erroring.
- At minimum, document that
max_files_per_process must be raised to ≥ 3× the configured max_open_files_for_partitioned_write for the default to be usable.
Option 2 seems most robust since max_safe_fds already accounts for the actual environment.
Summary
Writing to an Iceberg table whose partition spec produces many partition tuples per statement (e.g.
partition_by = 'source_id, day(created_at), bucket(32, entity_id)') fails with:The root cause is a mismatch between pg_lake's flush guard and PostgreSQL's per-backend transient file descriptor budget:
pg_lake_table.max_open_files_for_partitioned_write, whose default is 5000 (partitioned_dest_receiver.c#L108, GUC defined atpg_lake_table/src/init.c#L290). Each staging writer holds a descriptor obtained viaAllocateFile()(csv_writer.c#L394).max_safe_fds / 3(reserveAllocatedDesc()infd.c#L2508, cap applied atfd.c#L2543-L2548).max_safe_fdsis derived frommax_files_per_processat postmaster start, and per the PostgreSQL docs on max_files_per_process: "The default is one thousand files." With the default,max_safe_fdsends up around ~960, so the transient descriptor cap is ~320 per backend.Since 5000 ≫ ~320, the pg_lake flush guard can never trigger before PostgreSQL raises the error. On any stock-configured PostgreSQL, a single partitioned INSERT/COPY touching more than ~300 partition tuples fails.
Reproduction
With default
max_files_per_process = 1000, this reliably fails withexceeded maxAllocatedDescs. Reducing the batch size does not help unless the batch is also sliced by partition columns, because the open-file count depends on distinct partition tuples per statement, not row count.A practical consequence worth noting: users work around this by micro-batching their loads (slicing per partition column value + small row batches), which multiplies Iceberg commits/snapshots by orders of magnitude and creates severe downstream metadata bloat and VACUUM (snapshot expiry) load.
Suggested fix
Any of:
pg_lake_table.max_open_files_for_partitioned_writeto a value that is safe under PostgreSQL defaults (e.g. 200–250).Min(MaxOpenFilesForPartitionedWrite, max_safe_fds / 3 - headroom), so the flush-largest-partition path engages instead of erroring.max_files_per_processmust be raised to ≥ 3× the configuredmax_open_files_for_partitioned_writefor the default to be usable.Option 2 seems most robust since
max_safe_fdsalready accounts for the actual environment.