Skip to content

pgduck_server: cap s3_uploader_thread_limit by host memory#433

Draft
sfc-gh-mslot wants to merge 1 commit into
mainfrom
mslot/s3-uploader-thread-limit
Draft

pgduck_server: cap s3_uploader_thread_limit by host memory#433
sfc-gh-mslot wants to merge 1 commit into
mainfrom
mslot/s3-uploader-thread-limit

Conversation

@sfc-gh-mslot

@sfc-gh-mslot sfc-gh-mslot commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

DuckDB's httpfs S3 uploader keeps up to s3_uploader_thread_limit multipart-upload part buffers in flight per open file (default 50), each ~80MB and allocated against DuckDB's memory_limit (MemoryTag::EXTENSION). The default part size is ~80MB because it derives from s3_uploader_max_filesize / s3_uploader_max_parts_per_file (800GB / 10000). On a small host (memory_limit 2GB) a single large snapshot writing to S3 can pin up to ~4GB in part buffers alone, hit the limit, and crash pgduck_server with an out-of-memory error. systemd restarts it and the snapshot retries into the same failure, so one oversized table can crash-loop the server for hours, aborting concurrent queries for every other mirror each time.

Solution

Set s3_uploader_thread_limit at startup instead of leaving it at the upstream default of 50. The part buffers are charged against memory_limit, so the cap is sized against that budget rather than physical RAM (the two can differ, e.g. a 2GB limit on a 4GB host). We read the effective memory_limit back from DuckDB and use ~2 threads per GiB, clamped to [2, 50] (never above the upstream default). Uploads are network-bound and run on detached threads independent of DuckDB's threads setting, so CPU count is not the constraint.

memory_limit s3_uploader_thread_limit Worst-case S3 buffers/file (default was 50)
≤1 GiB 2 (floor) ~160 MB ~4 GB
2 GiB 4 ~320 MB ~4 GB
4 GiB 8 ~640 MB ~4 GB
8 GiB 16 ~1.3 GB ~4 GB
16 GiB 32 ~2.6 GB ~4 GB
24 GiB 48 ~3.8 GB ~4 GB
≥25 GiB 50 (cap) ~4.0 GB ~4 GB

The SET GLOBAL runs after memory_limit is configured and before the init file is applied, so it reflects the effective limit and an operator can still override it:

# init_file_path: raise the cap on a memory-rich host
SET GLOBAL s3_uploader_thread_limit TO 64;

Test plan

  • Single TU and full pgduck_server build clean under the tree's -Wall/-Werror=… flags.
  • Started the built binary at several limits; init logs s3_uploader_thread_limit is set to: N and SELECT current_setting('s3_uploader_thread_limit') returns it: --memory_limit=2GB → 4, 512MB → 2 (floor), a large limit → 50 (cap).

@sfc-gh-mslot sfc-gh-mslot force-pushed the mslot/s3-uploader-thread-limit branch 2 times, most recently from 50f89b7 to b116337 Compare July 9, 2026 08:07
@sfc-gh-mslot sfc-gh-mslot marked this pull request as ready for review July 9, 2026 08:07
@sfc-gh-mslot sfc-gh-mslot force-pushed the mslot/s3-uploader-thread-limit branch from b116337 to bcb7389 Compare July 9, 2026 12:38
@sfc-gh-mslot sfc-gh-mslot marked this pull request as draft July 9, 2026 12:45
@sfc-gh-mslot sfc-gh-mslot force-pushed the mslot/s3-uploader-thread-limit branch from bcb7389 to 0dd7cce Compare July 9, 2026 12:49
}

{
uint64 s3UploaderThreadLimit = DefaultS3UploaderThreadLimit();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useful to pass in memoryLimit here?

DuckDB's httpfs uploader defaults to 50 concurrent multipart-upload part
buffers per file, each ~80MB and allocated against memory_limit. On a
small host (memory_limit 2GB) a single large snapshot write to S3 can pin
up to ~4GB and crash pgduck_server with an out-of-memory error, taking
down concurrent queries for other mirrors.

Read the effective memory_limit back from DuckDB and set
s3_uploader_thread_limit to ~2 per GiB of it, clamped to [2, 50]. Sizing
against the budget the buffers are charged to (not physical RAM, which can
differ) keeps the cap correct when memory_limit is set below RAM. Runs
after memory_limit is configured and before the init file, so it reflects
the real budget and operators can still override it.

Signed-off-by: Marco Slot <marco.slot@snowflake.com>
@sfc-gh-mslot sfc-gh-mslot force-pushed the mslot/s3-uploader-thread-limit branch from 0dd7cce to a4f1bda Compare July 9, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant