GeoPackage (.gpkg): The Definitive Guide

Someone sent you a .gpkg and you don't know what to do with it. Or you have a folder full of Shapefiles, you've been told GeoPackage is "the modern replacement", and you want to know whether that's true or just fashion.

Both answers are below, and everything factual on this page was tested, not copied. We built GeoPackages and Shapefiles with GDAL 3.13.1 and SQLite 3.53.3, ran the conversions, opened the files with a plain SQLite client and read the bytes. Where a claim comes from the OGC specification rather than from our terminal, we say so. Everything here was verified on 14 July 2026.

One thing up front, because most articles get it wrong: a Shapefile does not lose your booleans. It loses other things — worse things — and we'll show you exactly which.

The mental unlock: a GeoPackage is a SQLite database

This is the whole thing. Once it clicks, everything else follows.

A .gpkg file is an ordinary SQLite database with a standardised set of tables defined by the OGC GeoPackage spec. It is not a new binary container. It is not a zip. It is SQLite, and every SQLite tool on earth can already open it.

Run file parcelas.gpkg and you get back SQLite 3.x database (OGC GeoPackage file), user version 10400. Read the first sixteen bytes and they are literally SQLite format 3\0. What makes it a GeoPackage rather than any old database is a four-byte application_id at offset 68, which spells GPKG in ASCII, plus a user_version — we measured 10400, meaning GeoPackage 1.4.

So this works, with no GIS software installed at all:

  • See what's inside: sqlite3 parcelas.gpkg "SELECT table_name, data_type FROM gpkg_contents;" — gpkg_contents is the layer registry. Ours came back with parcelas|features, vias|features, hillshade|tiles.
  • Query your data with SQL: sqlite3 parcelas.gpkg "SELECT fid, owner_name FROM parcelas WHERE status = 1;"
  • Dump attributes to CSV: sqlite3 -header -csv parcelas.gpkg "SELECT owner_name, area_m2 FROM parcelas;"
  • Find the geometry column: sqlite3 parcelas.gpkg "SELECT table_name, column_name, geometry_type_name, srs_id FROM gpkg_geometry_columns;"

When GDAL writes a GeoPackage it creates around fourteen tables. Alongside your data table you get gpkg_contents, gpkg_geometry_columns, gpkg_spatial_ref_sys (the projections, stored in the file — no more lost .prj), gpkg_extensions, gpkg_metadata, the gpkg_tile_matrix family for rasters, and a set of rtree_* tables that are the spatial index. It also installs about nineteen triggers to keep the R-tree and the feature counts honest as you insert and delete.

That last part matters: the spatial index is a real, live, queryable table. sqlite3 parcelas.gpkg "SELECT id, minx, miny FROM rtree_parcelas_geom WHERE minx > -38.55;" returns rows. Your bounding-box query is being answered by an R-tree inside the file, not by a scan.

What a Shapefile actually costs you

We took one small dataset — Brazilian survey parcels, accented names, a timestamp, a boolean, some NULLs — and pushed it through both formats. Here is what the Shapefile did to it.

It truncates your field names to 10 characters, silently

This is the famous one, and it is worse than people remember. The .dbf format caps field names at 10 characters. Convert and GDAL warns, then mangles:

  • identificacao_do_proprietario became identifica
  • area_construida_m2 became area_const
  • data_da_vistoria became data_da_vi
  • situacao_regular became situacao_r

And when two names collide, it invents new ones. We fed it observacao_tecnica_inicial and observacao_tecnica_final. Both truncate to observacao, so GDAL renamed the second to observac_1. Your schema is now positional trivia. Anyone reading that file later has to guess which one was "inicial".

This is permanent. It bit us mid-writing: we converted a Shapefile to GeoPackage and then ran a SQL filter on situacao_regular, and SQLite threw no such column: situacao_regular — because the round trip through .dbf had already destroyed the name. Converting back to GeoPackage does not restore it. The information is gone.

It has no real datetime

Convert a DateTime field to Shapefile and GDAL tells you plainly: Field data_da_vi created as String field, though DateTime requested. The .dbf date type holds a date and nothing else — no time, no timezone. So your timestamp gets demoted to a String(29) and every downstream tool now has to parse text and hope. In the GeoPackage the same field stays a genuine DateTime.

It cannot tell NULL from empty string

This is the one nobody demonstrates, so we did. We made two features: one with nota set to NULL, one with nota set to "".

Through the GeoPackage, SQL gives back exactly what went in — typeof() reports null for the first and text for the second. Through the Shapefile, both come back as (null). The .dbf pads unset fields with blanks and has no NULL sentinel, so "we never measured this" and "we measured it and it was empty" become the same value forever. If you are doing QA on field data, that distinction is the entire job.

The encoding mess is real, and GDAL's default will burn you

Here is the finding that surprised us most. We converted our UTF-8 source to a Shapefile with a plain ogr2ogr call, no flags. GDAL produced no .cpg file at all, and set the .dbf language-driver byte (offset 29) to 87 — which means Latin-1. It then transcoded our text: the raw bytes in the .dbf contain Jo\xe3o, the Latin-1 spelling of "João". The UTF-8 byte sequence is nowhere in the file.

GDAL reads its own output back correctly, because it checks that byte. But hand that Shapefile to anything that assumes UTF-8 — which in 2026 is most things — and "João Conceição" becomes mojibake. This is precisely why Portuguese and Spanish datasets arrive with João and María in them, and why every GIS team in Latin America has a folder of corrupted attribute tables.

The fix, if you must write a Shapefile: ogr2ogr -f "ESRI Shapefile" out_dir input.gpkg -lco ENCODING=UTF-8. We confirmed this writes a .cpg file containing the text UTF-8, sets the language byte to 0, and stores real UTF-8 bytes.

Reading a Latin-1 Shapefile that someone else made: ogr2ogr -f GPKG out.gpkg in.shp --config SHAPE_ENCODING ISO-8859-1. We ran it against our own mangled file and the accents came back intact.

A GeoPackage has no such problem. SQLite stores text as UTF-8. There is no byte to get wrong.

It is a pile of files, and one of them will go missing

Our conversion produced four files — .shp, .shx, .dbf, .prj — and that's the minimum. Add .cpg, .sbn, .sbx, .qix, .qmd in the wild. Lose the .prj and your layer has no projection. Lose the .dbf and it has no attributes. Email "the shapefile" to a colleague and you have emailed them nothing.

A GeoPackage is one file. You cannot lose half of it.

One geometry type per file

A Shapefile holds points, or lines, or polygons. Not two of them. A road network and its junction nodes are two files, two layers, two of everything.

We put a point layer, a line layer and a raster into a single GeoPackage and ogrinfo listed them cleanly as 1: parcelas (Point) and 2: vias (Line String), with gpkg_contents also carrying hillshade|tiles. One file, three layers, mixed types.

The 2 GB ceiling — and GDAL's dangerous default

The Shapefile spec stores file length as a count of 16-bit words in a signed 32-bit integer, which is where the well-known 2 GB practical limit comes from (this part is spec, not our terminal).

What we did verify is more interesting. GDAL's Shapefile driver exposes a creation option literally named 2GB_LIMIT, and its default is NO. Read that again: by default, GDAL will happily write you a .shp or .dbf larger than 2 GB — a file that is out of spec and that other software will refuse or misread. The format's limit doesn't protect you; it just waits.

GeoPackage inherits SQLite's limits instead, which top out in the terabytes. In practice you will run out of patience long before you run out of format.

What Shapefile does not break

Credibility cuts both ways, so: the "Shapefile has no booleans" claim you'll read everywhere is wrong. The .dbf format has a logical field type, and we watched a boolean survive the round trip — it came back as Integer(Boolean) with width 1, values 1 and 0, correct. Don't migrate for that reason. Migrate for the four above, which are real.

Rasters live in there too

Almost every GeoPackage article treats it as a vector format. It isn't. gpkg_contents.data_type can be features or tiles, and the tiles are a real pyramid.

We pushed a raster into the same file that already held two vector layers, with gdal_translate -of GPKG raster.png parcelas.gpkg -co APPEND_SUBDATASET=YES -co RASTER_TABLE=hillshade -a_srs EPSG:4326. Afterwards gdalinfo parcelas.gpkg reports Driver: GPKG/GeoPackage and Size is 64, 64, while ogrinfo on the same file still lists the vector layers.

The tiles are stored as blobs, one row per tile, keyed by zoom level and column and row. We ran SELECT zoom_level, tile_column, tile_row, length(tile_data) FROM hillshade; and got 0|0|0|338 bytes, with the blob's magic bytes reading 504E47 — hex for PNG. It is a tile cache in a table. TILE_FORMAT lets you choose PNG, JPEG or WEBP.

So a single .gpkg can carry your basemap imagery, your survey points and your parcel polygons, and you hand over one file.

How to open a .gpkg

  • QGIS: drag the file onto the canvas. If it has several layers QGIS asks which ones you want. This is the path of least resistance and it is genuinely fine.
  • List the layers from the terminal: ogrinfo -so parcelas.gpkg prints every layer and its geometry type without loading the data.
  • Inspect one layer's schema: ogrinfo -so parcelas.gpkg parcelas gives you field names, types and the feature count.
  • Plain SQLite: sqlite3 parcelas.gpkg ".tables" works. So does any SQLite GUI — DB Browser for SQLite, DBeaver, DataGrip. The geometry column is a blob with a small GeoPackage header wrapped around standard WKB.
  • Python: geopandas.read_file("parcelas.gpkg", layer="parcelas") is the one-liner. Under the hood it's fiona/pyogrio, and both accept a layer= argument — you'll need it, because a GeoPackage with multiple layers won't guess for you.
  • PostGIS: ogr2ogr -f PostgreSQL PG:"dbname=gis" parcelas.gpkg loads every layer straight in.

Converting, with commands that actually run

Every command in this section was executed against real files before publication.

  • Shapefile to GeoPackage: ogr2ogr -f GPKG output.gpkg input.shp
  • Shapefile to GeoPackage when the source is Latin-1 (assume it is, unless there's a .cpg saying otherwise): ogr2ogr -f GPKG output.gpkg input.shp --config SHAPE_ENCODING ISO-8859-1
  • GeoPackage to Shapefile, without wrecking the accents: ogr2ogr -f "ESRI Shapefile" out_dir input.gpkg -lco ENCODING=UTF-8
  • A whole folder of Shapefiles into one GeoPackage — run this once per file, and they accumulate as layers: ogr2ogr -f GPKG combined.gpkg roads.shp -nln roads then ogr2ogr -f GPKG -update -append combined.gpkg parcels.shp -nln parcels
  • Explode a multi-layer GeoPackage back into a directory of Shapefiles: ogr2ogr -f "ESRI Shapefile" out_dir input.gpkg — GDAL writes one Shapefile per layer, and truncates every field name over 10 characters on the way out.
  • Reproject while converting: ogr2ogr -f GPKG out.gpkg in.gpkg -t_srs EPSG:3857 -nln parcels_3857
  • Filter with SQL while converting: ogr2ogr -f GPKG regulars.gpkg source.gpkg -sql "SELECT * FROM parcels WHERE status = 1" -nln regulars
  • Fix the mixed single/multi geometry error that QGIS and PostGIS throw at you: ogr2ogr -f GPKG out.gpkg in.shp -nlt PROMOTE_TO_MULTI
  • GeoPackage to GeoJSON for the web — always reproject to 4326, the GeoJSON spec requires it: ogr2ogr -f GeoJSON web.geojson input.gpkg parcels -t_srs EPSG:4326
  • GeoPackage to FlatGeobuf: ogr2ogr -f FlatGeobuf out.fgb input.gpkg parcels

The -nln flag names the output layer. Skip it and you inherit whatever the source was called, which is how people end up with a production layer named OGRGeoJSON.

Where GeoPackage is the wrong answer

If we only told you the good parts you should stop believing the rest of the page.

Do not serve it to a browser. A GeoPackage is a database file. To read one feature a client must fetch and parse database pages. There is no streaming story worth having over HTTP. For web delivery use GeoJSON for small layers, vector tiles or PMTiles for big ones, or FlatGeobuf if you want random access with HTTP range requests. Publishing a 400 MB .gpkg and asking a browser to download it before the first pixel renders is a self-inflicted wound.

Do not use it as a multi-user write store. SQLite takes a write lock over the whole database. One writer at a time, full stop. Two field crews syncing into the same .gpkg on a network share will corrupt it or block each other — and SQLite over a network filesystem like NFS or SMB is explicitly a bad idea, because the locking primitives it depends on are unreliable there. If you have concurrent writers, you want PostGIS. This is not a knock on the format; it is a database file, and it behaves like one.

Do not assume every tool speaks it. Plenty of legacy municipal and utility software still only imports .shp. Some engineering and surveying packages, some old ArcGIS installs, and more than one government portal will demand a Shapefile from you. When that happens, export a Shapefile, accept the truncation, and keep the GeoPackage as your source of truth.

Do not reach for it for a five-feature scratch layer. If you're passing three points to a colleague, GeoJSON is human-readable, diffable in git, and requires no tooling to sanity-check. Not everything needs a database.

Shapefile vs GeoPackage vs GeoJSON vs FlatGeobuf

No table — just pick the one that matches your sentence.

Reach for GeoPackage when the data is the deliverable, or the archive, or the thing you hand to a client. Many layers, mixed geometry types, rasters alongside vectors, real types, real NULLs, honest field names, one file. It is the correct default for desktop GIS and for field data. It's the answer to "here is the dataset".

Reach for GeoJSON when a browser or an API is going to read it, the layer is small (say, under a few MB), or a human needs to read the file. It is text, it is universal, it diffs in git. It is also verbose, has no index, and is bound to EPSG:4326 by spec. It is the answer to "send this to the frontend".

Reach for FlatGeobuf when you have a large vector dataset that must be read over HTTP without a tile server. It is a binary, indexed, streamable format, and a client can pull just the features in a bounding box using HTTP range requests. It is the answer to "big layer, no backend".

Reach for vector tiles or PMTiles when users will pan and zoom across a large dataset in a web map. Pre-rendered, pyramided, cached. It is the answer to "make the map fast".

Reach for Shapefile when someone forces you to. That is the entire list. It is a 1990s format with a 10-character schema limit and an encoding byte from another era. It remains the most interoperable GIS format on the planet, which is why it will outlive all of us.

And if there are concurrent writers, none of these — use PostGIS.

For the measured version of the Shapefile/GeoPackage/GeoJSON comparison — sizes, field-name limits, encoding, CRS and load time on one real dataset — read Shapefile vs GeoPackage vs GeoJSON: which to use.

Gotchas that will actually bite you

The -wal and -shm sidecars. SQLite in WAL mode writes two extra files next to your database. We opened a GeoPackage, started writing, and confirmed the directory then held big.gpkg, big.gpkg-wal and big.gpkg-shm. After a clean close, both vanished. But if a process crashes — or if you copy the .gpkg while something has it open — you can capture a file whose most recent commits live in a -wal you left behind. When you back up or send a GeoPackage, make sure nothing has it open, or copy the sidecars too. The single-file promise only holds when the file is closed.

The file grows and never shrinks. SQLite marks deleted pages as free and reuses them; it does not hand the space back to the filesystem. We inserted bulk data into a GeoPackage until it hit 43,335,680 bytes, ran DROP TABLE, and the file was still 43,335,680 bytes — with 10,554 free pages sitting inside it. Then we ran sqlite3 file.gpkg "VACUUM;" and it collapsed to 106,496 bytes. If you have been editing a GeoPackage in QGIS and it has quietly become enormous, that's why. VACUUM is the answer, and it is safe.

Old ArcGIS chokes on GDAL's GeoPackages. GDAL 3.13 writes GeoPackage 1.4 by default — we read user_version as 10400 and the application_id as the ASCII string GPKG. Older Esri products expect the 1.0/1.1 era, whose application_id is GP10 and whose user_version is 0. If ArcMap or an older ArcGIS Pro refuses your file, regenerate it with ogr2ogr -f GPKG out.gpkg in.gpkg -dsco VERSION=1.0 — we ran exactly that and confirmed the application_id flips to GP10 and user_version drops to 0. GDAL accepts 1.0, 1.1, 1.2, 1.3, 1.4 and AUTO.

Layer names are table names. The layer name becomes a SQL table name, so a layer called 2024 parcels (final) is a quoting problem forever. Use lowercase, underscores, no spaces, no leading digits. Set it explicitly with -nln.

The spatial index is not guaranteed. GDAL creates one by default — SPATIAL_INDEX defaults to YES, and we counted 22 rtree_* objects in a normal two-layer file. Pass -lco SPATIAL_INDEX=NO and you get zero. We checked. Files produced by other tools, or by someone optimising an import, may have no index at all, and then every spatial query is a full scan. Check with sqlite3 file.gpkg "SELECT name FROM sqlite_master WHERE name LIKE 'rtree%';" — if it's empty, that's your slow map.

Don't hand-edit the geometry blobs. The geometry column is standard WKB wrapped in a small GeoPackage binary header. Write to it with GDAL, QGIS or a spatial SQLite extension. Do not craft the bytes yourself unless you enjoy debugging invalid geometries.

So: should you migrate?

Yes, for anything you own — with your eyes open.

Keep the GeoPackage as the source of truth. Export a Shapefile when a client or a portal demands one, and treat that export as a lossy render, the way you'd treat a JPEG of a layered design file. Never round-trip through Shapefile and expect your schema back, because the field names do not survive the trip.

If your data is going to a browser, it should not have been a GeoPackage in the first place. Convert to GeoJSON, FlatGeobuf, or tiles.

Where Geodocs fits

We build Geodocs, a platform for field data and GIS teams, and we wrote this guide because we deal with the format's edges every day. Our users upload GeoPackages, Shapefiles, KML and GeoJSON, and we handle exactly the problems above so they don't have to — detecting the .dbf encoding instead of guessing at it, keeping NULL distinct from empty, and not truncating anyone's field names to ten characters.

If you'd rather your team stopped emailing each other four-file Shapefiles and hoping the .prj made it, that's what we do.

Working the other direction? Our complete guide to the Shapefile format covers the format you're migrating away from in the same detail. And if you just need to look at a file right now, we have a free browser-based Shapefile viewer that needs no install — try the free Shapefile viewer yourself, right in your browser.

Spotted an error, or a gotcha we missed? Tell us. We re-test this page rather than copy-pasting it.

Last verified: 14 July 2026.

Related Articles

Navegação