When you are building mission-critical geospatial infrastructure, the architecture decision usually collapses into two unappealing options.
High-level dynamic frameworks — Python, Node.js — give you developer ergonomics and a rich geospatial ecosystem. They also give you poor concurrent performance under load, high memory usage, and CPU bottlenecks driven by single-threaded process locks like Python's GIL.
Pure compiled binaries — Rust, C++ — give you execution speed and small memory footprints. They also give you complex distributed state management, a steep learning curve for ordinary web routing, and real maintenance overhead once business logic gets involved.
We built TileserveX to find out whether that trade-off is actually necessary: a vector tile engine combining Elixir (Plug and Cowboy 2) with Rust (Rustler NIFs), implementing the OGC API specifications on top.
Architecture
TileserveX deliberately bypasses a full framework like Phoenix in favor of a minimal HTTP execution pipeline, separating web concurrency, state orchestration, and native CPU computation into three layers.

Bare-metal routing: Plug and Cowboy 2
Phoenix has excellent ergonomics for full-stack interactive applications. Serving high-throughput binary vector tiles is a different problem: what you want is minimal pipeline overhead.
Wiring Plug.Router directly into Cowboy 2 avoids controller pipeline stack allocations, parameter parsing overhead, and channel pub/sub setup. Requests are routed and matched directly, streaming packed Protocol Buffer payloads from the storage layer or cache straight to network sockets.
Spatial computation in Rust NIFs
Computational geometry is CPU-bound by nature — line simplification, polygon clipping (Liang-Barsky, Sutherland-Hodgman), coordinate transformations, MVT 2.1 protobuf encoding.
Rather than letting that math block the primary BEAM schedulers, those operations are delegated to Rust NIFs through Rustler, configured on dirty CPU schedulers. Two properties matter here:
- Zero-copy serialization. Vector geometry buffers pass between BEAM memory and the compiled Rust code with effectively no serialization overhead.
- Memory and thread safety at compile time. Rust's guarantees keep memory leaks and segmentation faults out of the Erlang VM, which is not a place you want either.
OGC API compliance
Most tile renderers do one thing. This one implements the standards a geospatial platform is actually evaluated against:
- OGC API — Tiles (Part 1): pre-rendered or dynamic vector and raster tiles in standard pyramid schemas.
- OGC API — Features (Parts 1–3): queryable vector datasets with spatial filtering, property constraints, and flexible CRS support.
- OGC API — Processes (Part 1): asynchronous execution of heavy spatial jobs — buffers, centroids, convex hulls — managed through BEAM GenServers.
That breadth is unusual. Comparable open-source servers implement tiles only.
Benchmarks
All runs used Vegeta against database-backed tile endpoints over identical PostGIS tables.
Sustained load at 5,000 RPS
Under a constant-rate attack of 5,000 requests per second against PostGIS tile endpoints, TileserveX and Martin both sustained the load at a 100% success rate. Median latency was ~1.3 ms against Martin's ~1.1 ms, P95 ~3.2 ms against ~2.8 ms, and P99 ~6.1 ms against ~5.4 ms. Tail latency stayed bounded — no garbage collection spikes, no scheduler starvation.

Saturation at 20,000 RPS
Pushed to 20,000 requests per second against the same PostGIS instance, the bottleneck moves. It is no longer web routing — it is database socket limits and connection pool saturation. Both engines started shedding a fraction of requests at this point, ~99.7% success for TileserveX against ~99.8% for Martin, with medians of ~2.4 ms and ~2.1 ms.
The interesting result is what did not happen. OTP process isolation, plus delegating CPU-bound work to Rustler's dirty schedulers, kept the BEAM from scheduler starvation and avoided memory spikes at maximum throughput. The two engines degrade the same way, for the same reason, at the same point.

The wider PostGIS ecosystem at 2,000 RPS
Against an un-warmed 2,000 RPS load on the same database, TileserveX ran level with the single-binary Rust servers — ~1.0 ms median against Martin's ~0.9 ms and BBOX's ~1.1 ms — and ahead of the Go-based engines, pg_tileserv at ~1.4 ms and Tegola at ~1.8 ms. All five held a 100% success rate.
The outlier is worth naming, because it is the option a lot of teams reach for first. tiPG, on Python with FastAPI and asyncpg, completed ~82.4% of requests, with a ~45 ms median and a P99 above 250 ms. That is not a tuning gap; it is the concurrency model showing through under sustained load.

What the results mean
The hypothesis was narrow: can an Elixir microservice, stripped down to Plug and Cowboy 2 and accelerated by Rust NIFs, match the execution speed of a native Rust binary without giving up OTP fault tolerance?
On these runs, yes. Median latencies sit within a fraction of a millisecond of Martin at 5,000 RPS and converge near 2 ms at the 20,000 RPS saturation point, which suggests BEAM process orchestration overhead becomes negligible once the full MVC stack is removed. The cost of fault tolerance, supervision, and hot code reloading turns out to be close to zero when you stop paying for the framework you are not using.
That is the useful takeaway for anyone making this decision: the choice between compiled execution speed and actor-model concurrency is less binary than it looks. Elixir for connection routing and process isolation, Rust for the heavy spatial math, is a combination worth considering before you commit to either extreme.