Custom Polygons vs. Uber's H3: Building a High-Performance Geofencing Backend in Go

Published: (March 31, 2026 at 02:46 PM EDT)
4 min read
Source: Dev.to

Source: Dev.to

When building logistics and telemetry platforms, processing thousands of GPS pings per second is routine. The core challenge isn’t just receiving the data; it’s determining exactly where that data lies in relation to your business logic. Is the truck inside the warehouse? Did it cross a restricted zone?

If you are building a geofencing architecture, you will inevitably face the dilemma: Should you use exact custom polygons (PostGIS) or spatial‑indexing grids like Uber’s H3?
Spoiler alert: For a truly scalable and user‑friendly system, you need both. Below is a hybrid approach using Go and PostGIS.

The Reality: Users Don’t Think in Hexagons

From a purely mathematical standpoint, Uber’s H3 is a masterpiece. But from a UX perspective, asking a warehouse manager to draw their loading dock using only rigid hexagons is a terrible idea. Real‑world facilities (e.g., distribution centers) are irregular, and users need to draw custom polygons on a map. In our Go backend we store these polygons directly in PostgreSQL using PostGIS.

The Standard Approach: PostGIS ST_Contains

When a GPS ping reaches our Go server, the most straightforward approach is to ask PostGIS whether the point lies inside any stored polygon.

// A simplified example of checking a GPS ping against PostGIS polygons
package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/lib/pq"
)

func checkGeofence(db *sql.DB, lat, lng float64) (string, error) {
    query := `
        SELECT fence_id, name 
        FROM geofences 
        WHERE ST_Contains(geom, ST_SetSRID(ST_MakePoint($1, $2), 4326));
    `

    var fenceID, name string
    err := db.QueryRow(query, lng, lat).Scan(&fenceID, &name)
    if err != nil {
        if err == sql.ErrNoRows {
            return "", nil // Not in any geofence
        }
        return "", err
    }

    return name, nil
}

ST_Contains uses spatial math (e.g., ray casting) to determine point‑in‑polygon. Even with GiST indexes, running complex spatial intersections for 10 000 pings per second can overwhelm the database CPU and spike cloud costs.

The Scaling Secret: Enter Uber’s H3

When we need to eliminate database latency and scale massively, we bring H3 into the backend. H3 divides the world into a grid of hexagons. Instead of performing expensive geometry calculations, H3 converts a latitude/longitude pair into a simple identifier (e.g., 8928308280fffff). Checking whether a truck is in a zone then becomes an O(1) hash‑map lookup.

The Hybrid Architecture: “Polyfill”

We don’t force users to draw hexagons. Users draw precise custom polygons, and an asynchronous worker “fills” each polygon with H3 hexagons—a process called polyfill. The resulting hexagon IDs are stored in a fast key‑value store (e.g., Redis) or an indexed table.

package main

import (
    "fmt"
    "github.com/uber/h3-go/v4"
)

// Convert a user‑defined polygon into H3 cells
func indexGeofenceWithH3(polygon []h3.LatLng, resolution int) []h3.Cell {
    // Create a GeoLoop from the user's custom polygon
    geoLoop := h3.NewGeoLoop(polygon)
    geoPolygon := h3.NewGeoPolygon(geoLoop, nil)

    // Fill the polygon with H3 hexagons at the desired resolution
    hexagons := h3.PolygonToCells(geoPolygon, resolution)

    return hexagons
}

// Handle an incoming GPS ping using the H3 index
func handleIncomingPing(lat, lng float64) {
    // 1. Convert incoming ping to H3 index instantly
    latLng := h3.NewLatLng(lat, lng)
    resolution := 9 // Roughly city‑block size
    cell := h3.LatLngToCell(latLng, resolution)

    // 2. O(1) lookup: check if this cell ID exists in our Redis cache
    // cache.Exists(cell.String())
    fmt.Printf("Ping mapped to Hexagon: %s\n", cell.String())
}

Benefits of the Hybrid Approach

  • UX / Precision: Users draw exact custom polygons.
  • Storage: PostGIS remains the source of truth for the exact geometries.
  • Real‑Time Processing: Go converts incoming pings to H3 indexes and checks them against a cached set of polyfilled hexagons in microseconds.

There is no silver bullet in software engineering, but for real‑time logistics telemetry, combining PostGIS precision with H3 speed is as close as it gets.

0 views
Back to Blog

Related posts

Read more »