Point-in-Polygon Algorithm Performance Benchmark
~621ns per check on a small polygon, ~1.16µs on the largest (1.87x) - the kernel a lookup reaches, which skips the parts of a ring a horizontal ray cannot cross and is therefore nearly flat in polygon size.
The same check over an unindexed coordinate array is ~38.9µs on the largest polygon (33.4x the packed cost) - which is what the stratification below is for, and what the latitude block index removed.
This page describes one point-in-polygon implementation. The other two are measured against it in Point-in-Polygon Acceleration Paths - which is where the ranking between them is stated, since it is a measurement that moves and a claim repeated in prose would not.
Measured on Linux x86_64, AMD EPYC 9V74 80-Core Processor @ 2.8701 GHz, Python 3.13.15, using the C extension (clang) point-in-polygon path. This is the configuration continuous integration tracks - what a plain pip install timezonefinder gives you. See Benchmarking Methodology.
Continuous integration tracks none of the rows on this page. This published table leads with Mean and belongs to the full on-demand suite, while the trend chart records the min estimator for the smaller benchmark_core subset.
System Status
Python Environment
Python Version: 3.13.15 (CPython)
NumPy Version: 2.5.2
Platform: Linux x86_64
Processor: x86_64
TimezoneFinder Configuration
C Implementation Available: True
Numba JIT Available: False
Performance Optimizations
✓ Compiled C extension for point-in-polygon operations
✗ Numba JIT compilation not available
Benchmark Input Provenance
Fixture Version: 3
Timezone Data Version: 2026c
Benchmark Configuration
Benchmark Source: pytest-benchmark
Batch Size: 2,500
Polygon Strata: small / medium / large (by vertex count percentile)
Each benchmark times one pass over 2,500 fixed, committed (point, polygon) pairs drawn from a single polygon-size stratum, so the cost of the largest polygons isn’t hidden behind an unweighted average. Mean/Median/StdDev/Min/Max are for the full 2,500-pair batch; Throughput is queries/second for that batch.
Note
The point and the polygon in each pair are drawn independently, so many pairs put the point nowhere near the polygon. That does not matter for the bare kernel, which scans the whole ring either way, but it means a share of the block-filtered checks are rejections rather than scans - cheapest on the small stratum, where a rejection is most of what is left. A real lookup reaches this stage only after a bounding-box check has passed, so read the block-filtered figures as a floor and Timezone Finding Performance Benchmark for what a query actually pays.
Results
bare kernel (C/clang)
Configuration |
Mean |
Median |
StdDev |
Min |
Max |
Rounds |
Throughput |
|---|---|---|---|---|---|---|---|
large polygons |
97.1ms |
97.1ms |
72.4µs |
97.0ms |
97.3ms |
15 |
25.7k/s |
medium polygons |
11.6ms |
11.6ms |
96.0µs |
11.5ms |
11.8ms |
15 |
216k/s |
small polygons |
4.48ms |
4.42ms |
120µs |
4.39ms |
4.78ms |
15 |
559k/s |
packed kernel (C/clang)
Configuration |
Mean |
Median |
StdDev |
Min |
Max |
Rounds |
Throughput |
|---|---|---|---|---|---|---|---|
large polygons |
2.91ms |
2.91ms |
19.3µs |
2.89ms |
2.96ms |
15 |
859k/s |
medium polygons |
1.71ms |
1.70ms |
28.1µs |
1.68ms |
1.77ms |
15 |
1.46M/s |
small polygons |
1.55ms |
1.55ms |
14.1µs |
1.54ms |
1.59ms |
15 |
1.61M/s |
bare kernel (pure Python)
Configuration |
Mean |
Median |
StdDev |
Min |
Max |
Rounds |
Throughput |
|---|---|---|---|---|---|---|---|
large polygons |
18.5s |
18.5s |
155ms |
18.3s |
18.8s |
15 |
135/s |
medium polygons |
1.25s |
1.28s |
42.5ms |
1.20s |
1.30s |
15 |
1.99k/s |
small polygons |
46.3ms |
46.2ms |
271µs |
45.9ms |
46.7ms |
15 |
54.0k/s |
packed kernel (pure Python)
Configuration |
Mean |
Median |
StdDev |
Min |
Max |
Rounds |
Throughput |
|---|---|---|---|---|---|---|---|
large polygons |
504ms |
504ms |
2.02ms |
501ms |
509ms |
15 |
4.96k/s |
medium polygons |
159ms |
158ms |
1.66ms |
158ms |
164ms |
15 |
15.7k/s |
small polygons |
8.47ms |
8.38ms |
328µs |
8.30ms |
9.62ms |
15 |
295k/s |
Performance Summary
What the stored index and payload buy, per polygon-size stratum - the same C predicate over the same pairs, reading the packed collection against reading a plain coordinate array with nothing in front of it:
Small polygons: packed kernel (C/clang) is 188% faster (2.88x) than bare kernel (C/clang) (1.55ms vs 4.48ms)
Medium polygons: packed kernel (C/clang) is 579% faster (6.79x) than bare kernel (C/clang) (1.71ms vs 11.6ms)
Large polygons: packed kernel (C/clang) is 3237% faster (33.4x) than bare kernel (C/clang) (2.91ms vs 97.1ms)
The C extension against pure Python, on the kernel a lookup reaches. Which of the two interpreted implementations these rows describe is decided by the measuring environment, not by the benchmark - see Point-in-Polygon Acceleration Paths, which measures all three against each other:
Small polygons: packed kernel (C/clang) is 445% faster (5.45x) than packed kernel (pure Python) (1.55ms vs 8.47ms)
Medium polygons: packed kernel (C/clang) is 9220% faster (93.2x) than packed kernel (pure Python) (1.71ms vs 159ms)
Large polygons: packed kernel (C/clang) is 17204% faster (173x) than packed kernel (pure Python) (2.91ms vs 504ms)
Overall: fastest is packed kernel (C/clang) - small polygons (1.55ms), slowest is packed kernel (pure Python) - large polygons (504ms) - 32322% faster (324x)