Logo
H3 Performance & Integration

H3 Performance & Integration

Nov 21, 2025
7 min read

Trade-Offs and Limitations

H3 isn’t perfect. Understanding its limitations helps you use it effectively and avoid pitfalls.

1. Pentagons

Due to the icosahedron projection, there are 12 pentagons (5-sided cells instead of 6-sided) at each resolution, located at the icosahedron vertices.

Impact:

  • Edge cases in neighbor traversal algorithms (pentagons have 5 neighbors, not 6)
  • Slightly distorted areas near pentagons
  • Some H3 operations return errors or special handling for pentagons
  • Parent-child relationships differ (pentagons don’t split into 7 children)
# Check if a cell is a pentagon
is_pent = h3.h3_is_pentagon('8009fffffffffff') # Resolution 0 example
print(is_pent) # True
# Pentagons have only 5 neighbors
neighbors = h3.k_ring('8009fffffffffff', 1)
print(len(neighbors)) # 6 (cell + 5 neighbors, not cell + 6)
Warning

Pentagon Locations: They’re strategically placed in oceans when possible (to minimize impact on land-based analysis), but resolution 0 has pentagons over Antarctica, Alaska, and other locations. Always use H3 library functions instead of manual indexing to handle pentagons correctly.

Mitigation:

  • H3 library functions handle pentagons automatically
  • If writing custom algorithms, use h3.h3_is_pentagon() to check and branch
  • For most applications, pentagons are rare enough (~0.001% of cells) to ignore

2. Discretization Error

Hexagons are approximations of continuous space. Two points in the same hexagon are treated as identical.

Example: At resolution 9 (~0.1km² cells, ~175m edge), two points 100m apart might map to:

  • Same cell → Treated as identical (0m apart in H3 logic)
  • Adjacent cells → 1 hop apart (depending on hexagon boundary placement)

Real-world impact:

# Two restaurants 100m apart
restaurant_a_h3 = h3.geo_to_h3(37.7749, -122.4194, 9)
restaurant_b_h3 = h3.geo_to_h3(37.7758, -122.4194, 9) # ~100m north
# Might be same cell or different cells!
if restaurant_a_h3 == restaurant_b_h3:
print("H3 treats them as identical location")
else:
print("H3 treats them as separate locations")

Mitigation:

  • Choose resolution appropriate for your accuracy needs
  • Use hybrid approach: H3 for filtering, exact distance for final results
  • For very precise applications (< 10m), use resolution 12+
  • Store raw lat/lng alongside H3 index for exact fallback

3. Storage Overhead

Every point needs an H3 index stored alongside its original coordinates (if you need exact positions).

Example:

  • Raw coordinates: 16 bytes (2 × 8-byte doubles)
  • H3 index: 8 bytes (64-bit integer)
  • Total: 24 bytes per location

For 1 billion locations:

  • Coordinates alone: 16 GB
  • With H3 indexes: 24 GB
  • Overhead: 8 GB (50% increase)

Mitigation:

  • If you only need H3 for analysis, can drop raw coordinates for some use cases
  • Multiple resolution indexes multiply storage (8 GB per resolution)
  • Index compression: H3 indexes compress well (many share prefixes)

4. Not Ideal for Linear Features

H3 is optimized for area-based analysis (density, coverage, aggregation). It’s less efficient for:

Road networks:

  • Roads are linear (1D), hexagons are areal (2D)
  • A straight road crosses many hexagon boundaries
  • Better to use specialized routing graphs (OpenStreetMap, GraphHopper)

Pipelines or railroads:

  • Long linear features don’t align with hexagonal grid
  • Use S2 (square grid better for edges) or specialized geometry

Navigation and routing:

  • H3 doesn’t understand road connectivity or turn restrictions
  • Use dedicated routing algorithms (Dijkstra, A* on road graphs)
Important

When to use H3 for linear features: If you’re analyzing aggregate patterns (traffic density along a corridor, congestion in road segments), H3 can still be useful. Just don’t use it for routing or precise path calculations.

5. Learning Curve

Understanding hierarchical hexagonal indexing requires upfront investment:

Your team needs to learn:

  • Resolution selection (which resolution for which use case?)
  • Hierarchy navigation (parent/child operations)
  • Neighbor traversal logic (k-ring vs hex-ring vs grid-distance)
  • When to aggregate vs. drill down
  • Pentagon edge cases

Documentation and tooling:

  • H3 documentation is excellent, but assumes geospatial knowledge
  • Debugging can be tricky (why did this cell get included in polyfill?)
  • Visualization tools help (Kepler.gl, H3-React components)

Mitigation:

  • Start with simple use cases (proximity search)
  • Build shared libraries wrapping common patterns
  • Invest in visualization tools for debugging

Integration and Performance

Database Integration

PostgreSQL with h3-pg Extension

-- 1. Install extension
CREATE EXTENSION h3;
-- 2. Add H3 index column
ALTER TABLE locations ADD COLUMN h3_index h3index;
-- 3. Populate H3 indexes (one-time or on insert trigger)
UPDATE locations
SET h3_index = h3_lat_lng_to_cell(lat, lng, 9);
-- 4. Create B-tree index on H3 column
CREATE INDEX idx_locations_h3 ON locations (h3_index);
-- 5. Query nearby locations
-- Find all locations in target cell + 2-hop neighbors
SELECT * FROM locations
WHERE h3_index = ANY(
SELECT h3_grid_disk('8928308280bffff', 2)
);
-- Returns ~19 hexagons worth of results
-- 6. Aggregate by hexagon
SELECT
h3_index,
COUNT(*) as location_count,
AVG(rating) as avg_rating
FROM restaurants
WHERE h3_index = ANY(
SELECT h3_grid_disk(h3_lat_lng_to_cell(37.7749, -122.4194, 9), 5)
)
GROUP BY h3_index;

Performance:

  • Index lookup: ~1-10ms for 1M rows
  • Aggregate query: ~10-50ms depending on cell count
  • Scales linearly with number of hexagons queried

Redis with H3

import redis
import h3
r = redis.Redis()
# Store driver locations by H3 cell
def index_driver(driver_id, lat, lng):
h3_index = h3.geo_to_h3(lat, lng, 9)
# Add to set for this cell
r.sadd(f'drivers:cell:{h3_index}', driver_id)
# Store driver details
r.hset(f'driver:{driver_id}', mapping={
'lat': lat,
'lng': lng,
'h3_index': h3_index
})
# Find nearby drivers (fast!)
def find_nearby_drivers(rider_lat, rider_lng, k_ring=2):
rider_h3 = h3.geo_to_h3(rider_lat, rider_lng, 9)
nearby_cells = h3.k_ring(rider_h3, k_ring)
# Collect drivers from all nearby cells
drivers = set()
for cell in nearby_cells:
cell_drivers = r.smembers(f'drivers:cell:{cell}')
drivers.update(cell_drivers)
return list(drivers)
# Update driver location (must remove from old cell, add to new)
def update_driver_location(driver_id, new_lat, new_lng):
# Get old cell
old_h3 = r.hget(f'driver:{driver_id}', 'h3_index')
# Calculate new cell
new_h3 = h3.geo_to_h3(new_lat, new_lng, 9)
# Only update if cell changed
if old_h3 != new_h3:
r.srem(f'drivers:cell:{old_h3}', driver_id)
r.sadd(f'drivers:cell:{new_h3}', driver_id)
# Update driver details
r.hset(f'driver:{driver_id}', mapping={
'lat': new_lat,
'lng': new_lng,
'h3_index': new_h3
})

Performance:

  • Driver indexing: < 1ms
  • Nearby driver search: < 5ms for k_ring=2 (19 cells)
  • Scales to millions of drivers with Redis clustering

Performance Characteristics

Coordinate → H3 Index:

  • ~100-500 nanoseconds (pure computation, no I/O)
  • CPU-bound, highly optimized C implementation
  • No allocations, suitable for hot paths

k-Ring (k=2):

  • ~1-5 microseconds (returns 19 cells)
  • Precomputed lookup tables for small k
  • Scales with 3k(k+1) formula

Polyfill (medium polygon):

  • ~10-100 microseconds (depends on resolution and polygon complexity)
  • Polygon with 100 vertices at resolution 9: ~50μs
  • Scales with polygon area and resolution

Database query with H3 index:

  • ~1-10 milliseconds (standard B-tree index lookup)
  • Depends on number of cells queried (k-ring size)
  • ~1ms per 10-20 cells on modern hardware
Tip

At Uber Scale: H3 enables 100,000+ geospatial queries per second on a single server by replacing expensive distance calculations with cheap index lookups and set operations.

Best Practices

1. Choose the Right Resolution

Too Coarse (resolution 5-7):

  • ❌ Lumps together unrelated areas
  • ❌ Poor accuracy for small-scale features
  • ❌ User experience: “Why is this restaurant 2km away showing as ‘nearby’?”

Too Fine (resolution 12-15):

  • ❌ Too many cells (storage bloat)
  • ❌ Excessive query complexity (k_ring returns hundreds of cells)
  • ❌ Diminishing returns on accuracy

Sweet Spot for most applications:

Use CaseRecommended ResolutionCell Size
City-level heatmaps5-7~1-8 km edge
Neighborhood analysis7-9~175m-1.2km edge
Building/venue level9-11~9-175m edge
Room-level precision12+< 9m edge

Rule of thumb: Choose resolution where cell edge is ~1/3 of your typical query radius. For a 1km proximity search, use resolution 9 (175m edge).

2. Precompute and Store H3 Indexes

Don’t convert coordinates on every query:

-- ❌ BAD: Converts on every query (slow!)
SELECT * FROM locations
WHERE h3_lat_lng_to_cell(lat, lng, 9) IN (nearby_cells);

Do precompute and index:

-- ✅ GOOD: Uses indexed column (fast!)
SELECT * FROM locations
WHERE h3_index IN (nearby_cells);

Implementation:

  • Add H3 column to table schema
  • Populate on insert/update (trigger or application logic)
  • Create B-tree index on H3 column
  • Refresh/rebuild periodically if using dynamic resolutions

3. Use Appropriate k-Ring Values

k=1: Immediate neighbors

  • Returns: 7 cells (center + 6 neighbors)
  • Coverage: ~1.2km² at resolution 9
  • Use for: Tight proximity (within ~500m)

k=2: 2-hop neighbors

  • Returns: 19 cells (center + 2 rings)
  • Coverage: ~3.4km² at resolution 9
  • Use for: Medium proximity (within ~1km)

k=3: 3-hop neighbors

  • Returns: 37 cells (center + 3 rings)
  • Coverage: ~6.5km² at resolution 9
  • Use for: Wide proximity (within ~1.5km)

Warning: k grows quadratically!

  • k=5: 91 cells
  • k=10: 331 cells
  • k=20: 1,261 cells

For large radii, consider using lower resolution instead of large k values.

4. Combine with Traditional Indexes

Use H3 as a fast filter, then refine with exact calculations:

# Pattern: H3 filter + exact refinement
# Step 1: Fast H3 filter (reduces 1M points to ~100 candidates)
user_h3 = h3.geo_to_h3(user_lat, user_lng, 9)
candidate_cells = h3.k_ring(user_h3, 2)
candidates = db.query("""
SELECT * FROM restaurants
WHERE h3_index IN (%s)
""", candidate_cells)
# Returns ~100 restaurants
# Step 2: Exact distance refinement (only 100 calculations, not 1M!)
results = []
for restaurant in candidates:
distance = haversine_distance(
user_lat, user_lng,
restaurant.lat, restaurant.lng
)
if distance < 2000: # 2km exact radius
results.append({
'restaurant': restaurant,
'distance_m': distance
})
results.sort(key=lambda x: x['distance_m'])
return results[:10] # Top 10 closest

This hybrid approach gets 99% of H3’s speed with 100% geographic accuracy.

Conclusion

H3 transforms geospatial problems from complex geometric calculations into simple index lookups. By discretizing the world into hierarchical hexagons, it makes location-based queries orders of magnitude faster while enabling new types of analysis.

Key Takeaways:

  • Understand trade-offs: Pentagons, discretization error, storage overhead
  • Choose resolution wisely: Balance accuracy vs. performance
  • Integrate properly: Precomputed indexes, hybrid filtering
  • Use best practices: Appropriate k-ring values, database indexing

When to use H3:

  • ✅ Area-based analysis (density, coverage, heatmaps)
  • ✅ Aggregation and drill-down at multiple resolutions
  • ✅ Fast proximity queries at scale
  • ✅ Privacy-preserving location data

When NOT to use H3:

  • ❌ Precise distance measurements (use exact calculations)
  • ❌ Linear features like roads (use specialized routing systems)
  • ❌ Navigation and routing (use routing graphs)
  • ❌ Sub-meter precision requirements

Understanding H3 deepens your knowledge of spatial indexing, hierarchical data structures, and performance trade-offs in distributed systems. H3 is open-source (h3geo.org) with libraries for Python, JavaScript, Java, Go, and more.

Start experimenting to see how hexagonal thinking can transform your geospatial applications!