We publish benchmarks the way we run them internally: identical work, settings matched on both sides, parallelism enabled for every engine, and every answer verified — not just timed.
The same spatial operations — joins, buffers, overlays, dissolve, predicates, measures — on the same data, on the same machine, with parallel execution enabled and verified for every engine. Click any bar for the full per-operation breakdown.
Identical work, matched N: each op is sized to the largest N the competitor completes within a fixed budget, and rg is timed on the same slice. Every result is output-verified before it is timed. Measured 2026-07-04/05 on a 16-core desktop (AMD Ryzen 9 7950X3D, 64 GB). Versions: GeoPandas 1.1.3 / Shapely 2.1.2, sf 1.0.24, PostGIS 16/3.4, DuckDB 1.5.4 + spatial, Apache Sedona single-node Spark. GeoPandas and sf are single-threaded by design and measured as shipped; PostGIS runs 32 parallel workers (EXPLAIN-verified), DuckDB threads=32, Sedona local[*] repartitioned across all cores.
Python’s standard geospatial stack (GeoPandas 1.1.3 / Shapely 2.1.2), measured as shipped on loaded, native objects — no I/O in the timed region on either side.
Bar lengths are log-scaled — speedups here span two orders of magnitude, and a linear bar would hide the smaller wins. Read the numbers; the bars are for shape.
Both engines timed on loaded data structures; results value-checked once per op in an untimed gate. overlay (intersection) is the two-layer table overlay — join + intersect + attributes from both layers; GeoPandas implements it natively and rg’s built-in overlay does the same work, so it counts like any other op. The largest gaps are uniform per-feature repair and reduction where rg’s columnar layout and parallelism outscale the per-object model; the low end is the heaviest shared constructive geometry. Identical work, matched N: each op is sized to the largest N the competitor completes within a fixed budget, and rg is timed on the same slice. Every result is output-verified before it is timed. Measured 2026-07-04/05 on a 16-core desktop (AMD Ryzen 9 7950X3D, 64 GB).
R’s standard geospatial stack, GEOS-backed and single-threaded as shipped. sf 1.1-2 (current CRAN), GEOS 3.14.1. Speedups span 4.1× to 536.4×.
Bar lengths are log-scaled — speedups here span two orders of magnitude, and a linear bar would hide the smaller wins. Read the numbers; the bars are for shape.
s2 disabled; all operands planar EPSG:3857 — sf runs its fast GEOS path. The pairwise constructive ops run through sf’s native by_element API. This table has 16 rows to GeoPandas’s 18 because two Python-suite ops have no faithful sf counterpart and are omitted, never counted as wins: overlay (sf has no table-style overlay; st_intersection(sf, sf) is the join-style idiom) and set_precision (st_set_precision sets a precision model on the object rather than snapping coordinates). Identical work, matched N: each op is sized to the largest N sf completes within a fixed budget, and rg is timed on the same slice. Every result is output-verified before it is timed. The rg column is the R package’s own build (rtools/GNU toolchain), exactly as an R user gets it — it currently runs some constructive kernels slower than rg’s Python build (MSVC), so rg’s absolute times differ between this table and the Python one. Measured 2026-08-17 — later than the other tables because sf 1.1 added vectorized pairwise operations after the July runs, and we upgraded to current CRAN and re-measured the entire suite rather than publish against an outdated sf — on the same 16-core desktop (AMD Ryzen 9 7950X3D, 64 GB).
The analytics database’s spatial extension, measured at 32 threads on loaded tables.
Bar lengths are log-scaled — speedups here span two orders of magnitude, and a linear bar would hide the smaller wins. Read the numbers; the bars are for shape.
DuckDB-spatial has no pointwise set_precision or ST_SymDifference — those two ops are unmapped, never counted as wins. The point-in-polygon predicate is measured on DuckDB’s prepared path so the comparison is prepared-vs-prepared. Each op runs in a fresh DuckDB process: a long-lived process slowed DuckDB’s later ops ∼8×, so per-op isolation is the configuration that favors DuckDB. Identical work, matched N: each op is sized to the largest N the competitor completes within a fixed budget, and rg is timed on the same slice. Every result is output-verified before it is timed. Measured 2026-07-04/05 on a 16-core desktop (AMD Ryzen 9 7950X3D, 64 GB).
The reference spatial database, measured at full parallelism — 32 workers per query, spatial index in place, queries timed inside the server.
Bar lengths are log-scaled — speedups here span two orders of magnitude, and a linear bar would hide the smaller wins. Read the numbers; the bars are for shape.
Tables loaded via COPY with per-table parallel_workers=32, worker counts verified per query with EXPLAIN ANALYZE; the grille point-in-polygon predicate uses a parallel partial aggregate so PostGIS runs it across workers rather than serially on the leader. Every engine starts from its already-loaded native format, and PostGIS is the only engine given a pre-built spatial index (GiST + ANALYZE, created untimed — its standard production setup); rg builds any spatial structures it needs inside each timed operation and holds no cached indexes between calls. Identical work, matched N: each op is sized to the largest N the competitor completes within a fixed budget, and rg is timed on the same slice. Every result is output-verified before it is timed. Measured 2026-07-04/05 on a 16-core desktop (AMD Ryzen 9 7950X3D, 64 GB).
A cluster engine measured on one machine: single-node Spark, local[*], 16 GB heap, data repartitioned across all cores, its broadcast index join used for predicates.
Bar lengths are log-scaled — speedups here span two orders of magnitude, and a linear bar would hide the smaller wins. Read the numbers; the bars are for shape.
Single-node Spark is noisy run-to-run — isolated re-runs measured medians of 7.8–15.6×. Pointwise set_precision is unmapped, never counted as a win. Identical work, matched N: each op is sized to the largest N the competitor completes within a fixed budget, and rg is timed on the same slice. Every result is output-verified before it is timed. Measured 2026-07-04/05 on a 16-core desktop (AMD Ryzen 9 7950X3D, 64 GB).
SpatialBench is Apache Sedona’s open benchmark for spatial analytics — not a benchmark we designed. SF1 holds 6,000,000 trip records, 156,095 zone polygons, 1.6 million buildings and supporting tables — about 1.8 GB of Parquet. Its 12 queries exercise the operations that make spatial engines hard to build: filters, distances, point-in-polygon joins, chained joins, grouped aggregation, dissolve, and nearest-neighbour search.
Same benchmark. Published queries. Published answers. One machine.Every rgSpatial result cell is validated against SpatialBench’s reference output (rtol 1e-6, positional) — a wrong answer would disqualify the timing entirely.
The official protocol times the file reads along with the analytics. This second reading removes them: the untimed setup loads every engine’s tables into that engine’s own in-memory form, and the clock starts on the query. The difference between the two readings is what each engine pays to get data off disk; this one is what it does with the data once it has it.
rgSpatial leads or ties every one of the 12 queries in this reading. Loading the tables first moves SedonaDB and PyCanopy by about a quarter and Spatial Polars almost not at all — their time is in the joins and in parsing WKB inside each query, not in the reads — while it halves rgSpatial’s, whose reads were already close to the operating system’s copy rate. Each engine holds its tables the way it ships: rgSpatial as native GeoArrow — a capability competitors lack, since SedonaDB rejects GeoArrow-native geometry and DuckDB-spatial cannot bind it — and the others as WKB bytes, with no index and no geometry pre-parse. Nothing is precomputed for anyone.
SpatialBench generates realistic synthetic data — a transportation-themed schema of points, polygons, buildings, and zones — reproducibly, at configurable scale factors.
Every competitor was timed by the benchmark’s own official runner: SedonaDB 0.4.1, DuckDB 1.5.5 + spatial, Spatial Polars 0.3.0, and PyCanopy 0.4.1. Each engine was measured on the newest Python it supports — PyCanopy in a Python 3.12 environment, the ceiling its packaging declares, the rest on Python 3.13. Spatial Polars’ and PyCanopy’s outputs verify cell-for-cell against the committed answers. Every engine here, rgSpatial (rg 0.5.1) included, was measured 2026-09-06 on a 16-core desktop (AMD Ryzen 9 7950X3D, 64 GB). rgSpatial’s totals sit in run-to-run bands of 3.71–3.79 s and 1.74–1.79 s.
DuckDB completed 11 of 12 queries in 453 s (724 s with its tables in memory) and was killed at the 600-second cap on q12 in both readings — no complete-suite total exists, so we print none.
The benchmarks above were run natively. But the same Rust engine also compiles to WebAssembly. Here it is live in the rgSpatial web app, running SpatialBench q1’s spatial work against all 6,000,000 trips: ST_DWithin keeps the trips within 50 km of Sedona, Arizona, ST_Distance ranks them, and the result lands on the map as a layer.
Real screen recording. Unedited. No server compute.
SELECT t_tripkey,
geometry,
ST_Distance(geometry, ST_GeomFromText('POINT (-111.7610 34.8697)')) AS distance_to_center
FROM sb_trip
WHERE ST_DWithin(geometry, ST_GeomFromText('POINT (-111.7610 34.8697)'), 50000)
ORDER BY distance_to_center ASC
LIMIT 100
Forty-five trips lie within 50 km — distances in geodesic metres, computed in a browser tab. The town is Sedona, Arizona; the benchmark is Apache Sedona’s; the engine is ours.
Pure-Rust readers, measured against GDAL/OGR on both its classic driver path and its Arrow batch fast path (RFC 86, GDAL_NUM_THREADS=ALL_CPUS) — every bar is against whichever GDAL configuration is faster. No GDAL configuration beats rg on any format.
Bar lengths are log-scaled — speedups here span two orders of magnitude, and a linear bar would hide the smaller wins. Read the numbers; the bars are for shape.
N = 100,000 features per format, identical third-party-generated input files, row-count correctness gate, median of repeated runs after warmup. rg 0.4.0, GDAL 3.11.4 via pyogrio. CSV and Excel are measured against pandas, not GDAL: CSV 9.7× read / 141× write, Excel 56× read / 101× write. Measured 2026-07-25/26 on a 16-core desktop (AMD Ryzen 9 7950X3D, 64 GB).
Same protocol, write direction: every file is read back and verified before it is timed. There is no format where GDAL is faster in either direction.
Bar lengths are log-scaled — speedups here span two orders of magnitude, and a linear bar would hide the smaller wins. Read the numbers; the bars are for shape.
GPX write compares against GDAL’s classic driver (its Arrow write path is n/a for GPX). TopoJSON has no GDAL writer — rg’s 48 ms write is shown without a comparison bar. rg 0.4.0, GDAL 3.11.4 via pyogrio. Measured 2026-07-25/26 on a 16-core desktop (AMD Ryzen 9 7950X3D, 64 GB).
GeoParquet and Arrow IPC, codec-matched (snappy). One comparison target per group; all bars share one linear scale.
The three baselines are independent code paths. GeoPandas reads these formats through pyarrow, not GDAL, then decodes WKB into per-feature shapely objects — most of its cost. GDAL measured via its Arrow-stream read and WritePyArrow write (conda-forge build) — a configuration absent from the standard pyogrio wheel. rg 0.4.0, GDAL 3.13.1 conda-forge. Measured 2026-07-25/26 on a 16-core desktop (AMD Ryzen 9 7950X3D, 64 GB).