Skip to main content

s4db - Simple DB on S3

A lightweight embedded key-value store where keys are strings and values are strings or bytes. Data is written to numbered binary files on disk and synced to S3. Values are Snappy-compressed when compression helps. An in-memory index tracks the exact file and byte offset for every live key, so reads never scan - they seek directly. Concurrent writers are safe: each writer's files live in their own namespace, and index commits are compare-and-swapped (per-key last-writer-wins).

Installation

pip install s4db

s4db requires python-snappy, which links against the native Snappy C library.

# macOS
brew install snappy

# Ubuntu / Debian
apt-get install libsnappy-dev

Quick start

from s4db import S4DB

db = S4DB(
    bucket="my-bucket",
    prefix="my-db/",              # S3 key prefix; include a trailing slash
    region_name="ap-south-1",     # any extra kwargs go to boto3.client("s3", ...)
)

db.put({"hello": "world"})
print(db.get("hello"))  # "world"
db.delete(["hello"])
print(db.get("hello"))  # None

On __init__, the index is downloaded from S3 into memory. If no index exists, the database starts empty. No local directory is created or used until a write operation (put / delete) is called.

When to use s4db

s4db fits workloads that need durable key-value semantics on ephemeral compute - without a running database.

Good fits

  • Lambda / serverless state - load index on cold start (~50 ms S3 RTT), mutate in memory, upload() once before return. No VPC, no connections.
  • Batch pipeline checkpoints - write processed keys as you go, upload() periodically. Restart resumes from the existing index.
  • Read-heavy config / lookup tables - write once, download() on each worker at startup, all reads from local disk at ~0.009 ms.
  • ETL joins - pre-load a lookup table into s4db, upload to S3, workers download at startup. ~0.009 ms median lookup vs ~50 ms per S3 GET.
  • Experiment tracking - log metrics and artefacts to s4db, sync to S3 at end of run. Queryable by key from any machine.

Not a fit - key space doesn't fit in RAM, range queries needed, cross-key transactions, or sub-ms reads without a warm local copy. Concurrent writers on one prefix are supported since 0.9.0 (writer-namespaced files + compare-and-swap index commits with per-key last-writer-wins), but if you need transactional multi-writer semantics, use a database.

Key numbers (real S3, ap-south-1 / ap-east-1):

Operation Latency / throughput
get() - local disk ~0.009 ms median
get() - S3 range request ~49 ms median, ~113 ms p99
put() batched (1000 keys/call) ~12 769 keys/sec
s4db vs naive one-object-per-key writes 181x faster, 1000x fewer PUTs, 49x cheaper

Full numbers in Benchmarks.

S3 layout

Given bucket="my-bucket" and prefix="my-db/":

my-bucket/
  my-db/
    index.idx
    data_000001.s4db
    data_000002.s4db
    ...

Data files are named data_XXXXXXXX_NNNNNN.s4db - an 8-hex-digit writer namespace plus a zero-padded sequence number (legacy data_NNNNNN.s4db files from pre-0.9 databases remain readable). The index file is always index.idx.

Typical workflows

Read-only from S3 - no local directory needed

db = S4DB("my-bucket", "my-db/")
# Index is loaded from S3 into memory; gets use S3 range requests
print(db.get("some-key"))
print(db.keys())

Write locally, sync later

db = S4DB("my-bucket", "my-db/", local_dir="/tmp/my-db")
db.put({"a": "1", "b": "2"})
db.delete(["a"])
db.upload()   # push everything to S3 when done

Write without specifying local_dir (temp dir created automatically)

db = S4DB("my-bucket", "my-db/")
db.put({"a": "1"})   # temp dir created here on first write
db.upload()

Full local mirror

db = S4DB("my-bucket", "my-db/", local_dir="/tmp/my-db")
db.download()   # pull everything local
print(db.get("some-key"))   # served from disk, no S3 call

Iterate over all key/value pairs

# One S3 range request per key (no local files needed)
db = S4DB("my-bucket", "my-db/")
for key, value in db.iter():
    print(key, value)

# Download missing files first, then read entirely from disk
db = S4DB("my-bucket", "my-db/", local_dir="/tmp/my-db")
for key, value in db.iter(local=True):
    print(key, value)

Periodic compaction

db = S4DB("my-bucket", "my-db/", local_dir="/tmp/my-db")
db.download()   # ensure all data files are present
db.compact()    # rewrite, clean up S3, upload new files

Index recovery

db = S4DB("my-bucket", "my-db/", local_dir="/tmp/my-db")
db.rebuild_index(from_s3=True)  # download data files (no index needed) and replay them
db.upload()                     # push repaired index (and any orphaned files) to S3

Documentation

Edge cases and gotchas

  • local_dir is not required for read-only usage. A temporary directory is created automatically on the first put() or delete() call if none was provided.
  • put() and delete() do not push to S3 automatically. Call upload() explicitly.
  • get() on a key whose data file is not local will make a ranged S3 request on every call. Use download() if you expect repeated access to the same keys.
  • compact() and rebuild_index() require all data files to be present in local_dir. Always run download() first if you are not certain the local directory is up to date.
  • delete() silently skips keys that are not in the index. It never writes unnecessary tombstones.
  • If the process is interrupted during put() or delete(), the data file may contain entries that the index does not reference. rebuild_index() will recover them.
  • max_file_size is a soft limit. An entry is never split across files, but a single oversized entry can make a file exceed the limit slightly.
  • iter(local=False) makes one S3 range request per key. For large datasets prefer iter(local=True) to batch the S3 downloads upfront.
  • iter(local=True) only downloads files referenced by the current in-memory index. Files that contain only deleted or overwritten entries are not downloaded.

Dependencies

Development

pip install -e ".[dev]"
pytest tests/ -v

Tests use moto to mock S3 - no real AWS credentials required.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

s4db-0.10.0.tar.gz (31.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

s4db-0.10.0-py3-none-any.whl (23.0 kB view details)

Uploaded Python 3

File details

Details for the file s4db-0.10.0.tar.gz.

File metadata

  • Download URL: s4db-0.10.0.tar.gz
  • Upload date:
  • Size: 31.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for s4db-0.10.0.tar.gz
Algorithm Hash digest
SHA256 a94e66f380c4dd74c565d8516b7c56b099c899c291f7e69f341d4abfb9f9f62d
MD5 fc72ba418e48218d55bae97a88afc989
BLAKE2b-256 2c8d30f6efc367bc4f901d8f942e5429084b1be41e9af87aaf5930d010f695a3

See more details on using hashes here.

File details

Details for the file s4db-0.10.0-py3-none-any.whl.

File metadata

  • Download URL: s4db-0.10.0-py3-none-any.whl
  • Upload date:
  • Size: 23.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for s4db-0.10.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9f6e3b2910901a758ba6fd75450b5e3ee00fcbad8a18014b76f6b6035feecaea
MD5 968b4dafbcea99cb23e56f68908fa3f2
BLAKE2b-256 6af044793bc412defba14a5f95758bc896d760ad0aa96dc50b1f4b6de07e028b

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.10.0 This release

2 files

0.8.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page