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, someone told you GeoPackage is "the modern replacement", and you want to know if that's true or just a fad.

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 conversions, opened the files with a plain SQLite client and read the bytes. When a claim comes from the OGC specification instead of our terminal, we say so. Everything here was verified on July 14, 2026.

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

The mental pivot: a GeoPackage is a SQLite database

That’s it, that’s all. Once the penny drops, everything else follows from there.

A .gpkg file is a normal SQLite database, with a standardized set of tables defined by the OGC GeoPackage specification. It’s not a new binary container. It’s not a zip. It’s SQLite, and every SQLite tool on the planet already knows how to 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 instead of just any database is a four-byte application_id at offset 68 that spells GPKG in ASCII, plus a user_version — we measured 10400, i.e. GeoPackage 1.4.

So this works, with no GIS software installed:

  • See what's inside: sqlite3 parcelas.gpkg "SELECT table_name, data_type FROM gpkg_contents;" — gpkg_contents is the layer registry. Ours returned 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 about fourteen tables. Along with your data table come gpkg_contents, gpkg_geometry_columns, gpkg_spatial_ref_sys (the projections, stored inside the file — never a lost .prj again), 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 feature counts honest as you insert and delete.

That last bit 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 a small dataset — Brazilian cadastral parcels, accented names, a timestamp, a boolean, some NULLs — and ran it through both formats. Here’s what the Shapefile did to it.

It silently truncates your field names to 10 characters

This is the famous one, and it’s worse than people remember. The .dbf format limits field names to 10 characters. Convert and GDAL warns, then mutilates:

  • 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 names. We gave 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. Whoever opens that file later will have to guess which one was the "initial" one.

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

It doesn't have real datetimes

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 stores a date and nothing else — no time, no zone. So your timestamp is downgraded to a String(29) and every tool downstream now has to parse text and hope. In the GeoPackage, the same field remains a real DateTime.

It can't distinguish NULL from empty string

This is the one nobody demonstrates, so we demonstrate it. We created two features: one with nota as NULL, another with nota equal to "".

Through the GeoPackage, the SQL returns 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 fills undefined fields with blank space and has no sentinel for NULL, so "we never measured this" and "we measured and it was empty" become the same value, forever. If you do field QA, that distinction is the whole job.

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

Here’s the discovery that surprised us the most. We converted our source in UTF-8 to Shapefile with a plain ogr2ogr call, no flags. GDAL did not produce any .cpg file, and set the DBF language driver byte (offset 29) to 87 — which means Latin-1. And then it 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 re-reads its own output correctly, because it checks that byte. But hand that Shapefile to anything that assumes UTF-8 — which in 2026 is almost everything — and "João Conceição" becomes mojibake. This is exactly 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 that this writes a .cpg file containing the text UTF-8, sets the language byte to 0 and writes actual 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 this against our own mangled file and the accents returned intact.

A GeoPackage does not have this problem. SQLite stores text as UTF-8. There is no stray byte to get wrong.

It’s a stack of files, and one of them will go missing

Our conversion produced four files — .shp, .shx, .dbf, .prj — and that’s the minimum. In the real world, add .cpg, .sbn, .sbx, .qix, .qmd. 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 sent nothing.

A GeoPackage is a single file. You can’t lose half of it.

One geometry type per file

A Shapefile stores points, or lines, or polygons. Not two of them. A road network and its intersection nodes are two files, two layers, two of each thing.

We put a point layer, a line layer and a raster inside 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 specification stores file size as a 16-bit word count in a signed 32-bit integer, and that’s where the famous practical 2 GB limit comes from (that part is spec, not our terminal).

What we actually verified is more interesting. GDAL’s Shapefile driver exposes a creation option literally called 2GB_LIMIT, and its default is NO. Read that again: by default, GDAL will happily write a .shp or .dbf larger than 2 GB — a file outside the spec, which other software will refuse or read incorrectly. The format’s limit does not protect you; it merely expects you to obey it.

GeoPackage inherits SQLite’s limits, which reach into the terabytes. In practice, your patience runs out long before the format does.

What a Shapefile does not break

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

Rasters live in there too

Almost every article on GeoPackage treats the format as if it were vector-only. 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 had two vector layers, with gdal_translate -of GPKG raster.png parcelas.gpkg -co APPEND_SUBDATASET=YES -co RASTER_TABLE=hillshade -a_srs EPSG:4326. After that, 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, 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 magic bytes showing 504E47 — hex for PNG. It’s a tile cache inside a table. TILE_FORMAT lets you pick PNG, JPEG or WEBP.

So: a single .gpkg can carry your basemap image, your survey points and your parcel polygons, and you deliver a single file.

How to open a .gpkg

  • QGIS: drag the file into the canvas. If it has multiple layers, QGIS asks which you want. It’s the path of least resistance and it’s genuinely good.
  • List layers from the terminal: ogrinfo -so parcelas.gpkg prints all layers and their geometry types without loading data.
  • Inspect a layer’s schema: ogrinfo -so parcelas.gpkg parcelas gives field names, types and feature count.
  • Plain SQLite: sqlite3 parcelas.gpkg ".tables" works. Any SQLite GUI too — DB Browser for SQLite, DBeaver, DataGrip. The geometry column is a blob with a small GeoPackage header wrapping standard WKB.
  • Python: geopandas.read_file("parcelas.gpkg", layer="parcelas") is the one-liner. Under the hood it’s fiona/pyogrio, and both accept the 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 all layers 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 in 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 destroying accents: ogr2ogr -f "ESRI Shapefile" out_dir input.gpkg -lco ENCODING=UTF-8
  • A whole folder of Shapefiles into one GeoPackage — run once per file, and they accumulate as layers: ogr2ogr -f GPKG combined.gpkg roads.shp -nln roads and 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 in the output path.
  • Reproject during conversion: ogr2ogr -f GPKG out.gpkg in.gpkg -t_srs EPSG:3857 -nln parcels_3857
  • Filter with SQL during conversion: ogr2ogr -f GPKG regulars.gpkg source.gpkg -sql "SELECT * FROM parcels WHERE status = 1" -nln regulars
  • Fix that single/multi geometry mix error 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, 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 called OGRGeoJSON.

Where GeoPackage is the wrong answer

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

Don't serve it to a browser. A GeoPackage is a database file. To read a feature, the client has to fetch and parse database pages. There is no decent streaming story over HTTP. For web delivery, use GeoJSON for small layers, vector tiles or PMTiles for large ones, or FlatGeobuf if you want random access via HTTP range requests. Publishing a 400 MB .gpkg and asking the browser to download it before the first pixel appears is shooting yourself in the foot.

Don’t use it as a multi-user writable store. SQLite takes a write lock on the entire database. One writer at a time, period. Two field teams syncing to the same .gpkg on a network share will corrupt the file or block each other — and SQLite on top of a network filesystem like NFS or SMB is explicitly a bad idea, because the locking primitives it relies on are not reliable there. If you have concurrent writers, what you want is PostGIS. This is not criticism of the format; it is a database file, and it behaves like one.

Don’t assume every tool speaks the format. A lot of municipal and utility legacy software still only imports .shp. Some engineering and surveying packages, some older 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.

Don’t reach for it for a draft layer with five features. If you’re passing three points to a colleague, GeoJSON is human-readable, diffs in git, and needs no tool to check if it’s right. Not everything needs a database.

Shapefile vs GeoPackage vs GeoJSON vs FlatGeobuf

No table — just pick what matches your sentence.

Use 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, a single file. It’s the right standard for desktop GIS and for field data. It’s the answer to "here’s the dataset."

Use GeoJSON when a browser or an API will read the file, the layer is small (say, a few MB at most), or a human needs to read the file. It’s text, universal, diffs in git. It’s also verbose, has no index, and is locked to EPSG:4326 by specification. It’s the answer to "send this to the frontend."

Use FlatGeobuf when you have a large vector dataset that needs to be read over HTTP without a tile server. It’s a binary, indexed, streamable format, and a client can fetch only features inside a bounding box using HTTP range requests. It’s the answer to "large layer, no backend."

Use vector tiles or PMTiles when users will pan and zoom over a large dataset on a web map. Pre-rendered, pyramided, cached. It’s the answer to "make the map fast."

Use Shapefile when someone makes you. That’s the whole list. It’s 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 exactly why it will outlive us all.

And if you have concurrent writers, none of these — use PostGIS.

Traps that will actually bite you

The satellite `-wal` and `-shm` files. SQLite in WAL mode writes two extra files alongside your DB. We opened a GeoPackage, started writing, and confirmed the directory then contained big.gpkg, big.gpkg-wal and big.gpkg-shm. After a clean close, the two disappeared. 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 left behind. When you back up or send a GeoPackage, ensure nothing has it open, or copy the satellite files too. The single-file promise only holds with the file closed.

The file grows and never shrinks. SQLite marks deleted pages as free and reuses them; it does not return space to the filesystem. We inserted bulk data into a GeoPackage until it hit 43,335,680 bytes, ran DROP TABLE, and the file stayed at 43,335,680 bytes — with 10,554 free pages sitting inside. Then we ran sqlite3 file.gpkg "VACUUM;" and it collapsed to 106,496 bytes. If you edit a GeoPackage in QGIS and it silently got huge, this is why. VACUUM is the answer, and it’s safe.

Old ArcGIS chokes on GDAL-produced GeoPackages. GDAL 3.13 writes GeoPackage 1.4 by default — we read user_version as 10400 and 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, generate it again with ogr2ogr -f GPKG out.gpkg in.gpkg -dsco VERSION=1.0 — we ran exactly that and confirmed application_id becomes GP10 and user_version falls 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 for the rest of its life. Use lowercase, underscores, no spaces, no leading digit. 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 have zero. We checked. Files produced by other tools, or by someone optimizing an import, might have no index at all, and then every spatial query turns into a full scan. Check with sqlite3 file.gpkg "SELECT name FROM sqlite_master WHERE name LIKE 'rtree%';" — if it returns empty, that’s your slow map.

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

So: should you migrate?

Yes, for anything that is yours — with eyes open.

Keep the GeoPackage as the source of truth. Export a Shapefile when a client or a portal requires it, and treat that export as a lossy render, the way you’d treat a JPEG of a layered design file. Never go back and forth through Shapefile expecting your schema to return, because field names do not survive the trip.

If your data is going to a browser, it shouldn’t be a GeoPackage in the first place. Convert to GeoJSON, FlatGeobuf or tiles.

Where Geodocs fits in

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

If you’d like your team to stop sending four-file Shapefiles by email hoping the .prj arrived too, that’s exactly what we do.

Working the other way? Our complete Shapefile format guide covers, at the same level of detail, the format you’re migrating from. And if you just need to peek at a file now, we have a free Shapefile viewer, right in the browser, no installation required.

Found an error, or a trap we missed? Tell us. This page is retested, not copy-pasted.

Last checked: July 14, 2026.

Related Articles

Navegação