We benchmark rgSpatial against the tools most teams use today — GeoPandas, sf, DuckDB Spatial, PostGIS, Apache Sedona, and GDAL for file I/O. These are our own measurements: one machine, the same data and settings on each side, parallelism enabled for every engine, and every result checked for correctness. Versions and dates are listed with each result.
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 an open benchmark for spatial analytics, published by the Apache Sedona project. 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 every one of the 12 queries in this reading. Loading the tables first moves SedonaDB by about a quarter, PyCanopy by about a tenth 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 more than 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.5.0, DuckDB 1.5.6 + spatial, Spatial Polars 0.4.1, and PyCanopy 0.4.1 — each its latest release. 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. SedonaDB 0.5.0 publishes no Windows build on PyPI, so it ran from conda-forge’s. Every query each engine completed verifies cell-for-cell against the committed answers. Every engine ran on the same 16-core desktop (AMD Ryzen 9 7950X3D, 64 GB): rgSpatial (rg 0.6.4), SedonaDB, DuckDB and PyCanopy were measured 2026-10-08, Spatial Polars 2026-09-25. Four runs of rgSpatial that day gave totals of 3.11–3.21 s, and three in memory 1.39–1.40 s; the charts show the slowest.
DuckDB completed 11 of 12 queries in 455 s (665 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. Its two longest queries, q10 and q11, run for minutes and were timed once rather than three times.
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.
rg’s Rust readers against GeoPandas, the path Python users have today: the GDAL formats through pyogrio, on both its classic driver path and its Arrow batch path, with GDAL’s multithreading turned on, and GeoParquet and Feather through pyarrow. Both read the same file, and every bar is against whichever GeoPandas path is faster.
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. GeoPandas builds shapely geometries from what it reads, which is most of its cost. Each file is the reference’s own: GDAL writes the GDAL formats and GeoPandas the GeoParquet and Feather files, except TopoJSON, which is rg’s, since GDAL has no TopoJSON writer. Both write the same columns (GDAL’s KML through its LIBKML driver); GeoParquet and Feather use zstd at level 1 on both sides. Row-count correctness gate; median of five runs after a warmup, each in a fresh process. rg 0.6.5 against geopandas 1.2.0 on GDAL 3.13.3 (conda-forge, the newest release) and pyarrow 26.0.0. CSV and Excel are measured against pandas 3.0.6: CSV 8.8× read / 65× write, Excel 16× read / 94× write, pandas reading Excel through python-calamine and writing it through xlsxwriter. Measured 2026-10-11/12 on a 16-core desktop (AMD Ryzen 9 7950X3D, 64 GB) running Windows 11.
Same protocol, write direction: both engines write the same features, each to its own file, with the same codec and the rows in their given order.
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.
GeoPandas converts its shapely geometries for every write. GPX write compares against GDAL’s classic driver (its Arrow write path is n/a for GPX). TopoJSON has no GDAL writer — rg’s 45 ms write is shown without a comparison bar. rg 0.6.5 against geopandas 1.2.0 on GDAL 3.13.3 (conda-forge, the newest release) and pyarrow 26.0.0. Measured 2026-10-11/12 on a 16-core desktop (AMD Ryzen 9 7950X3D, 64 GB) running Windows 11.
GDAL at its fastest, without GeoPandas and with its multithreading on: its Arrow stream and Arrow writer, through pyogrio’s read_arrow and write_arrow.
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.
Except for GeoParquet and Feather, GDAL’s reads return geometry still encoded as WKB, and rg’s return it decoded. GeoParquet and Feather use GDAL’s own Parquet and Arrow drivers (conda-forge, absent from the standard pyogrio wheel), with rg’s row groups and record batches, the same codec, no bbox column and the rows in their given order. The other files, the gate and the timing are as above. rg 0.6.5 against GDAL 3.13.3 (conda-forge, the newest release). Measured 2026-10-11/12 on a 16-core desktop (AMD Ryzen 9 7950X3D, 64 GB) running Windows 11.