Performance

Measured against Ray and Rust, including the runs it lost

Every number here comes from runs on AWS, with the answers of all three frameworks checked against each other. The workload is the eight taxibench queries: scans, filters, sums and group-bys by zone, over NYC yellow taxi trips 2011–2024 as an Iceberg table in S3: 1,438,321,685 rows in 416 Parquet files (19.5 GB), written by 168 appends. Machines: AWS c7g.4xlarge (16 Graviton3 vCPUs, 32 GiB), us-east-2, k3s, 16 task slots per node. Runs from 1 November 2026.

Rust is iceberg-rust 0.10 with parquet and tokio, distributed by a small driver that hands each worker one file at a time. Ray is Ray with PyIceberg and pyarrow, one Ray task per file. Each is written the way its users would write it, and each gets the same nodes. The numbers are early: one run per configuration, on one day, and the comparison frameworks moved by about 10% between two runs of the same thing.

Distributed, 3 nodes ms, the eight queries, median pass

Run DistaRustRay
2 passes 12,00828,74438,570
5 passes 11,04018,37721,275
Each query
Query DistaRustRay
q1_scan_count 1,9812,3064,128
q2_month_range 157429409
q3_payment_sum 1,5993,3474,168
q4_tip_ratio 1,9803,5383,528
q5_top_zones 1,8893,4813,483
q6_zone_revenue 247468493
q7_wide 1,0011,0741,219
q8_selective 2,1693,7563,603

Three worker nodes, 16 task slots each, one task per data file, in two runs on separate clusters the same day. With 2 passes the median is the mean of both, so the slow first pass counts in it, and Rust and Ray lose more on that pass than Dista; with 5 the median is a warm pass. Even warm, Rust and Ray were slower on the first cluster: the clusters' own variation. Each query is from the 5-pass run. Every answer matches across the three frameworks: row counts exactly, sums to floating-point rounding. Results: matrix-2026-11-01 and gate-2026-11-01-readpath

Distributed, 3, 6 and 12 nodes ms, median of 5 passes

Run DistaRustRay
3 nodes 11,04018,37721,275
6 nodes 7,18410,26712,737
12 nodes 3,7516,6829,955

The eight queries' total on 3, 6 and 12 worker nodes; answers identical across every run. On 12 nodes, each framework's best of three runs: Ray's moves by up to 50% between runs. Results: matrix-2026-11-01

One process, 16 threads ms, the eight queries

DistaRustRay (PyIceberg + pyarrow)
Total 56,38096,23394,504
Each query
Query DistaRustRay (PyIceberg + pyarrow)
q1_scan_count 7,82815,5979,192
q2_month_range 315830446
q3_payment_sum 9,22918,36514,937
q4_tip_ratio 10,66318,36022,689
q5_top_zones 15,30919,90716,625
q6_zone_revenue 7479191,233
q7_wide 8881,026706
q8_selective 11,40121,22928,676

The head node alone, no workers, every framework on its 16 cores: Dista's scan with 16 workers, Rust's multi-threaded tokio runtime with one task per core, pyarrow's default thread pools. Answers identical across the three. Rust's numbers are from local-2026-11-01-2, a re-run. Results: local-2026-11-01

One process, one thread, 8 files' reads in flight ms, the eight queries

DistaRustRay (PyIceberg + pyarrow)
Total 211,887208,341184,327
Each query
Query DistaRustRay (PyIceberg + pyarrow)
q1_scan_count 28,02234,31817,537
q2_month_range 391834533
q3_payment_sum 38,32939,69029,969
q4_tip_ratio 50,36542,20644,095
q5_top_zones 41,52546,05731,709
q6_zone_revenue 1,7181,6961,945
q7_wide 1,1321,0971,211
q8_selective 50,40542,44357,328

One decoding thread each, with up to 8 files' reads in flight: Dista's files_at_once, iceberg-rust's concurrency limits on a current-thread runtime, pyarrow's I/O pool next to one CPU thread. Dista is level with Rust and 15% behind Ray; what's left is decode speed on one core. Results: local-2026-11-01

The second benchmark: a shuffle

The taxi queries spend most of their time reading Parquet. This one spends it in user code and in moving data between tasks: every trip grouped by pickup zone, dropoff zone and hour of day (1,044,000 groups), each group adding up its trips, speeds, tip ratios and fares. Map tasks read one data file each and split their groups by key into one partition per reducer; each reducer takes its partition from every map task and adds them up. Dista and Ray only, on the same table and nodes, from 2 November 2026. The query and its answer.

The shuffle, distributed on 3, 6 and 12 nodes ms, median of passes

Run DistaRay
3 nodes 6,29114,537
6 nodes 3,79511,407
12 nodes 3,97610,570

One map task per data file, which splits its groups into one partition per reducer; one reducer per task slot, up to 96. More reducers made Dista about twice as slow, since 416 map tasks then make tens of thousands of partitions; Ray's best on 12 nodes, with 48 reducers, is 9,644 ms. The 12-node row is from shuffle-reducers-2026-11-02. Every answer matches both one-process references: groups and trips exactly, sums to about 1e-13. Results: shuffle-matrix-2026-11-02-2

The shuffle, one process ms

Run DistaRay (PyIceberg + NumPy)
16 threads 15,897–
8 threads 26,650114,993
1 thread 105,656112,448

The head node alone. Each Dista thread reads whole files into arrays of its own, added up at the end. Ray's time barely moves with threads: its decoding uses them, but its per-batch NumPy work runs on one. Ray's 16- and 12-thread runs failed, likely from memory pressure on the head node, which also runs Kubernetes. Results: shuffle-local-2026-11-02-3

Where the time went

Dista records a timeline of every task, which opens in Perfetto. Before the read path, a file task waited on S3 for about 95% of its time: decoding the same file from local disk takes 12 to 40 ms. Planning each query read the table's 168 manifests one after another, and every task re-read the table's metadata. Now the footer comes in one read, a file's column chunks and a plan's manifests are fetched at once, and the metadata goes out with the task. Rust's iceberg-rust reader already did the first two.

From the timelinebeforeafter
A file task, median691 ms177 ms
A file task, p90–243 ms
Planning one query~4,000 ms295 ms
Submitting a query's 416 tasks800–1,100 ms3 ms
S3 requests, one-column readbeforeafter
Table metadata (133 KB, 168 snapshots)10, sent with the task
Footer2: trailer, then footer1: a 512 KiB tail
Column chunks, 4 row groups4, one after another4 at once

Results and the trace: gate-2026-11-01-readpath

Building and shippingcold build on the head node

Imagesize, MB: gzip (unpacked)whole build (s)dependencies (s)compile (s)
Dista 32 (93) 122 35.1 54
Rust 41 (132) 170 93.4 57.9
Ray 204 (607) 44 17.5 –

Dista's image is distroless: the binary and the libraries it links, one image for head, workers and driver. Ray's needs no compile and is the largest.

The runtime itself

Tasks through one head

about 160,000 a second

empty tasks, flat from 4 to 192 workers (bench/fanout.mojo, local)

Actor calls

1,000 ordered calls in about 22 ms

one counter actor, local

The 3-node AWS gate

about 13 minutes, about $1.50

a head and 3 workers: builds, three frameworks, two passes, teardown