The short answer
A Cloud Optimized GeoTIFF (COG) is an ordinary GeoTIFF organised internally so that a client can read just the part it needs directly from object storage, without downloading the whole file. Three things make it work: the image is stored as square internal tiles instead of strips, it carries embedded overviews (a pre-computed resolution pyramid), and its header and tile-index are laid out at the front so a reader can discover the layout in one small request. A COG opens in any GeoTIFF-capable software as a normal raster; the optimisation is invisible until you read it over HTTP.
The reason this matters: a 5 GB DEM on S3 served as a COG can be panned and zoomed in a browser while transferring only a few hundred KB at a time, because the client uses HTTP range requests to fetch exactly the tiles and overview level in view.
What "cloud optimized" actually means
A classic GeoTIFF is often written in horizontal strips and with no internal overviews. To show a small region, software historically had to read large parts of the file; to read it from the cloud, a client had to download the whole thing first. COG removes both constraints by imposing three structural rules.
1. Internal tiling
The raster is divided into square blocks, conventionally 512×512 pixels. A reader that wants a 1 km window fetches only the tiles intersecting that window. With strips, the same window could require reading every strip that crosses it — far more data. Tiling turns "read a region" into "read a handful of independent blocks."
2. Internal overviews (the pyramid)
A COG embeds downsampled copies of the image at successively coarser resolutions — overview levels at 1/2, 1/4, 1/8, and so on. When a viewer is zoomed out, it reads a small overview instead of resampling the full-resolution data. Without overviews, every zoom-out forces the client to pull and downsample the entire image, which is exactly the cost COG exists to avoid.
3. Header-first layout
The TIFF format stores its directory of offsets (the IFD — Image File Directory) and tile-offset tables wherever the writer puts them. A COG mandates that this metadata sits at the front of the file, and that tiles are ordered predictably. A client then issues one small range request (typically the first ~16 KB), learns where every tile lives, and from then on requests only the byte ranges it needs. Without header-first ordering, the client cannot plan its reads and the format loses its point.
These rules together enable the real payoff: HTTP range requests (Range: headers, supported by every major object store). The file lives once on storage and serves many clients efficiently, with no tile server in front of it.
Creating a COG with GDAL
Since GDAL 3.1 there is a dedicated COG driver that handles tiling, overviews, and layout in one step:
# Recommended: the COG driver does everything correctly
gdal_translate input.tif output_cog.tif \
-of COG \
-co COMPRESS=DEFLATE \
-co BLOCKSIZE=512 \
-co OVERVIEWS=AUTO
For elevation or scientific floating-point data, DEFLATE (lossless) is the safe default. For 8-bit imagery where some loss is acceptable, LZW or even JPEG compression yields far smaller files. To keep predictor benefits on continuous data, add -co PREDICTOR=2 (integer) or PREDICTOR=3 (floating point), which compresses smoothly varying rasters like DEMs much better.
If you are stuck on older GDAL, build the COG manually:
# 1. add internal overviews
gdaladdo -r average input.tif 2 4 8 16
# 2. rewrite as a tiled GeoTIFF with header-first layout
gdal_translate input.tif output_cog.tif \
-co TILED=YES -co BLOCKXSIZE=512 -co BLOCKYSIZE=512 \
-co COPY_SRC_OVERVIEWS=YES -co COMPRESS=DEFLATE
The COPY_SRC_OVERVIEWS=YES step is what folds the external .ovr pyramid into the file; omitting it leaves you with a tiled GeoTIFF that still lacks internal overviews.
Validating a COG
Producing a file is not the same as producing a valid COG. GDAL ships a validator:
python validate_cloud_optimized_geotiff.py output_cog.tif
It checks the three rules: internal tiling, presence and ordering of overviews, and front-loaded IFD/header. A quick sanity check with gdalinfo output_cog.tif should show LAYOUT=IFDS_BEFORE_DATA in the structural metadata (with the COG driver) and list the overview levels under each band. If gdalinfo shows no overviews, or block size is 1 in one dimension (a strip), the file is not optimized regardless of its extension.
A worked example: serving a national DEM
Suppose you have a 30 m national DEM, ~6 GB, and want it pannable in a MapLibre or Leaflet viewer.
- Reproject and tile into a COG in one pass:
gdalwarp -t_srs EPSG:3857 -r bilinear srtm_national.tif tmp.tif gdal_translate tmp.tif dem_cog.tif -of COG \ -co COMPRESS=DEFLATE -co PREDICTOR=3 -co BLOCKSIZE=512 - Validate with the script above.
- Upload to object storage (S3, GCS, or any HTTP server that honours range requests).
- Point a COG-aware client at it — a
titilerdynamic tile endpoint, QGIS via/vsicurl/, or a browser library like georaster-layer-for-leaflet.
QGIS can open it remotely with no download:
gdalinfo /vsicurl/https://example-bucket.s3.amazonaws.com/dem_cog.tif
The DEM now serves at any zoom from a single static file. Compare that with the alternative — running a tile server, or shipping the whole 6 GB to every user — and the operational saving is obvious.
Common mistakes and why they happen
- Renaming a strip GeoTIFF to imply it is a COG. The extension does not matter; without internal tiles and overviews it still forces whole-file reads. Always validate.
- Forgetting overviews. A tiled-but-pyramid-less file works at full zoom and crawls when zoomed out, because each zoom-out resamples the full raster.
gdaladdoor the COG driver fixes this. - JPEG-compressing elevation data. Lossy compression on a DEM corrupts the very values you measure from. Reserve JPEG for visual imagery; use DEFLATE/LZW with a predictor for continuous scientific data.
- Serving from storage that ignores range requests. COG depends on
Range:support. A misconfigured server or CDN that returns the whole file defeats the format entirely. - Tiny block sizes. Very small tiles inflate the tile index and multiply the number of requests; 256 or 512 is the practical sweet spot.
Validation and QA before delivery
Before shipping a COG, run the validator, confirm overviews exist with gdalinfo, and test an actual remote read with /vsicurl/ to prove range requests work end to end. Check the CRS and nodata value survived the conversion, and verify a known elevation or pixel value at a control point matches the source — compression settings and resampling can subtly alter values if misconfigured. Record the compression, predictor, and block size in the dataset metadata so the file is reproducible.
Bathyl perspective
We default to COG for any raster that has to be shared or shown on the web — DEMs, hillshades, satellite composites — because it removes the tile server from the equation: one static file on storage serves desktop GIS, web viewers, and downstream pipelines alike. The discipline is to always validate and to record the compression choices, so the file another team receives is provably optimized rather than just plausibly named.
Related reading
- Shapefile vs GeoPackage vs GeoJSON
- CSV Coordinates to GIS Layers
- Why Your GIS Layers Do Not Line Up
- Spatial data products