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

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.

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.

When to escalate

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

Exporting Cloud-Optimized GeoTIFFs with Rasterio