How to configure S3 blob tiering#
This guide shows you how to configure tiered blob storage so that small blobs stay in PostgreSQL and large blobs go to S3.
Install the S3 extra#
pip install zodb-pgjsonb[s3]
# or
uv pip install zodb-pgjsonb[s3]
This installs the zodb-s3blobs dependency with the boto3 S3 client.
Add S3 configuration to ZConfig#
Add the S3 keys to your <pgjsonb> section in zope.conf:
%import zodb_pgjsonb
<zodb_db main>
<pgjsonb>
dsn dbname=zodb user=zodb host=localhost port=5432
s3-bucket-name my-zodb-blobs
s3-region us-east-1
s3-prefix production/
blob-threshold 100KB
blob-cache-dir /var/cache/zodb-blobs
blob-cache-size 1GB
</pgjsonb>
</zodb_db>
S3 configuration keys#
Key |
Default |
Description |
|---|---|---|
|
none |
S3 bucket name (setting this enables S3 tiering) |
|
none |
AWS region name |
|
none |
S3 endpoint URL (for Garage, Ceph, or other S3-compatible stores) |
|
none |
AWS access key ID (uses boto3 credential chain if omitted) |
|
none |
AWS secret access key (uses boto3 credential chain if omitted) |
|
|
Enable SSL for S3 connections |
|
empty |
S3 key prefix for namespace isolation |
|
zodb-s3blobs default |
Connect timeout (seconds) for uploads, deletes and listings |
|
zodb-s3blobs default |
Socket read timeout (seconds) for uploads, deletes and listings |
|
zodb-s3blobs default |
Connect timeout (seconds) for downloads and HEAD requests |
|
zodb-s3blobs default |
Socket read timeout (seconds) for downloads and HEAD requests |
|
zodb-s3blobs default |
Total attempts per download and HEAD request |
Blob storage keys#
Key |
Default |
Description |
|---|---|---|
|
|
Blobs larger than this go to S3 |
|
auto |
Local cache directory for S3 blobs |
|
|
Maximum size of the local blob cache |
Choose a blob-threshold strategy#
The blob-threshold setting controls how blobs are distributed between PostgreSQL and S3.
PG-only mode (no S3):
Omit s3-bucket-name entirely.
All blobs stay in PostgreSQL bytea columns regardless of size.
Tiered mode (default with S3):
Set blob-threshold to a size like 100KB (the default).
Blobs smaller than this stay in PostgreSQL for fast access.
Blobs at or above this threshold go to S3.
S3-only mode:
Set blob-threshold to 0.
All blobs go to S3, keeping the PostgreSQL database compact.
Set up a local blob cache#
Set blob-cache-dir to a local directory for caching S3 blobs on disk.
This avoids repeated S3 downloads for frequently accessed blobs.
Set blob-cache-size to the maximum disk space for the cache.
The cache uses LRU eviction when the limit is reached.
For production, always configure a blob cache directory to reduce S3 latency and cost.
Tune S3 timeouts#
Leave the S3 timeout keys unset to use the zodb-s3blobs defaults. Set them only to fix one of the problems below. Refer to Configuration options for the keys and their defaults, and to Blob downloads in the request path for why downloads and uploads have separate settings.
If blob requests fail with IdleInTransactionSessionTimeout while a blob is downloaded from S3, lower the download worst case below ZODB_PGJSONB_IDLE_IN_XACT_TIMEOUT_MS (60 seconds by default).
The worst case is roughly 2 × s3-download-max-attempts × (s3-download-connect-timeout + s3-download-read-timeout).
If your S3 provider answers downloads slowly under load and they time out, raise s3-download-read-timeout, but keep the worst case below the PostgreSQL idle timeout.
If uploads time out under concurrent load, raise s3-read-timeout.
If a stalled upload blocks other writers for too long, lower it instead.
Set the keys in the <pgjsonb> section:
<pgjsonb>
dsn dbname=zodb user=zodb host=localhost
s3-bucket-name my-zodb-blobs
s3-download-read-timeout 15
s3-download-max-attempts 2
s3-read-timeout 300
</pgjsonb>
To take a value from the environment, use ZConfig’s $(NAME) syntax, for example s3-download-read-timeout $(S3_DOWNLOAD_READ_TIMEOUT).
The plain $NAME syntax only expands names defined with %define.
Use Garage for development#
Garage is a lightweight S3-compatible store.
Write a minimal garage.toml:
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "lmdb"
replication_factor = 1
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"
rpc_secret = "<output of: openssl rand -hex 32>"
[s3_api]
s3_region = "garage"
api_bind_addr = "[::]:3900"
root_domain = ".s3.garage.localhost"
Start a single-node Garage container.
--single-node applies the cluster layout, and --default-access-key / --default-bucket create the key and the bucket from the GARAGE_DEFAULT_* variables:
docker run -d --name garage \
-p 3900:3900 \
-v "$PWD/garage.toml:/etc/garage.toml:ro" \
-e GARAGE_DEFAULT_ACCESS_KEY=GK0123456789abcdef01234567 \
-e GARAGE_DEFAULT_SECRET_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
-e GARAGE_DEFAULT_BUCKET=zodb-blobs \
dxflrs/garage:v2.3.0 \
/garage server --single-node --default-access-key --default-bucket
Garage access keys are GK followed by 24 hex characters; secret keys are 64 hex characters.
Configure the storage to use Garage:
<pgjsonb>
dsn dbname=zodb user=zodb host=localhost port=5432
s3-bucket-name zodb-blobs
s3-endpoint-url http://localhost:3900
s3-region garage
s3-access-key GK0123456789abcdef01234567
s3-secret-key 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
s3-use-ssl false
blob-threshold 100KB
</pgjsonb>