Crash-resumable Parquet streaming to S3 from Python. One multipart upload per file, a checkpoint any process can finish, parallel reads back.
Install · Quickstart · How it works · Performance · API
Highlights
- Resumes after a crash: a checkpoint is a small blob; any process turns it into a valid Parquet file without re-uploading a byte.
- About 2× pyarrow on writes (219 vs 116 MiB/s), with parallel reads (benchmarks).
- Rust core, Arrow in and out without copies: PyO3 bindings, GIL released around every network call.
- No conditional writes needed: works on S3-compatible stores that ignore
If-Match.
writer = arroyo.ParquetStreamWriter(config, "events/2026-10-28.parquet", batch.schema)
writer.write(batch)
state = writer.checkpoint() # small, serializable, survives the process
... # the process dies here
arroyo.finish(config, state) # any other process: a valid Parquet file, no re-upload
Why
S3 objects cannot be appended to, and a streaming job that writes one file across
many checkpoints has to keep a multipart upload open. Existing Python writers
(pyarrow's S3FileSystem, obstore) stream a multipart upload but cannot resume one:
when the process dies, the file is gone. arroyo implements the method from Arroyo's
filesystem sink (Streaming to S3 is hard)
so that a checkpoint carries enough to complete the file later, from anywhere, with
nothing already uploaded sent again. It needs no conditional writes, so it works on
S3-compatible stores that accept and ignore If-Match.
| streams a multipart upload | resumes after a crash | parallel reads | |
|---|---|---|---|
pyarrow S3FileSystem |
yes | no | no |
| obstore | yes | no | partially |
| arroyo | yes | yes | yes |
Install
pip install arroyo-python
Python 3.12 or newer. Wheels ship for Linux (x86_64, aarch64), macOS (arm64, x86_64) and Windows. Building from source needs a Rust toolchain and maturin.
How it works
Write path
Batches are buffered until a row group is full. Every leaf column is then encoded on its own rayon task, the encoded bytes are sliced into fixed-size parts without copying, and the parts upload on a tokio runtime while the next row group is encoded. The GIL is released around every network call.
Checkpoint and recovery
checkpoint() returns a WriterState: the parts acknowledged so far and the
trailing bytes, which are the bytes not yet uploaded plus a Parquet footer for
the row groups written so far. finish(config, state) uploads the trailing bytes
as the last part(s) and completes the upload. The result is a valid Parquet file
holding every row up to that checkpoint. Calling it twice is safe.
close() writes the real footer as the last part and returns a PendingCommit;
commit() completes the upload. That split is the second phase of a two-phase
commit: record the pending state durably first, then commit.
Read path
read_parquet reads the footer with one range request, then fetches concurrency
row groups ahead of the consumer with coalesced range GETs, decodes them on the
blocking pool and hands batches to Python through the Arrow C stream interface, in
file order.
Quickstart
import pyarrow as pa
import arroyo
config = arroyo.S3Config.from_env(bucket="events") # AWS_ENDPOINT, AWS_ACCESS_KEY_ID, ...
writer = arroyo.ParquetStreamWriter(
config,
"events/part-0001.parquet",
schema, # anything with __arrow_c_schema__
part_size=16 * 1024 * 1024, # 5 MiB .. 5 GiB
row_group_size=128 * 1024 * 1024, # buffered Arrow bytes that close a row group
compression="zstd",
)
writer.write(batch) # RecordBatch, Table, RecordBatchReader, polars ...
state = writer.checkpoint() # closes the row group, waits for parts in flight
blob = state.to_bytes() # store this with your own checkpoint
writer.write(next_batch) # same file, same upload
pending = writer.close() # real footer uploaded, upload not yet completed
record(pending.to_bytes()) # your durable checkpoint
pending.commit() # completes the multipart upload
# Another process, after a crash, from the last recorded blob:
arroyo.finish(config, arroyo.WriterState.from_bytes(blob))
reader = arroyo.read_parquet(config, "events/part-0001.parquet", columns=["a", "b"], concurrency=8)
table = pa.RecordBatchReader.from_stream(reader).read_all()
S3Config pickles, so it can be handed to multiprocessing workers.
Performance
| write MiB/s | read MiB/s | |
|---|---|---|
| arroyo | 219 | 261 |
| pyarrow | 116 | 166 |
| obstore | 134 | 287 |
Apple M3 Pro, 12 cores, 20,000,000 rows (801 MiB of Arrow data) per file, pyarrow 26.0.0, obstore 0.12.1, arroyo 0.1.0. arroyo reads with concurrency=8; the obstore baseline reads through pyarrow's own multithreaded decoder.
The benchmark writes and reads one Parquet file of synthetic int64, float64 and
string columns against a local S3-compatible server (RustFS in Docker) with every
library at its defaults. Numbers are the mean of three runs; make bench and
python benchmarks/plot.py reproduce the chart on your machine.
Rules for callers
- One writer per state. There is no compare-and-set. Stop the old writer before
finish(state)runs in a new process. A writer that is still running can complete the upload with a different part list; one of the two wins and the other fails the size check withArroyoError. - Abandoned uploads cost storage. Configure an
AbortIncompleteMultipartUploadlifecycle rule on the bucket, and callarroyo.abort(config, state)orwriter.abort()for uploads you give up on. The writer aborts its upload when awithblock exits with an exception. A writer that is dropped withoutclose()orabort()leaves its upload in place, so a recorded checkpoint stays finishable. - Keep files short-lived. Some providers expire upload IDs after days. Roll files by size or time. A file may hold at most 10,000 parts; the writer raises before it would send part 10,001.
- Every
checkpoint()closes a row group, so frequent checkpoints mean small row groups.
API
S3Config(**kwargs) / S3Config.from_env(**kwargs) |
bucket, region, endpoint, credentials, allow_http, virtual_hosted_style. from_env reads AWS_* first. |
ParquetStreamWriter(config, path, schema, **options) |
write(batches), checkpoint() -> WriterState, close() -> PendingCommit, abort(), rows_written, rows_pending, context manager |
WriterState |
to_bytes(), from_bytes(), path, upload_id, parts, uploaded_bytes, expected_size, rows, is_pending_commit |
PendingCommit |
commit(), state, to_bytes() |
finish(config, state) |
uploads the trailing bytes and completes the upload; idempotent |
abort(config, state) |
aborts the upload |
read_parquet(config, path, *, columns, concurrency, batch_size) |
returns an Arrow C stream (pa.RecordBatchReader.from_stream(...)) |
ArroyoError |
every storage or format failure; bad arguments raise ValueError |
Writer options
| option | default | meaning |
|---|---|---|
part_size |
16 MiB | bytes per part; every part except the last has exactly this size |
row_group_size |
128 MiB | buffered Arrow bytes that close a row group |
max_row_group_rows |
1,048,576 | rows that close a row group |
max_in_flight_parts |
4 | parts uploading at once before write waits |
compression |
"snappy" |
uncompressed, snappy, zstd, lz4, gzip, brotli |
Reader options
| option | default | meaning |
|---|---|---|
columns |
all | top-level columns; returned in file order |
concurrency |
4 | row groups fetched and decoded ahead of the consumer |
batch_size |
65,536 | rows per returned batch |
Memory for a read is about concurrency row groups' worth of compressed bytes plus
a couple of decoded batches per row group.
Design notes
Footer without a fork. Arroyo patches parquet to write a footer without closing
the writer. arroyo builds the same footer from upstream
SerializedFileWriter::flushed_row_groups() with ParquetMetaDataWriter. Column
chunk offsets in row-group metadata are absolute file offsets, so the footer is valid
at the current bytes_written(). The upstream writer assigns page-index offsets only
in the final footer, so a checkpoint footer carries no page index while a file closed
with close() keeps it. Both are valid Parquet; the tests read recovered files back
with the Arrow schema, timezone-aware timestamps and nested columns intact.
Upload ID access. ObjectStore::put_multipart hides the upload ID, so the crate
uses object_store::multipart::MultipartStore on the concrete AmazonS3. Stores are
built from S3Config; accepting an obstore store object is not supported yet.
State format. WriterState.to_bytes() is a magic prefix, a format version and a
postcard-encoded body. Validation on from_bytes() rejects truncated blobs and part
lists with gaps.
Versions. pyo3-arrow 0.20 pins arrow 59, so the crate uses parquet 59 and
object_store 0.13, one version of each.
Development
uv venv && source .venv/bin/activate
uv pip install maturin pytest pyarrow "moto[server]" obstore numpy
make develop # maturin develop
make ci # cargo fmt --check, clippy -D warnings, cargo test
make test-py # pytest against an in-process moto S3
make s3 # RustFS in Docker on 127.0.0.1:9000 (rustfsadmin / rustfsadmin)
Set ARROYO_TEST_ENDPOINT, ARROYO_TEST_BUCKET and the AWS_* variables to run
pytest against RustFS or any other store instead of moto. The benchmark takes
ARROYO_BENCH_ENDPOINT and ARROYO_BENCH_BUCKET:
python benchmarks/bench_throughput.py --rows 20000000 --repeat 3 --json results.json
python benchmarks/plot.py results.json benchmarks/results/throughput.svg
License
MIT OR Apache-2.0. The write method follows Arroyo (MIT OR Apache-2.0).
Metadata
Release files for arroyo-python 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| arroyo_python-0.1.2.tar.gz | 53.1 kB | With provenance | Details for arroyo_python-0.1.2.tar.gz |
Built distributions (wheels)
| File | Actions Reset | |||
|---|---|---|---|---|
| arroyo_python-0.1.2-cp312-abi3-win_amd64.whl | CPython 3.12 | abi3 | Windows x86-64 | Details for arroyo_python-0.1.2-cp312-abi3-win_amd64.whl |
| arroyo_python-0.1.2-cp312-abi3-manylinux_2_28_x86_64.whl | CPython 3.12 | abi3 | Linux glibc 2.28+ x86-64 | Details for arroyo_python-0.1.2-cp312-abi3-manylinux_2_28_x86_64.whl |
| arroyo_python-0.1.2-cp312-abi3-manylinux_2_28_aarch64.whl | CPython 3.12 | abi3 | Linux glibc 2.28+ ARM64 | Details for arroyo_python-0.1.2-cp312-abi3-manylinux_2_28_aarch64.whl |
| arroyo_python-0.1.2-cp312-abi3-macosx_11_0_arm64.whl | CPython 3.12 | abi3 | macOS 11.0+ ARM64 | Details for arroyo_python-0.1.2-cp312-abi3-macosx_11_0_arm64.whl |
| arroyo_python-0.1.2-cp312-abi3-macosx_10_12_x86_64.whl | CPython 3.12 | abi3 | macOS 10.12+ x86-64 | Details for arroyo_python-0.1.2-cp312-abi3-macosx_10_12_x86_64.whl |
Total release size: 36.0 MB
Release files / arroyo_python-0.1.2.tar.gz
| Download URL | arroyo_python-0.1.2.tar.gz |
|---|---|
| Size | 53.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
00cd556020d13424f022219be2cc78a5606e6ee1ef259b35807981096c049f76
|
|
BLAKE2b-256 checksum How to use checksums |
a41557c2a20a55bda338b811cee58c23aa87eaa4d29cecb4a95683c975bcf72a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 1, 2026.
Transparency logRelease files / arroyo_python-0.1.2-cp312-abi3-win_amd64.whl
| Download URL | arroyo_python-0.1.2-cp312-abi3-win_amd64.whl |
|---|---|
| Size | 7.2 MB |
| Tags | CPython 3.12 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
dbc4b223ba26bbbd1c229b0a809678e98029044cf2efad6d6b77a958fba15b75
|
|
BLAKE2b-256 checksum How to use checksums |
91dd1058d9a1ce0a2f46c2ef3301e3ac9ee89012a116003bd6f9acd84c8e9ce6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 1, 2026.
Transparency logRelease files / arroyo_python-0.1.2-cp312-abi3-manylinux_2_28_x86_64.whl
| Download URL | arroyo_python-0.1.2-cp312-abi3-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 7.5 MB |
| Tags | CPython 3.12 Linux glibc 2.28+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
5c9e425beb5d644011076d35172bea8dcb88a08a0bb17f464b8214f322879b4a
|
|
BLAKE2b-256 checksum How to use checksums |
2c608f1193602623a310260005536a59b4a5810a681f7c8cf167035d6c2db263
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 1, 2026.
Transparency logRelease files / arroyo_python-0.1.2-cp312-abi3-manylinux_2_28_aarch64.whl
| Download URL | arroyo_python-0.1.2-cp312-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 7.3 MB |
| Tags | CPython 3.12 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
e8531e88ce25aac689c50d56d7df3506340241db057f60d9ff4c34defba41c5a
|
|
BLAKE2b-256 checksum How to use checksums |
d06d35847ffadeee9fdbe17d3bf2b9ac1c372ab6ee47e08a7716c6631659b856
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 1, 2026.
Transparency logRelease files / arroyo_python-0.1.2-cp312-abi3-macosx_11_0_arm64.whl
| Download URL | arroyo_python-0.1.2-cp312-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 6.8 MB |
| Tags | CPython 3.12 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
72cfe9088a259377e4185a7a526d8fb3080561161dfd9e345842ae3f53dcc81e
|
|
BLAKE2b-256 checksum How to use checksums |
a1a5642a265da8bc27cbea6e8884053473631702b33123c78531f00ddfe27ffe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 1, 2026.
Transparency logRelease files / arroyo_python-0.1.2-cp312-abi3-macosx_10_12_x86_64.whl
| Download URL | arroyo_python-0.1.2-cp312-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 7.2 MB |
| Tags | CPython 3.12 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
93e6eb983ecdd4fb6ccf817e621f0ac74ec261647599638d6246e82298b3ebae
|
|
BLAKE2b-256 checksum How to use checksums |
0c53117802b687ff0628623456eac8195aa20681388e02a70597167dbae9ae47
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Nov 1, 2026.
Transparency logSecurity for arroyo-python 0.1.2
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.
Full provenance
All 6 files in this release have been uploaded with provenance metadata.
Attestation for arroyo_python-0.1.2.tar.gz
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 Nov 1, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.