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 pentagonis_pent = h3.h3_is_pentagon('8009fffffffffff') # Resolution 0 exampleprint(is_pent) # True
# Pentagons have only 5 neighborsneighbors = 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 apartrestaurant_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 extensionCREATE EXTENSION h3;
-- 2. Add H3 index columnALTER TABLE locations ADD COLUMN h3_index h3index;
-- 3. Populate H3 indexes (one-time or on insert trigger)UPDATE locationsSET h3_index = h3_lat_lng_to_cell(lat, lng, 9);
-- 4. Create B-tree index on H3 columnCREATE INDEX idx_locations_h3 ON locations (h3_index);
-- 5. Query nearby locations-- Find all locations in target cell + 2-hop neighborsSELECT * FROM locationsWHERE h3_index = ANY( SELECT h3_grid_disk('8928308280bffff', 2));-- Returns ~19 hexagons worth of results
-- 6. Aggregate by hexagonSELECT h3_index, COUNT(*) as location_count, AVG(rating) as avg_ratingFROM restaurantsWHERE 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 redisimport h3
r = redis.Redis()
# Store driver locations by H3 celldef 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 Case | Recommended Resolution | Cell Size |
|---|---|---|
| City-level heatmaps | 5-7 | ~1-8 km edge |
| Neighborhood analysis | 7-9 | ~175m-1.2km edge |
| Building/venue level | 9-11 | ~9-175m edge |
| Room-level precision | 12+ | < 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 locationsWHERE h3_lat_lng_to_cell(lat, lng, 9) IN (nearby_cells);Do precompute and index:
-- ✅ GOOD: Uses indexed column (fast!)SELECT * FROM locationsWHERE 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 closestThis 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!