Skip to main content

arroyo

Crash-resumable Parquet streaming to S3 from Python. One multipart upload per file, a checkpoint any process can finish, and parallel reads back. Rust core, PyO3 bindings, Arrow in and out without copies.

ci release PyPI Python 3.12+ License: MIT OR Apache-2.0

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

flowchart LR
    A[Arrow batches] --> B[Row group buffer]
    B -->|row or byte limit reached| C[Column-parallel encode on rayon]
    C --> D[Encoded bytes]
    D -->|part_size slices, no copy| E[Background part uploads, bounded in flight]
    E --> F[(S3 multipart upload)]
    D -->|remainder + checkpoint footer| G[Trailing bytes in the state]

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

sequenceDiagram
    participant W as Writer (process A)
    participant S as S3
    participant F as finish (process B)
    W->>S: CreateMultipartUpload
    W->>S: UploadPart 1..n (background, bounded)
    W->>W: checkpoint(): close row group, wait for parts in flight, build footer
    Note over W: state = upload id + parts + trailing bytes (unsent bytes + footer)
    W--xW: process dies
    F->>S: UploadPart n+1.. (trailing bytes)
    F->>S: CompleteMultipartUpload(parts 1..n+1)
    Note over F,S: on failure HEAD the object. The expected size means success

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 and read throughput of arroyo, pyarrow and obstore

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 with ArroyoError.
  • Abandoned uploads cost storage. Configure an AbortIncompleteMultipartUpload lifecycle rule on the bucket, and call arroyo.abort(config, state) or writer.abort() for uploads you give up on. The writer aborts its upload when a with block exits with an exception. A writer that is dropped without close() or abort() 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.0

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

Source distribution (sdist)

Source distribution for arroyo-python 0.1.0
File Size Uploaded Provenance Actions
arroyo_python-0.1.0.tar.gz 51.7 kB With provenance Details for arroyo_python-0.1.0.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for arroyo-python 0.1.0
File Actions
arroyo_python-0.1.0-cp312-abi3-win_amd64.whl CPython 3.12 abi3 Windows x86-64 Details for arroyo_python-0.1.0-cp312-abi3-win_amd64.whl
arroyo_python-0.1.0-cp312-abi3-manylinux_2_28_x86_64.whl CPython 3.12 abi3 Linux glibc 2.28+ x86-64 Details for arroyo_python-0.1.0-cp312-abi3-manylinux_2_28_x86_64.whl
arroyo_python-0.1.0-cp312-abi3-manylinux_2_28_aarch64.whl CPython 3.12 abi3 Linux glibc 2.28+ ARM64 Details for arroyo_python-0.1.0-cp312-abi3-manylinux_2_28_aarch64.whl
arroyo_python-0.1.0-cp312-abi3-macosx_11_0_arm64.whl CPython 3.12 abi3 macOS 11.0+ ARM64 Details for arroyo_python-0.1.0-cp312-abi3-macosx_11_0_arm64.whl
arroyo_python-0.1.0-cp312-abi3-macosx_10_12_x86_64.whl CPython 3.12 abi3 macOS 10.12+ x86-64 Details for arroyo_python-0.1.0-cp312-abi3-macosx_10_12_x86_64.whl

Total release size: 36.0 MB

Release files / arroyo_python-0.1.0.tar.gz

Download URL arroyo_python-0.1.0.tar.gz
Size 51.7 kB
Tags Source
SHA-256 checksum
How to use checksums
154cad9fb9615a793d14ca032fc48673ebcfe0a42fae4d07259343cf96f078b0
BLAKE2b-256 checksum
How to use checksums
009fc81411c97e9f1f28154639ce042b83b1a7c0e48a9a50fb67b024f9ee75cf
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 log

Release files / arroyo_python-0.1.0-cp312-abi3-win_amd64.whl

Download URL arroyo_python-0.1.0-cp312-abi3-win_amd64.whl
Size 7.2 MB
Tags CPython 3.12 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
0f073d55567ee392543fe45ce7414f627b9f87375d0126fabababb13c5ea69e9
BLAKE2b-256 checksum
How to use checksums
ea9920b8cbdf055850267c8c8ecb91ec60529988d75882228cbca3667c4d798b
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 log

Release files / arroyo_python-0.1.0-cp312-abi3-manylinux_2_28_x86_64.whl

Download URL arroyo_python-0.1.0-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
206c11118a5ee5c18efb5f2943d8c927acfe5148b00431aef938d1487258233a
BLAKE2b-256 checksum
How to use checksums
044502ac3f332e6f4f3b6048b9b63f109a3fcc2838f5604f0ed48fe86677ec5a
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 log

Release files / arroyo_python-0.1.0-cp312-abi3-manylinux_2_28_aarch64.whl

Download URL arroyo_python-0.1.0-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
2ad6a0c6def38b237f8dc5a8cd7a3458d0f329343b01337d829d6c33ab4e8d54
BLAKE2b-256 checksum
How to use checksums
ca7e23dbace697427cae3114dcaa956ab9c252027fe54b46e6bc13103167bfcb
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 log

Release files / arroyo_python-0.1.0-cp312-abi3-macosx_11_0_arm64.whl

Download URL arroyo_python-0.1.0-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
59809ec881a12c372c6481804d941033d648576c2f75608de1363881a91839b9
BLAKE2b-256 checksum
How to use checksums
aba03a6cc9f5cd6f8227c227a53d79d5fc9df360a153ce1479ff4a147d3220b1
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 log

Release files / arroyo_python-0.1.0-cp312-abi3-macosx_10_12_x86_64.whl

Download URL arroyo_python-0.1.0-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
0f4ba6e594afa1e9b5f4f993f837f10665da09e91bd0ba9c5fe8dcb72d7c8590
BLAKE2b-256 checksum
How to use checksums
571bc4301c72f4cf1176e1508f776c0b135712c353f34d650372793815c8161b
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 log

Security for arroyo-python 0.1.0

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.

Select from 6 files

Attestation for arroyo_python-0.1.0.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 log

A 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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

6 release files Full provenance

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