Optimizing GeoJSON Payloads for APIs
Optimizing GeoJSON payloads for APIs requires a multi-layered approach: reduce coordinate precision to 5–6 decimal places, strip unused properties, apply server-side bounding-box clipping, compress responses with Brotli or gzip, and stream large datasets using chunked transfer encoding. For production systems serving millions of requests, consider migrating to binary geospatial formats like FlatGeobuf or GeoParquet when client-side parsing latency becomes a bottleneck.
GeoJSON’s text-based JSON structure introduces inherent serialization overhead. Every coordinate pair is stored as a floating-point string, and nested arrays multiply the payload size. Understanding GeoJSON Overhead and Serialization Costs is critical before applying optimizations, as blind compression without structural pruning often yields diminishing returns. The following strategies target the exact bottlenecks that impact API latency, bandwidth consumption, and client memory allocation.
1. Reduce Coordinate Precision
GPS accuracy rarely exceeds 5–6 decimal places (~1.1 meters at the equator). Most GIS libraries export coordinates with 15+ decimal places, which adds ~40% unnecessary payload weight. Trimming precision slashes transmission size without measurable spatial degradation for web mapping, routing, or analytics workloads.
The following Python utility recursively rounds coordinates while preserving the RFC 7946 structure and avoids mutating the original object:
import json
from typing import Any
def round_geojson_coords(obj: Any, precision: int = 6) -> Any:
"""Recursively round coordinates in a GeoJSON object without mutating input."""
if isinstance(obj, list):
# Heuristic: coordinate arrays contain only numbers and length 2 or 3
if len(obj) in (2, 3) and all(isinstance(x, (int, float)) for x in obj):
return [round(x, precision) for x in obj]
return [round_geojson_coords(item, precision) for item in obj]
elif isinstance(obj, dict):
return {k: round_geojson_coords(v, precision) for k, v in obj.items()}
return obj
def serialize_geojson(data: dict, precision: int = 6) -> bytes:
"""Round coordinates and return minified JSON bytes."""
rounded = round_geojson_coords(data, precision)
return json.dumps(rounded, separators=(",", ":")).encode("utf-8")
Implementation notes:
- Use
separators=(",", ":")to strip whitespace. This alone reduces payload size by 15–20%. - Avoid rounding inside database queries; it’s faster and safer to handle at the serialization layer.
- For real-time streaming, apply precision reduction during chunk generation rather than buffering the full response.
2. Enforce Server-Side Spatial Filtering
Never transmit full datasets to the client. Apply bounding-box clipping and spatial indexing at the query layer before serialization. In PostGIS, use ST_Intersects or ST_MakeEnvelope to filter geometries server-side. In GeoPandas or DuckDB, leverage spatial indexes (spatialindex or spatial_filter) to avoid loading out-of-viewport features into memory. Combine this with property projection (SELECT id, name, geom instead of SELECT *) to eliminate unused attributes.
Best practices for API endpoints:
- Viewport-driven queries: Accept
bbox=min_lon,min_lat,max_lon,max_latparameters and translate them directly into spatial predicates. - Index utilization: Ensure your geometry column uses a spatial index (e.g., GiST in PostGIS). Without it, bounding-box filters trigger full table scans, negating any payload optimization.
- Feature thinning: For zoomed-out map views, apply server-side simplification (
ST_SimplifyorST_SnapToGrid) to reduce vertex counts before serialization.
3. Enable HTTP Compression & Streaming
Always enable HTTP compression. Brotli typically achieves 20–30% better compression ratios than gzip for JSON-like payloads, especially when coordinate strings repeat. Pair compression with chunked transfer encoding to stream large FeatureCollections without holding the entire response in memory.
Configure your API gateway or reverse proxy to negotiate compression dynamically:
- Set
Accept-Encoding: br, gzipin client requests. - Ensure
Content-Encoding: bris returned when supported. - Use streaming serializers (e.g.,
ijson,orjsonwith generators, or FastAPI’sStreamingResponse) to emit chunks as they’re processed.
Refer to MDN Web Docs on HTTP Compression for implementation guidelines across Nginx, Cloudflare, and application servers.
Memory impact: Streaming + compression reduces peak RAM usage by 60–80% on endpoints returning >50MB of GeoJSON. This prevents OOM crashes during concurrent requests and keeps p95 latency stable.
4. Transition to Binary Formats at Scale
When client-side parsing latency or bandwidth costs dominate your SLA, text-based GeoJSON becomes a liability. Binary geospatial formats eliminate string serialization overhead, preserve coordinate precision natively, and support zero-copy memory mapping.
- FlatGeobuf: A streaming-friendly binary format that supports spatial indexing and HTTP range requests. Ideal for web clients that need progressive loading without downloading entire files.
- GeoParquet: Columnar storage optimized for analytical workloads. Best suited for data pipelines, batch exports, and server-side aggregations rather than direct browser rendering.
Evaluate your architecture against broader storage trade-offs documented in Geospatial Storage Fundamentals & Format Comparison before committing to a migration path. Binary formats require client-side decoders (e.g., flatgeobuf-js, geoparquet loaders), so factor in SDK size and browser compatibility.
Quick Optimization Checklist
| Bottleneck | Primary Fix | Expected Payload Reduction |
|---|---|---|
| Excessive decimal places | Round to 5–6 precision | 30–40% |
| Unused properties | Server-side projection | 15–25% |
| Full-dataset transfers | Bounding-box + spatial index | 70–95% (viewport-dependent) |
| Text serialization | Brotli/gzip + chunked streaming | 50–70% |
| Client parsing latency | FlatGeobuf / GeoParquet | 60–80% (binary vs JSON) |
Implement these layers incrementally. Start with precision reduction and spatial filtering, enable compression at the edge, and only migrate to binary formats when profiling confirms JSON parsing or bandwidth as your primary constraint.