ZeroBucket
Your database. Your images. Zero buckets.
ZeroBucket is a database-native image storage library. It lets you store and retrieve images using the PostgreSQL database you already have, instead of standing up a separate object-storage service like S3.
from zerobucket import ZeroBucket
images = ZeroBucket(database_url="postgresql://...")
image_id = images.put("avatar.jpg")
image = images.get(image_id)
print(image.mime_type) # "image/jpeg"
print(image.size_bytes) # 1116478
print(image.data) # raw bytes, ready to serve
Full documentation, architecture notes, and benchmark results live in the GitHub repository.
Installation
pip install zerobucket
Requires Python 3.10+ and PostgreSQL 13+ (uses gen_random_uuid(), built
in since Postgres 13).
Quick reference
| Method | What it does |
|---|---|
images.put(image, filename=None, optimize=False, max_width=None, format=None, quality=None) |
Validates, optionally optimizes, checksums, and stores an image. Accepts a file path, raw bytes, or a file-like object (including framework upload objects). Returns the new image's id. |
images.get(image_id) |
Returns an Image (data, mime_type, filename, width, height, size_bytes, checksum_sha256). Raises ImageNotFoundError if missing. |
images.get_stream(image_id, chunk_size=1MB) |
Like get(), but returns an iterator of chunks instead of one bytes object -- avoids holding the full image in Python memory at once. Raises ImageNotFoundError if missing. |
images.stream_to(image_id, destination, chunk_size=1MB) |
Writes an image's bytes directly to destination (anything with .write(bytes)), chunk by chunk. Returns total bytes written. |
images.metadata(image_id) |
Same fields as get() but without the raw bytes -- cheap existence/info check. |
images.exists(image_id) |
Returns True/False. |
images.delete(image_id) |
Deletes the image. Returns True if it existed. |
images.close() |
Releases database connections. ZeroBucket also works as a context manager. |
from zerobucket import ZeroBucket, ImageNotFoundError, ImageValidationError
images = ZeroBucket(
database_url="postgresql://user:pass@localhost/mydb",
max_bytes=8 * 1024 * 1024, # default: 8MB
)
image_id = images.put("photo.jpg")
image_id = images.put(open("photo.jpg", "rb"))
image_id = images.put(request.files["avatar"]) # framework upload objects
image = images.get(image_id)
info = images.metadata(image_id)
try:
images.get("nonexistent-id")
except ImageNotFoundError:
...
try:
images.put("not-actually-an-image.txt")
except ImageValidationError:
...
Serving from a web API
from flask import Flask, Response
app = Flask(__name__)
images = ZeroBucket(database_url=DATABASE_URL)
@app.route("/images/<image_id>")
def serve_image(image_id):
image = images.get(image_id)
return Response(image.data, mimetype=image.mime_type)
Streaming reads for large files
for chunk in images.get_stream(image_id, chunk_size=1024 * 1024):
response.write(chunk)
# or:
total_bytes = images.stream_to(image_id, response, chunk_size=1024 * 1024)
Avoids holding the full image in Python memory at once (one chunk at a
time instead), via ranged substring() queries. This is a Python-side
memory optimization only -- Postgres still handles the full stored value
the way it always has, and this isn't HTTP range/partial-content
support. See the full explanation
on GitHub, including the mid-stream-delete safety behavior and how
put() bounds memory for rejected oversized file-like uploads.
Optimizing images (compression)
Off by default -- put() stores your exact input bytes unless you opt in:
image_id = images.put(
"photo.jpg",
optimize=True,
max_width=1600, # downscale if wider, aspect ratio preserved
format="webp", # optional re-encode target: "jpeg", "png", "webp"
quality=90, # 1-100, JPEG/WebP only; omit for data-backed defaults
)
Quality defaults (JPEG=90, WebP=88) are backed by measured SSIM data
across multiple content types -- typical photos see 70-95% size
reduction with no visible quality loss. One thing this data caught:
don't target format="jpeg" for flat/graphic content (screenshots,
logos) -- it can make them larger, not smaller. See
COMPRESSION_RESULTS.md
on GitHub for the full methodology.
What it validates
- Format: JPEG, PNG, WebP built in, plus HEIC/HEIF (iPhone photos) via
the optional
pip install zerobucket[heic]extra -- detected from actual file content, never from filename extension or a client-suppliedContent-Typeheader. - Corruption: truncated or malformed images are decoded and rejected before they reach the database.
- Decompression bombs: a tiny compressed file that decodes to an enormous pixel grid is rejected, not silently allocated.
- Size: configurable via
max_bytes(default 8MB) -- see the benchmark results for why.
Transactions
By default, put()/get()/delete() each use their own independent
database connection -- not your application's own transaction, even
against the same database. Pass your own open psycopg connection via
connection= to make a write participate in your transaction (e.g. "user
- avatar, atomically, or neither"). See the Transactions section on GitHub for a worked example -- this was verified by direct experiment during development, not assumed.
Batch operations
results = images.put_many([open("a.jpg", "rb"), open("b.jpg", "rb")])
fetched = images.get_many([id1, id2])
deleted = images.delete_many([id1, id2])
Best-effort, not all-or-nothing -- check .success/.error per item.
get_many/delete_many are genuine single-query batch operations; see
the full docs
on GitHub for what's actually batched vs. still per-item.
Retry behavior
Transient errors (connection drops, deadlocks, serialization failures)
are automatically retried with exponential backoff (max_retries=3 by
default). Important: passing your own connection= disables
automatic retry for that call -- see the
full explanation
on GitHub for why that's a deliberate safety rule, not an oversight.
Custom content types (PDFs and beyond)
from zerobucket.validators.pdf import PDFValidator
doc_id = images.put(pdf_bytes, validator=PDFValidator())
doc = images.get(doc_id) # no special handling needed -- ever
Everything (transactions, retry, batch ops) works identically regardless of which validator produced a row. See the full explanation on GitHub for why this is a pluggable hook rather than native PDF support built into the core.
CLI
zerobucket init # create the schema if missing
zerobucket info # image count, total size, breakdown by format
zerobucket verify # re-checksum every image to detect corruption
Takes --database-url or reads ZEROBUCKET_DATABASE_URL from the
environment. verify exits non-zero on any mismatch, so it's usable in
cron/CI. See the
full CLI docs on
GitHub.
Deduplication
images = ZeroBucket(database_url=DATABASE_URL, dedup=True)
id1 = images.put("photo.jpg")
id2 = images.put("photo.jpg") # identical content -- stored exactly once, referenced twice
Opt-in (dedup=True), uses separate tables from classic mode, so it's
safe to add later without touching existing data. See the
full explanation
on GitHub, including the migration path for existing classic-mode data.
Tuning and observability
images = ZeroBucket(
database_url=DATABASE_URL,
pool_max_size=10, # default: 5
on_operation=lambda e: print(e.operation, e.duration_seconds, e.success),
)
Pool sizing (pool_min_size/pool_max_size/pool_timeout) was
previously hardcoded, now configurable. on_operation fires after every
storage operation with timing, retry count, and success/failure --
callback exceptions are caught and never break a real operation. See the
full explanation
on GitHub.
Limitations (read before using in production)
- Not built for large files or high-volume media.
get_stream()avoids holding a full image in Python memory during a read (see above), but Postgres itself still handles the full stored value the same way it always has -- no true server-side memory reduction, no HTTP range requests, no CDN. - Deduplication is opt-in, not automatic. Default
dedup=Falsestores every upload as a separate row; passdedup=Truefor content-addressed, reference-counted storage (see above). - PostgreSQL only, for now. The storage layer is abstracted for future adapters, but only Postgres exists today.
See the full README and roadmap on GitHub for more detail.
License
MIT -- see LICENSE.
Release files for zerobucket 0.11.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| zerobucket-0.11.0.tar.gz | 59.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| zerobucket-0.11.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 101.6 kB
Release files / zerobucket-0.11.0.tar.gz
| Download URL | zerobucket-0.11.0.tar.gz |
|---|---|
| Size | 59.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
93180ba762ed3ed4fe4c564735879f250d0a6d1db047d35dbe7a5868d94f94a7
|
|
BLAKE2b-256 checksum How to use checksums |
c20bae5d360ebdb59bcaf6ba6399497ca64e0289dd25aeaece9a68eaf42aa8f8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.9
|
Release files / zerobucket-0.11.0-py3-none-any.whl
| Download URL | zerobucket-0.11.0-py3-none-any.whl |
|---|---|
| Size | 42.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ce2d63979b69e0a3d7b382b47e2d6cadd28751d484ad2afb1bf064da11db4b10
|
|
BLAKE2b-256 checksum How to use checksums |
4d706f85448f8eb744f90a842ed904a981dd8a30205662031bccde40b79dcf02
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.9
|