Short answer
FlatGeobuf (.fgb) is a single-file, binary, streamable vector format that stores features in a Google FlatBuffers layout with an embedded packed Hilbert R-tree spatial index. Because there is no parse step and the spatial index lets a reader jump straight to the features inside a bounding box, it is dramatically faster than GeoJSON for large datasets and uniquely good at HTTP range-request streaming — a web map can load only the bytes for its current view from a static file on cloud storage. It is not a replacement for GeoPackage's multi-layer, editable database; it is a fast exchange and read format.
What FlatGeobuf is
FlatGeobuf was created by Björn Harrtell and is supported as a first-class driver in GDAL/OGR, QGIS, and a JavaScript reader for the browser. Its design borrows from FlatBuffers, a zero-copy serialization library: the on-disk bytes are the in-memory representation, so a reader can access a feature's coordinates and attributes without deserializing the whole file. A .fgb file has three parts in order:
- a header (magic bytes, CRS/SRID, schema/columns, feature count, geometry type, index metadata),
- an optional packed Hilbert R-tree spatial index, and
- the feature data, sorted along a Hilbert space-filling curve so spatially-near features sit near each other in the file.
That ordering is the trick: spatially adjacent features are byte-adjacent, so a bounding-box read touches a small, contiguous region of the file.
Why it is fast
Three properties compound:
- No parsing. Unlike GeoJSON (which must be fully tokenised and parsed before a single feature is usable), FlatGeobuf is read with simple offset arithmetic. There is no JSON overhead and no full-file pass.
- Spatial index built in. A shapefile needs a sidecar
.qix/.sbn; GeoJSON has nothing. FlatGeobuf's R-tree means a bbox query isO(log n + k)instead of scanning every feature. - Streamable over HTTP. This is the headline feature. The format is laid out so a client can issue HTTP range requests: read the header, read the R-tree, compute which feature byte-ranges intersect the viewport, then request only those bytes. The file can be a plain object on S3, Azure Blob, or any static host — no tile server, no database, no GeoServer. A 5 GB national dataset can back an interactive web map while the browser only ever downloads a few hundred kilobytes per pan.
When to use it (and when not)
Reach for FlatGeobuf when:
- you need to stream a large vector layer to a web map from static cloud storage,
- you are doing bulk sequential or bbox reads of one layer (analytics, ETL ingestion),
- you want a single self-contained file with a spatial index and CRS baked in, and
- the data is read-mostly after creation.
Prefer GeoPackage when you need multiple layers in one container, in-place editing, attribute SQL and indexes, or a working project database. Prefer GeoJSON only for small, human-readable, web-API payloads. Prefer (Geo)Parquet for column-oriented analytical workloads in data-lake tooling. FlatGeobuf is single-layer and row-oriented; it is not a query engine and is not designed for frequent in-place updates.
| Need | Best choice |
|---|---|
| Multi-layer editable project | GeoPackage |
| Stream large layer to browser from static host | FlatGeobuf |
| Tiny human-readable web payload | GeoJSON |
| Multi-user concurrent editing & SQL | PostGIS |
| Columnar analytics in a data lake | GeoParquet |
Converting to and from FlatGeobuf
GDAL handles it directly:
# Shapefile or GeoPackage -> FlatGeobuf (index built by default)
ogr2ogr -f FlatGeobuf faults.fgb faults.gpkg faults
# Reproject on the way out to WGS84 for web streaming
ogr2ogr -f FlatGeobuf faults_wgs84.fgb faults.gpkg faults -t_srs EPSG:4326
# FlatGeobuf -> GeoPackage for editing
ogr2ogr -f GPKG faults.gpkg faults.fgb
By default OGR writes the spatial index; you can disable it with the layer creation option SPATIAL_INDEX=NO for a write-once, read-sequentially file, but keep it on for anything served over HTTP. Inspect the result:
ogrinfo -so faults.fgb
Check the feature count, geometry type, and that the SRS matches what your consumer expects.
Worked example: serverless web map of a large layer
You have a 2 GB national geology polygon layer in PostGIS and want an interactive web map without standing up a tile server.
- Export to FlatGeobuf in WGS84 with the index on:
ogr2ogr -f FlatGeobuf geology.fgb PG:"dbname=geo" geology_units -t_srs EPSG:4326 - Upload
geology.fgbto an object store (S3/Azure Blob) and enable CORS and HTTP range requests (Accept-Ranges: bytes). - In the browser, use the FlatGeobuf JS reader with a bounding box bound to the map viewport; it range-requests only intersecting features.
- As the user pans, only the new viewport's feature bytes are fetched — initial load is a few hundred KB, not 2 GB.
The cost is near-zero static hosting; the constraint is that the file is read-only — to change the data you re-export and re-upload.
How the Hilbert curve makes streaming work
The reason FlatGeobuf streams well is worth understanding, because it explains both its strength and its limits. A Hilbert curve is a space-filling curve that visits every cell of a 2D grid in an order that keeps spatially-near cells near each other along the 1D path. By sorting features along this curve before writing them, FlatGeobuf ensures that features close together on the map are close together in the file's byte stream. The packed R-tree index then maps any query bounding box to a small set of contiguous byte ranges.
This is what lets a browser be economical. When the map shows a city, the client reads the header and index (small, fixed cost), intersects the viewport with the R-tree, and issues HTTP range requests for just the byte ranges covering that city's features — typically one or a few contiguous reads rather than thousands of scattered ones. The same property is why FlatGeobuf is poor for random single-feature updates: changing one feature's geometry can change its Hilbert position, which would require re-sorting and rewriting. The format optimises bulk and spatial reads at the deliberate expense of in-place mutation, which is the correct trade for an exchange format and the wrong one for a working database.
Common pitfalls and why they happen
- Expecting random row updates. FlatGeobuf is not a database; editing means rewriting the file. Keep your editable master in GeoPackage/PostGIS and treat
.fgbas a derived artefact. - Streaming fails over HTTP. If the host does not honour range requests or CORS, the browser falls back to downloading the whole file (or errors). Verify
Accept-Ranges: bytesand CORS headers. - Wrong CRS for web use. Web clients expect EPSG:4326 (or reproject client-side); shipping a UTM
.fgbto a Leaflet/MapLibre layer misplaces everything. Reproject with-t_srson export. - Disabling the index then querying by bbox. With
SPATIAL_INDEX=NO, every bbox read scans the whole file — the opposite of the format's strength. - Treating it like GeoJSON for tiny payloads. Binary FlatGeobuf is overkill (and not human-readable) for a 20-feature API response; GeoJSON is fine there.
Quality checks
ogrinfo -soto confirm feature count, geometry type, and SRS after conversion.- Confirm the spatial index is present (default) for any HTTP-served file.
- Test a range request against the hosted file (
curl -r 0-99 -I <url>) and confirm a206 Partial Contentresponse. - Validate a sample of geometries round-tripped back to GeoPackage with
ST_IsValid. - Document the CRS, source, and date alongside the file as you would any delivery.
Bathyl perspective
We use FlatGeobuf as the delivery layer for large, read-mostly datasets that need to drive a web map without a server, keeping the editable master in GeoPackage or PostGIS. The combination of a single file, a built-in spatial index, and HTTP range streaming lets a client explore a multi-gigabyte layer interactively from plain object storage — fast, cheap, and reproducible.
Related reading
- When to Use GeoPackage Instead of Shapefile
- Metadata Every GIS Dataset Needs
- Spatial Data Delivery Checklist
- Spatial data products