Performance

Every number is dated, matched, and verified.

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.

Summary

Five engines. One protocol. Median speedup.

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.

Engine 1 of 5 · Python

vs GeoPandas — median 40.2×, 18 of 18 won.

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.

make_valid100,000 features · rg 27 ms vs 7,746 ms
286.4×
set_precision (pointwise)1,000,000 features · rg 16 ms vs 2,066 ms
125.5×
centroid10,000,000 features · rg 56 ms vs 6,849 ms
122.6×
intersects1,000,000 features · rg 35 ms vs 3,406 ms
98.3×
simplify (Douglas-Peucker)1,000,000 features · rg 38 ms vs 2,854 ms
75.7×
erase1,000,000 features · rg 139 ms vs 7,929 ms
57.0×
convex_hull1,000,000 features · rg 168 ms vs 8,400 ms
49.9×
length10,000,000 features · rg 24 ms vs 997 ms
42.4×
clip1,000,000 features · rg 143 ms vs 5,745 ms
40.3×
spatial_join (left)1,000,000 features · rg 12 ms vs 477 ms
40.2×
overlay (intersection)1,000,000 features · rg 554 ms vs 22,029 ms
39.8×
area10,000,000 features · rg 23 ms vs 855 ms
37.2×
buffer100,000 features · rg 43 ms vs 1,235 ms
28.5×
difference100,000 features · rg 66 ms vs 1,467 ms
22.4×
intersection100,000 features · rg 67 ms vs 1,442 ms
21.6×
sym_difference100,000 features · rg 76 ms vs 1,574 ms
20.8×
distance (points)10,000,000 features · rg 54 ms vs 1,024 ms
18.8×
dissolve (unary union)100,000 features · rg 182 ms vs 2,529 ms
13.9×

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

Engine 2 of 5 · R

vs sf — median 37.4×, 16 of 16 won.

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

centroid5,000,000 features · rg 47 ms vs 25,251 ms
536.4×
length10,000,000 features · rg 42 ms vs 18,857 ms
445.4×
area10,000,000 features · rg 44 ms vs 13,607 ms
309.9×
spatial_join (left)1,000,000 features · rg 21 ms vs 5,757 ms
279.1×
simplify (Douglas-Peucker)1,000,000 features · rg 91 ms vs 8,251 ms
90.3×
erase1,000,000 features · rg 129 ms vs 8,699 ms
67.3×
clip1,000,000 features · rg 130 ms vs 7,422 ms
57.0×
intersects1,000,000 features · rg 88 ms vs 3,445 ms
39.3×
convex_hull1,000,000 features · rg 631 ms vs 22,468 ms
35.6×
distance (points)10,000,000 features · rg 36 ms vs 803 ms
22.4×
make_valid100,000 features · rg 86 ms vs 1,868 ms
21.7×
buffer100,000 features · rg 129 ms vs 2,264 ms
17.5×
difference100,000 features · rg 127 ms vs 1,396 ms
11.0×
intersection100,000 features · rg 218 ms vs 1,442 ms
6.6×
sym_difference100,000 features · rg 395 ms vs 1,895 ms
4.8×
dissolve (unary union)100,000 features · rg 425 ms vs 1,744 ms
4.1×

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

Engine 3 of 5 · SQL analytics

vs DuckDB Spatial — median 13.0×, 15 of 15 won.

The analytics database’s spatial extension, measured at 32 threads on loaded tables.

make_valid100,000 features · rg 27 ms vs 7,301 ms
266.0×
buffer100,000 features · rg 45 ms vs 1,240 ms
27.8×
intersection100,000 features · rg 65 ms vs 1,489 ms
23.0×
difference100,000 features · rg 66 ms vs 1,478 ms
22.4×
spatial_join (left)1,000,000 features · rg 14 ms vs 219 ms
16.2×
simplify (Douglas-Peucker)1,000,000 features · rg 37 ms vs 590 ms
16.1×
dissolve (unary union)100,000 features · rg 184 ms vs 2,590 ms
14.1×
convex_hull1,000,000 features · rg 161 ms vs 2,099 ms
13.0×
erase1,000,000 features · rg 132 ms vs 1,641 ms
12.4×
clip1,000,000 features · rg 134 ms vs 1,302 ms
9.7×
distance2,000,000 features · rg 11 ms vs 52 ms
4.7×
intersects1,000,000 features · rg 35 ms vs 122 ms
3.5×
length2,000,000 features · rg 3.8 ms vs 8.3 ms
2.2×
centroid2,000,000 features · rg 11 ms vs 18 ms
1.7×
area2,000,000 features · rg 4.2 ms vs 6.9 ms
1.6×

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

Engine 4 of 5 · spatial database

vs PostGIS — median 11.7×, 17 of 17 won.

The reference spatial database, measured at full parallelism — 32 workers per query, spatial index in place, queries timed inside the server.

erase1,000,000 features · rg 139 ms vs 7,808 ms
56.3×
spatial_join (left)1,000,000 features · rg 12 ms vs 633 ms
53.9×
clip1,000,000 features · rg 137 ms vs 5,847 ms
42.7×
distance2,000,000 features · rg 12 ms vs 300 ms
26.0×
intersects1,000,000 features · rg 34 ms vs 798 ms
23.2×
make_valid100,000 features · rg 26 ms vs 492 ms
18.7×
dissolve (unary union)100,000 features · rg 180 ms vs 2,473 ms
13.7×
length2,000,000 features · rg 4.3 ms vs 52 ms
12.0×
area2,000,000 features · rg 4.6 ms vs 53 ms
11.7×
centroid2,000,000 features · rg 11 ms vs 112 ms
10.5×
set_precision1,000,000 features · rg 16 ms vs 48 ms
2.9×
convex_hull1,000,000 features · rg 159 ms vs 375 ms
2.4×
difference100,000 features · rg 64 ms vs 130 ms
2.0×
buffer100,000 features · rg 46 ms vs 92 ms
2.0×
intersection100,000 features · rg 68 ms vs 130 ms
1.9×
sym_difference100,000 features · rg 76 ms vs 132 ms
1.7×
simplify (Douglas-Peucker)1,000,000 features · rg 40 ms vs 62 ms
1.6×

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

Engine 5 of 5 · cluster engine

vs Apache Sedona — median 9.5×, 16 of 16 won.

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.

length2,000,000 features · rg 4.5 ms vs 297 ms
66.0×
dissolve (unary union)100,000 features · rg 178 ms vs 10,579 ms
59.3×
distance2,000,000 features · rg 12 ms vs 522 ms
45.2×
area2,000,000 features · rg 4.2 ms vs 186 ms
44.1×
spatial_join (left)1,000,000 features · rg 15 ms vs 305 ms
20.9×
centroid2,000,000 features · rg 11 ms vs 229 ms
20.6×
intersects1,000,000 features · rg 37 ms vs 732 ms
19.7×
difference100,000 features · rg 64 ms vs 625 ms
9.8×
simplify (Douglas-Peucker)1,000,000 features · rg 40 ms vs 374 ms
9.3×
make_valid100,000 features · rg 27 ms vs 250 ms
9.2×
intersection100,000 features · rg 73 ms vs 593 ms
8.2×
sym_difference100,000 features · rg 83 ms vs 485 ms
5.9×
buffer100,000 features · rg 47 ms vs 252 ms
5.4×
clip1,000,000 features · rg 117 ms vs 342 ms
2.9×
convex_hull1,000,000 features · rg 183 ms vs 470 ms
2.6×
erase1,000,000 features · rg 110 ms vs 276 ms
2.5×

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

Apache SpatialBench

Six million trips. Twelve spatial workloads. 3.75 seconds.

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.

Not our benchmark. Not our queries. Not our answers.
Just our engine.

Official protocol

the benchmark’s own runner · a fresh process per query · every file read inside the clock · mean of 3
rgSpatialall 12 workloads · every answer correct
3.75 s
PyCanopyRust spatial-index engine — 12/12, answers verified · rg is 3.3× faster
12.50 s
Apache SedonaDBfrom the project that created the benchmark — 12/12 · rg is 3.8× faster
14.42 s
Spatial Polarsthe Polars ecosystem’s spatial extension — 12/12 · rg is 39× faster
147.8 s
DuckDB Spatial11 of 12 — q12 killed at the 600 s cap; the suite was never completed
DNF
12 / 12  workloads correct 100%  of result cells match the published answers 0  timings accepted without a verified answer

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.

In memory — the analytical work alone

same runner, same fresh process · every table loaded before the clock starts · only the query timed

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.

rgSpatialtables held as native GeoArrow — how rg ships
1.78 s
Apache SedonaDBtables as in-memory WKB — 12/12 · rg is 6.1× faster
10.85 s
PyCanopytables as in-memory WKB — 12/12 · rg is 6.2× faster
10.98 s
Spatial Polarstables as in-memory WKB — 12/12 · rg is 84× faster
148.7 s
DuckDB Spatial11 of 12 — q12 killed at the 600 s cap
DNF

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.

Live in the Browser

No, really — SpatialBench runs in the browser.

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.

File I/O · reads

Reads: 5.8–197× faster than GDAL at its fastest.

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.

FileGDBrg 5.3 ms
197×
GeoJSONrg 25 ms
93×
GeoJSON-Lrg 22 ms
78×
KMLrg 32 ms
58×
Shapefilerg 25 ms
29×
TopoJSONrg 55 ms
28×
FlatGeobufrg 21 ms
25×
GPXrg 72 ms
18×
KMZrg 125 ms
15×
GeoPackagerg 20 ms
5.8×

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

File I/O · writes

Writes: 6.5–163× faster than GDAL.

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.

FileGDBrg 15 ms
163×
KMLrg 34 ms
149×
KMZrg 43 ms
100×
GPXrg 35 ms
99×
GeoJSONrg 23 ms
98×
GeoJSON-Lrg 27 ms
71×
Shapefilerg 37 ms
32×
FlatGeobufrg 42 ms
24×
GeoPackagerg 89 ms
6.5×
TopoJSONrg 48 ms · GDAL has no writer benchmark for this 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.

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

File I/O · columnar

Faster than the columnar reference implementations.

GeoParquet and Arrow IPC, codec-matched (snappy). One comparison target per group; all bars share one linear scale.

vs GeoPandasthe columnar path Python users have today
GeoParquet read
16.0×
GeoParquet write
5.5×
Feather read
60.0×
Feather write
31.0×
vs GDAL native columnar driversGDAL 3.13.1 at its strongest configuration
GeoParquet read
2.6×
GeoParquet write
2.7×
Feather read
2.0×
Feather write
7.1×
vs Apache Arrow C++the reference implementation of the format itself (pyarrow)
GeoParquet read
2.1×
GeoParquet write
2.6×

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