Fixing COG Validation Errors in GDAL

You exported a GeoTIFF you believed was cloud-optimized, ran GDAL’s validate_cloud_optimized_geotiff.py (or rio cogeo validate), and it printed <file> is NOT a valid cloud optimized GeoTIFF followed by a list of concrete faults: The file is not tiled, The file does not have overviews, The offset of the main IFD should be after the offset of the overview IFDs, or Overview block size mismatch. The raster still opens in QGIS and draws correctly, which is exactly why this is confusing — the file is a valid GeoTIFF, it is just not laid out the way a range-request client needs. This page maps each error string to the byte-level cause and gives the minimal re-emit that fixes it deterministically.

Why GDAL rejects a GeoTIFF as “not a valid COG”

A COG is a GeoTIFF with three extra structural guarantees, and the validator checks each one independently. First, the pixels must be stored in internal tiles (square blocks), not the horizontal strips a default GeoTIFF uses — strips force a client to read a full image-width band to sample any tile. Second, the file must embed decimated overviews, a downsampled pyramid, so a zoomed-out request does not read full-resolution pixels. Third — the subtle one — the IFD ordering must place the overview image file directories and their tile offsets ahead of the main full-resolution image in the byte stream, so the very first HTTP range request lands on the header and pyramid index. A file can satisfy two of these and fail the third; the validator reports exactly which.

The reason a normal write fails ordering is mechanical: GDAL’s classic GTiff driver writes the main image first and appends overviews afterward (or builds them in a sidecar .ovr), so the offsets come out backwards for streaming even when tiling and overviews are both present. The dedicated COG driver exists precisely to reorganize the whole file on close so the offsets are correct. Getting this right is the export stage of the DEM/DSM raster export pipeline, and the full authoring path is covered in exporting Cloud-Optimized GeoTIFFs with Rasterio.

Invalid versus valid COG byte layout Two horizontal byte-layout strips compared. The top strip, labelled invalid, shows a header followed immediately by full-resolution image tiles and then overviews appended at the end, so a client must seek to the end for the pyramid. The bottom strip, labelled valid COG, shows a header, then the overview image file directories and overview tiles, then the full-resolution image tiles, so the first range request reaches the pyramid index. An arrow labelled re-emit via COG driver connects the invalid strip to the valid strip. Invalid: overviews appended at end header full-resolution image tiles overviews (seek to end) re-emit via COG driver Valid COG: pyramid index first header overview IFDs + overview tiles full-resolution image tiles The four checks a COG validator runs, in order Four sequential validator checks with the message each emits when it fails. First, the file must be tiled rather than striped, otherwise the validator reports that the image is not tiled. Second, overviews must exist, otherwise it reports the file has no overviews. Third, the image file directories must appear before the image data, otherwise it reports that the header is not at the beginning. Fourth, overviews must themselves be tiled and ordered from smallest to largest. Each check names the creation option that satisfies it. 1 · is the image tiled? fails as "The file is greater than 512xH or Wx512, but is not tiled" TILED=YES, BLOCKSIZE=512 2 · do overviews exist? fails as "The file has no overviews" build_overviews([2,4,8,16]) 3 · are the headers at the front? fails as "The offset of the main IFD should be ..." re-emit through driver="COG" 4 · are overviews tiled and ordered? fails as "Overview of index N is not tiled" or an ordering complaint GDAL_TIFF_OVR_BLOCKSIZE=512

Figure 1 — The validator’s messages map one-to-one onto four structural properties. Reading them as a checklist rather than as errors turns the fix into a lookup.

Minimal reproducible solution

Almost every validation error is fixed by the same move: re-emit the raster through GDAL’s COG driver, which enforces tiling, builds overviews, and fixes IFD ordering in one pass. The routine below runs the validator, and on failure re-emits with the correct options and re-validates. It is intentionally small so the fix is auditable.

from rio_cogeo.cogeo import cog_translate, cog_validate
from rio_cogeo.profiles import cog_profiles

def repair_cog(src_path: str, dst_path: str, *, is_float: bool = False) -> None:
    """Validate a raster and, if it fails, re-emit a correct COG."""
    valid, errors, warnings = cog_validate(src_path)   # structural check
    if valid:
        print(f"already a valid COG: warnings={warnings}")
        return

    print(f"invalid COG, re-emitting. errors={errors}")
    # 'deflate' base profile: tiled 512, DEFLATE, correct IFD ordering.
    profile = cog_profiles.get("deflate")
    profile.update(blockxsize=512, blockysize=512)
    # predictor 3 for float DEMs, 2 for integer rasters — this is a creation option.
    creation = {"predictor": 3 if is_float else 2}

    cog_translate(
        src_path,
        dst_path,
        profile,
        overview_resampling="average",   # 'nearest' for categorical rasters
        **creation,
    )
    ok, errs, warns = cog_validate(dst_path)            # re-validate the output
    assert ok, f"still invalid after re-emit: {errs}"
    print(f"repaired -> {dst_path}, warnings={warns}")

cog_translate streams the source in windows, so it never loads the whole raster into RAM, and the cog_profiles.get("deflate") base already sets tiled=True with a 512 block — the two conditions that fix the not tiled and block size errors. Passing overview_resampling is what fixes the overviews missing error, and writing through this driver at all is what fixes the IFD ordering error. If you would rather drive this from the command line, gdal_translate -of COG -co COMPRESS=DEFLATE -co PREDICTOR=2 in.tif out.tif produces the identical layout.

Why a windowed write then needs a second pass Two file layouts compared. The first is produced by writing windows to a plain GeoTIFF: the pixel data comes first and the image file directory is appended at the end once the sizes are known, which is valid TIFF and invalid COG. The second is produced by re-emitting that file through the COG driver: the driver knows all the sizes up front, so it writes the directories first and the tiles after, and adds the overviews in ascending order. An arrow between them is labelled as a full rewrite, with a note that it costs one extra read and write of the whole raster and cannot be avoided. after a windowed write — valid TIFF, invalid COG tile data, written window by window IFD (at the end) re-emit through driver="COG" — a full rewrite after re-emission — valid COG IFDs first overviews full-resolution tiles The rewrite costs one extra read and write of the raster and cannot be skipped — a directory cannot be moved in place. Budget for it: on a large orthomosaic the export stage is effectively two passes, not one.

Figure 2 — Windowed writing and COG ordering are structurally incompatible: the directory cannot be written first until every tile offset is known. That is why the two-pass export is a design constraint rather than an oversight.

Edge-case matrix

The exact validator message tells you which guarantee failed. Map it to the cause and the fix.

Validator error Root cause Expected fix
The file is not tiled Written as strips (no tiled=True) Re-emit via COG driver; base profile sets 512 tiling
The file does not have overviews No embedded pyramid, or overviews in a sidecar .ovr Pass overview_resampling; COG driver embeds them internally
The main IFD should be after the overview IFDs GTiff wrote main image before overviews Re-emit via COG driver, which reorders IFDs on close
Overview block size mismatch Overviews tiled at a different block size than the main image Re-emit so overviews inherit the main 512 block size
Missing NoData / mask (warning) NoData not written or mask not internal Set nodata on export, or add an internal alpha/mask band
The block size is not a power of two Non-standard blockxsize (e.g. 500) Set blockxsize=blockysize=512

The block-size mismatch case is the sneaky one: a file can be tiled and have overviews and still fail because the overviews were built at 128 while the main image is 512, so a client cannot use one tiling scheme for both. Re-emitting from source resolution through the COG driver forces both to the same block size.

Verification snippet

After the repair, assert the three structural guarantees independently so a regression tells you which one broke, not just that validation failed.

import rasterio
from rio_cogeo.cogeo import cog_validate

def assert_valid_cog(path: str) -> None:
    with rasterio.open(path) as ds:
        assert ds.profile["tiled"] is True, "not tiled"
        assert ds.profile["blockxsize"] == 512, "unexpected block size"
        assert len(ds.overviews(1)) >= 3, f"too few overviews: {ds.overviews(1)}"
    ok, errors, _ = cog_validate(path)
    assert ok, f"structural validation failed: {errors}"
    print("COG verified:", ds.overviews(1))

Run this in CI as a hard gate. The overviews(1) list also confirms the pyramid depth — an empty list means the overviews missing error will recur, and fewer than three levels usually means the source was too small or the factors were truncated.

A last check worth adding to CI: validate over HTTP as well as on local disk. The structural rules exist to make range requests work, and a file can satisfy every local check while still being unusable remotely because the server does not honour range requests or strips the relevant headers. Serving the finished COG from a temporary local HTTP server and opening it through a virtual filesystem path exercises the same path a client will, and catches a class of deployment fault that no amount of local validation can see.

When to escalate

The re-emit above fixes structural (layout) faults. Escalate to the parent guide when the problem is not layout:

Keep the validator itself pinned alongside the rest of the toolchain. The COG specification has been revised since it was first published, and a file that a newer validator accepts can be rejected by an older one running in a client’s environment — or, more awkwardly, the reverse. Recording the validator version next to the pass result turns a disputed file into a two-minute comparison rather than an argument about whose tooling is correct.

Exporting Cloud-Optimized GeoTIFFs with Rasterio