Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

dura.stream

dura.stream

Minimal durable streaming on local disk. Append-only, crash-safe, tailable streams you import into any Python app, no server, no dependencies: stdlib only.

Install

uv add durastream

Requires Python 3.12+. No runtime dependencies: pure Python, stdlib only.

Quick start

from durastream import Store

store = Store("./data")
stream = store.create("orders", content_type="text/plain")

stream.append(b"order-1")  # -> 1  (new next_offset)
stream.append(b"order-2")  # -> 2
stream.append_many([b"o-3", b"o-4"])  # -> 4  batch: one fsync for the whole list

stream.read(0)  # [b"order-1", b"order-2", b"o-3", b"o-4"]  all records from offset 0
stream.read(1)  # [b"order-2", b"o-3", b"o-4"]              from offset 1 to tail
stream.read(0, 1)  # [b"order-1"]                           half-open [start, end)

stream.next_offset  # 4                                     record count
stream.content_type  # "text/plain"

Durability is per-flush: append() writes a length+CRC-framed record and fsyncs before returning; append_many() writes the whole list in one fsync (much faster for bulk ingest, same durability guarantee once it returns). After a crash, reopening the store rebuilds state by scanning the log, a torn or corrupt tail record is dropped, the intact prefix survives.

store2 = Store("./data")
stream = store2.open("orders")
stream.read(0)  # [b"order-1", b"order-2", b"o-3", b"o-4"]  recovered from the log

Tailing (tail -f)

subscribe() yields existing records from an offset, then blocks and yields new ones as they're appended:

import threading


def worker():
    for record in stream.subscribe(0):
        print("got", record)


threading.Thread(target=worker, daemon=True).start()
stream.append(b"live-1")

The iterator ends once the stream is closed and the consumer has caught up.

Async

AsyncStore mirrors the sync API with the same names, awaitable:

from durastream import AsyncStore

store = AsyncStore("./data")
stream = await store.create("chat")

await stream.append(b"hello ")
await stream.read(0)  # [b"hello "]

async for record in stream.subscribe(0):  # replay, then tail (pushed, no polling)
    print(record)

subscribe gets each record the moment it is appended in this process, from any thread, handed over by the append itself (no polling, no disk read), so it stays cheap with many clients per stream. await stream.wait(offset) is the same wake-up for your own read loop (see demos/fastapi_resume.py). For LLM output, AsyncBatchWriter groups tokens into one fsync per 20 ms window without weakening durability: readers only ever see records that are already on disk.

from durastream import AsyncBatchWriter

async with AsyncBatchWriter(stream) as w:
    async for token in llm_tokens():
        w.write(token)  # returns at once
# every token is durable here
await stream.close()

Demo

demos/bulk_stream.py bulk-streams 100k JSON readings through one stream while a second thread tails them live, then reopens the store from disk to prove the data survived a restart:

make demo

Output (numbers vary by machine):

ingesting 100,000 readings in batches of 1,000 ...
  tailed  10,000     0.5 MB     409,514 rec/s
  ...
ingested 100,000 readings (5.1 MB) in 0.22s  ->  447,142 rec/s, 23 MB/s
reopened from disk: next_offset=100,000  (DS token 00000000000000100000)
resumed read at offset 50,000: [b'{"id": 50000, ...}', b'{"id": 50001, ...}']
durable OK - data survived the restart.

demos/append_vs_batch.py (make demo-bench) contrasts append() (one fsync per record) with append_many() (one fsync per batch). Same durability, ~27x faster here (more on platforms with a costlier fsync).

demos/restart_stream.py (make demo-restart) ingests 10M records across 5 stop/start sessions, resuming at the persisted offset each time. Proof the log survives repeated restarts (10M in ~12s here). It also prints the reopen scan time per session, which grows linearly with log size (the O(n) recovery cost a persisted index would remove).

demos/work_queue.py (make demo-queue) is a durable job queue: a worker consumes jobs, checkpoints its offset to a file, "crashes" at 60%, then restarts and resumes from the checkpoint. Every job processed at least once, none lost.

demos/ledger.py (make demo-ledger) is an event-sourced bank account: it appends deposit/withdraw events, then reopens and rebuilds the balance purely by replaying the log (read(0)), including a point-in-time balance query. The log is the source of truth; state is derived.

demos/concurrent_users.py (make demo-concurrent) runs 40 concurrent users on one AsyncStore in a single process: first a stream per user (no cross-talk), then all 40 writing into one shared stream while a consumer tails the interleaved firehose (nothing lost). In-process concurrency is fully coordinated by the per-stream lock; cross-process writes to the same stream need a single writer.

Closing & deleting

stream.close()  # no more appends; reads still work
stream.append(b"x")  # raises StreamClosed
stream.closed  # True (persisted)

store.delete("orders")  # removes the log file + metadata row
store.list()  # ["other-stream", ...]

Stream names are lowercase letters, digits, ., _ and - (so two names can never share a file on a case-insensitive disk). All errors derive from DurastreamError: StreamClosed, StreamLocked (another process owns the stream) and CorruptStream (bytes on disk fail their checksum).

Offsets

Offset = logical record index (0-based). next_offset is the record count and the position the next append lands at. Helpers convert to/from the DS wire token format:

from durastream import to_token, from_token

to_token(1)  # "00000000000000000001"
from_token("-1", next_offset)  # 0            (start of stream)
from_token("now", next_offset)  # next_offset  (current tail)
from_token("00000000000000000003", next_offset)  # 3

On-disk layout

data/
  meta.db                 SQLite: name, content_type, closed, created_at
  streams/
    orders.log            append-only frames: [u32 len][u32 crc32][payload]...

CRC is CRC-32/ISO-HDLC (the same as zlib.crc32). Writes to a stream are serialized by an in-process lock; SQLite runs in WAL mode.

Concurrency

dura.stream is a single-process engine. Within one process it is fully concurrent: many threads or coroutines, many streams, or many writers into one shared stream are all coordinated by a per-stream lock, so offsets stay consistent and no data is lost (see make demo-concurrent). Use AsyncStore from async code.

Across separate processes the rule is one writer per stream, and it is enforced: the first process to open a stream takes an OS file lock (flock) on its log. Other processes get a read-only view (stream.writable is False): reads work, append/close/delete raise StreamLocked, and they never touch the file. A read-only view is a snapshot; open it again from a new Store to see new records. The lock is released when the writer closes the store or exits (even on a crash). A forked child never writes through its parent's streams; open stores after forking. On Windows there is no flock, so the rule is not enforced there.

Develop

You need uv.

git clone git@github.com:terminalkitten/durastream.git && cd durastream
uv sync          # .venv + dev deps

Layout:

src/durastream/          the package: storage engine, async API, fan-out hub
tests/                   test suite
scripts/bench_duplex.py  LLM -> stream -> SSE clients benchmark
demos/                   runnable examples (make demo, make demo-serve, ...)

Everyday commands

make check       # lint + test: what CI runs, run this before pushing
make full        # format, check, bench, build, in that order (edits files!)

Test

make test           # pytest
make test-diskfull  # disk-full rollback against a real ENOSPC (needs Docker)
uv run pytest -q tests/test_sync.py::test_tail   # a single test

tests/test_disk_full.py fills a tiny file system so an append is half-written when the disk runs out, and checks the log rolls back to its last intact record. It skips unless DURASTREAM_SMALL_FS points at a small tmpfs; make test-diskfull provides one in Docker, and CI mounts one on its Linux runners.

Lint and format

make lint        # check only: ty, ruff check, ruff format --check
make format      # apply fixes: ruff --fix, ruff format
make typecheck   # ty only

Benchmark

make bench                                     # ~1 min
uv run python scripts/bench_duplex.py --quick  # half the chats and tokens

Models an LLM/SSE app: chats append tokens at 50 tok/s while SSE-style clients tail them with subscribe, some disconnecting and resuming. Latency is from token produced to seen by a client. Apple M-series:

scenario p50 p99 CPU per 1k tokens
1 chat 0.4 ms 1.6 ms 670 ms
50 chats 1.0 ms 8.2 ms 179 ms
200 chats × 3 clients 4.8 ms 13.8 ms 128 ms
same, AsyncBatchWriter 22.7 ms 38.5 ms 79 ms

Build

make build       # wheel + sdist into dist/

CI and release

ci.yml runs on every push to main and every PR: make lint, and the tests on Ubuntu, macOS and Windows with Python 3.12 and 3.14 (plus the disk-full test on Ubuntu).

The version comes from the git tag (hatch-vcs): tag v0.0.1a3 builds 0.0.1a3, there is nothing to bump. To release, tag an up-to-date main and push the tag:

git checkout main && git pull
git tag v<version> && git push origin v<version>

release.yml then builds the wheel and sdist, installs the wheel on Linux, macOS and Windows and runs the test suite there. The publish step waits for your approval (GitHub environment pypi, Actions > the run > "Review deployments"); once approved it publishes to PyPI via trusted publishing and creates the GitHub release with the files attached (a/b/rc/dev versions as pre-release). A version can only be published once, so fix and tag the next version rather than re-tag.

An experimental Rust engine for durastream (same API and on-disk format, ~10x faster recovery and replay) was built and then set aside to keep this package pure Python. It lives on in terminalkitten/durastream-core.

Metadata

Release files for durastream 0.0.1a3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for durastream 0.0.1a3
File Size Uploaded
durastream-0.0.1a3.tar.gz 31.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for durastream 0.0.1a3
File Interpreter ABI Platform
durastream-0.0.1a3-py3-none-any.whl Python 3 none any Details

Total release size: 50.8 kB

Release files / durastream-0.0.1a3.tar.gz

Download URL durastream-0.0.1a3.tar.gz
Size 31.9 kB
Tags Source
SHA-256 checksum
How to use checksums
9cadac9efd73c6c2d37c824cc6fb2f3c9a3ce7c8d9757ccb19d9a84f6792f75c
BLAKE2b-256 checksum
How to use checksums
c640623f2ff8aee0a22a187a52da2d964c2564e692a61fa20c2d3f262fd93a92
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / durastream-0.0.1a3-py3-none-any.whl

Download URL durastream-0.0.1a3-py3-none-any.whl
Size 18.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
210588840d1eb672677907c14122836776c4579cc8d29e4e63b58eb2560cca6a
BLAKE2b-256 checksum
How to use checksums
df23f976499da7019af4103c73cb4c83718b15fdea6c89eb936836fc943505ac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log
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