Comparison against tzfpy
~2.17µs per lookup here against ~435ns for tzfpy 1.3.3 - 4.98x slower, over uniformly random query points answered by both packages in the same process on the same machine. That gap is what full-resolution boundary polygons cost; Alternatives is where the trade is argued rather than measured.
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
Tzfpy Version: 1.3.3
Both packages answer the same committed query points (see benchmarks/conftest.py) in the same process, so the ratios below are a measurement rather than two figures from two machines set side by side. Each is called through its own API - timezone_at(lng=, lat=) and get_tz(lng, lat) - with no adapter frame on either side, which at these per-query times would itself be worth tens of percent.
Note
The two packages are not answering quite the same question. This one stores the boundary polygons exactly as the source dataset provides them; tzfpy simplifies them. TimezoneFinderL is measured alongside as the closest thing in this package to the same bargain - it answers from the shortcut index alone and does not read polygon data at all. A speed ratio between different accuracy classes is a price, not a verdict.
Lookup Throughput
Per-query time, derived from one pass over 2,500 points. The last column states this package against tzfpy, computed from these measurements at render time rather than asserted.
Every figure in this section is the min over the measured rounds, not the mean - the estimator this project tracks everywhere, and the only fair one here. Both packages run the identical batch every round, so a slow round is the machine rather than the library; and because tzfpy’s rounds are the shorter ones, that noise lands on its mean hardest. Scoring a competitor on the estimator that flatters this package would not be a measurement. The mean, median and spread of every round are in the full statistics below.
Query points |
tzfpy.get_tz() |
TimezoneFinder.timezone_at() (in-memory) |
TimezoneFinderL.timezone_at() |
vs tzfpy.get_tz() |
|---|---|---|---|---|
random points |
435ns |
2.17µs |
1.50µs |
4.98x slower |
on-land points |
477ns |
2.84µs |
1.55µs |
5.95x slower |
unique-shortcut points |
349ns |
1.42µs |
1.40µs |
4.07x slower |
ambiguous-shortcut points |
958ns |
8.90µs |
2.23µs |
9.29x slower |
Full Statistics
TimezoneFinder.timezone_at() (in-memory)
Configuration |
Mean |
Median |
StdDev |
Min |
Max |
Rounds |
Time/Query (min) |
Throughput (min) |
|---|---|---|---|---|---|---|---|---|
ambiguous-shortcut points |
22.5ms |
22.5ms |
131µs |
22.3ms |
23.2ms |
100 |
8.90µs |
112k/s |
on-land points |
7.19ms |
7.18ms |
51.9µs |
7.10ms |
7.35ms |
100 |
2.84µs |
352k/s |
random points |
5.53ms |
5.50ms |
114µs |
5.42ms |
6.35ms |
100 |
2.17µs |
461k/s |
unique-shortcut points |
3.62ms |
3.60ms |
96.7µs |
3.56ms |
4.43ms |
100 |
1.42µs |
703k/s |
TimezoneFinderL.timezone_at()
Configuration |
Mean |
Median |
StdDev |
Min |
Max |
Rounds |
Time/Query (min) |
Throughput (min) |
|---|---|---|---|---|---|---|---|---|
ambiguous-shortcut points |
5.72ms |
5.70ms |
132µs |
5.58ms |
6.15ms |
100 |
2.23µs |
448k/s |
on-land points |
3.90ms |
3.90ms |
21.9µs |
3.87ms |
3.99ms |
100 |
1.55µs |
646k/s |
random points |
3.80ms |
3.79ms |
32.9µs |
3.76ms |
3.97ms |
100 |
1.50µs |
665k/s |
unique-shortcut points |
3.54ms |
3.54ms |
20.8µs |
3.50ms |
3.61ms |
100 |
1.40µs |
713k/s |
tzfpy.get_tz()
Configuration |
Mean |
Median |
StdDev |
Min |
Max |
Rounds |
Time/Query (min) |
Throughput (min) |
|---|---|---|---|---|---|---|---|---|
ambiguous-shortcut points |
2.47ms |
2.46ms |
87.2µs |
2.39ms |
3.25ms |
100 |
958ns |
1.04M/s |
on-land points |
1.23ms |
1.22ms |
25.9µs |
1.19ms |
1.36ms |
100 |
477ns |
2.10M/s |
random points |
1.11ms |
1.11ms |
21.9µs |
1.09ms |
1.22ms |
100 |
435ns |
2.30M/s |
unique-shortcut points |
902µs |
898µs |
19.1µs |
873µs |
980µs |
100 |
349ns |
2.86M/s |
Time to First Answer
Wall clock of a fresh python -c that imports one package and answers exactly one lookup. This is the honest form of a startup time row, because the two packages spend that time in completely different places: this one imports NumPy and H3 and builds its index when a finder is constructed, while tzfpy imports in about a millisecond and deserialises its index inside the first query. Timing construction alone would score the second as free.
Every row includes interpreter startup, which the baseline row measures on its own. Process launch has a floor and a long, noisy tail, so the bullets below are again the min over the rounds; the mean of a row here can sit tens of percent above its own median, and reading a ranking off it would be reading the scheduler.
Configuration |
Mean |
Median |
StdDev |
Min |
Max |
Rounds |
|---|---|---|---|---|---|---|
bare interpreter (baseline) |
28.9ms |
28.8ms |
794µs |
27.9ms |
30.4ms |
20 |
timezonefinder, file-based |
209ms |
208ms |
2.59ms |
204ms |
215ms |
20 |
timezonefinder, in-memory |
199ms |
199ms |
2.19ms |
196ms |
204ms |
20 |
TimezoneFinderL |
189ms |
188ms |
2.83ms |
184ms |
197ms |
20 |
tzfpy |
194ms |
194ms |
1.46ms |
192ms |
196ms |
20 |
Net of that baseline:
TimezoneFinderL: 156ms to a first answer
tzfpy: 164ms to a first answer
timezonefinder, in-memory: 168ms to a first answer
timezonefinder, file-based: 176ms to a first answer
What This Page Does Not Measure
Accuracy. The two packages disagree on a small fraction of points, and a disagreement count on its own says nothing about which answer is right - settling that needs ground truth, which neither package carries. Alternatives states the design difference instead of scoring it.
Memory footprint and distribution size. TimezoneFinder Memory Footprint measures this package only: the harness behind it (
scripts/measure_memory.py) constructs finders from this repository and has no tzfpy configuration.Any other machine. One CPU, one Python build, one acceleration path, all named above. The ratio survives a change of machine far better than the absolute numbers do, but neither is a promise - see Benchmarking Methodology.