Overview Pyramids and Internal Tiling for COG

For a Cloud-Optimized GeoTIFF, use 256-pixel internal tiles for interactive web maps and 512-pixel tiles for analytics, and build power-of-two overviews until the coarsest level fits inside roughly one tile. These two choices — block geometry and pyramid depth — decide how much data a client must move to satisfy a zoom or pan, and they matter far more than most codec tweaks. Tiling controls how a single viewport maps to bounded byte ranges; overviews control how a zoom-out avoids reading full-resolution pixels. Get both right and a pan touches a handful of adjacent tiles while a zoom-out touches one small overview tile. This page is the layout companion to Cloud-Optimized GeoTIFF for Raster Workloads; it works through the sizing decisions, gives you gdal and rasterio snippets, and covers where the defaults break.

Quick-Reference Table

Layout choice Recommended value Effect on range reads Primary use case
Block size (web) 256 × 256 Small, viewport-aligned fetches; more tiles, larger directory Slippy maps, WMTS, browser tile clients
Block size (analytics) 512 × 512 Fewer, larger fetches; smaller offset directory Windowed reads, batch xarray/rasterio jobs
Overview factors 2, 4, 8, 16, 32 (to ~1 tile) Zoom-out reads one small overview tile, not full-res Any zoomable tile service
Resampling (continuous) average / gauss Smooth downsampled imagery DEMs, reflectance, orthophotos
Resampling (categorical) nearest / mode Preserves discrete class codes Land-cover, classifications, palettes

How Tiling and Overviews Shape a Read

An internal tile is the smallest unit the reader can fetch. When a map client requests a viewport, GDAL computes which tiles the viewport overlaps, looks up each tile’s byte offset in the cached header, and issues one HTTP range request per tile. A 256-pixel block means a typical 256-pixel display tile aligns to one or two internal tiles, so panning one tile east fetches one new block. A 512-pixel block covers four times the area per fetch: fewer, larger requests, and a smaller tile-offset directory in the header because there are a quarter as many tiles to index. The cost is that a small viewport may pull a 512 block when it only needed a corner of it. This is the same read-amplification-versus-request-count tension that governs row group sizing for Parquet on the vector side, transposed to two dimensions.

Overviews handle the other axis of navigation: zoom. Without a pyramid, drawing a country-scale view from a 10-metre scene means reading every full-resolution tile in frame and downsampling on the fly — hundreds of range requests and a large decode bill for a handful of screen pixels. An overview is a pre-computed, decimated copy stored as its own tiled directory inside the file. When the requested resolution is coarser than full-res, GDAL selects the nearest overview and reads its tiles instead. Because each overview is roughly a quarter the pixels of the one below it, a five-level pyramid adds only about a third to file size while turning every zoom-out into a small, bounded read. The overview level whose resolution is closest to the display resolution is chosen automatically, so the client always reads near the minimum data required.

Resampling method is where correctness enters. Downsampling continuous imagery with average or gauss produces smooth, representative coarse pixels. Applying the same average to a categorical raster — land-cover classes encoded as integers 1 through 12 — invents nonsense values like class 6.5 that correspond to nothing. Categorical and palette data must use nearest or mode so overview pixels remain valid class codes. Mismatched resampling is one of the most common and most silent COG defects, because the file validates and renders; it is only wrong.

Building the Pyramid

The snippet below writes a tiled COG with an explicitly chosen block size and a pyramid deep enough that the top level fits in one tile, selecting the resampling method from whether the raster is categorical.

python
# rasterio>=1.3, GDAL>=3.4
from __future__ import annotations

import math
from pathlib import Path

import rasterio
from rasterio.shutil import copy as rio_copy


def overview_factors(width: int, height: int, block: int) -> list[int]:
    """Power-of-two factors until the coarsest level fits within one tile."""
    longest = max(width, height)
    factors, f = [], 2
    while longest / f > block:
        factors.append(f)
        f *= 2
    return factors or [2]


def write_tiled_cog(
    src_path: str | Path,
    dst_path: str | Path,
    *,
    block: int = 512,
    categorical: bool = False,
) -> None:
    """Write a COG with explicit tiling and a full overview pyramid.

    Args:
        src_path: Source raster.
        dst_path: Destination COG path.
        block: Internal tile edge in pixels (256 for web, 512 for analytics).
        categorical: If True, use nearest resampling to preserve class codes.
    """
    resampling = "nearest" if categorical else "average"
    with rasterio.open(src_path) as src:
        factors = overview_factors(src.width, src.height, block)
        profile = src.profile.copy()
        profile.update(
            driver="COG",
            blocksize=block,
            overview_resampling=resampling,
            compress="ZSTD",
            NUM_THREADS="ALL_CPUS",
            BIGTIFF="IF_SAFER",
        )
        # The COG driver derives its own overview factors; we log the target
        # depth so operators can confirm the top level fits in one tile.
        print(f"target overview factors for {block}px tiles: {factors}")
        rio_copy(src, dst_path, **profile)

If you prefer the command line, the GDAL COG driver produces an equivalent file in one call, and gdaladdo can rebuild overviews on an existing tiled GeoTIFF:

bash
# GDAL >= 3.4 — one-shot COG with 512 tiles and ZSTD
gdal_translate scene.tif scene_cog.tif \
  -of COG -co BLOCKSIZE=512 -co COMPRESS=ZSTD -co OVERVIEW_RESAMPLING=AVERAGE

# Rebuild overviews on an existing internally tiled GeoTIFF
gdaladdo -r average scene_cog.tif 2 4 8 16 32

Validation

Confirm the block size and overview count before publishing. gdalinfo reports both, and a well-formed pyramid shows a descending stack of overview dimensions.

bash
gdalinfo scene_cog.tif | grep -E "Block=|Overviews"
# Expected, e.g.:
#   Band 1 Block=512x512 Type=UInt16 ...
#   Overviews: 8192x8192, 4096x4096, 2048x2048, 1024x1024, 512x512

The equivalent check in Python confirms the reader sees the overviews and reads a window without touching full resolution:

python
# rasterio>=1.3
import rasterio
from rasterio.enums import Resampling

with rasterio.open("scene_cog.tif") as src:
    print("block:", src.block_shapes[0])          # expect (512, 512)
    print("overview levels:", src.overviews(1))   # expect [2, 4, 8, 16, 32]
    # Read a decimated preview — GDAL serves it from an overview, not full-res
    preview = src.read(1, out_shape=(512, 512), resampling=Resampling.average)
    print("preview shape:", preview.shape)

Expect the overview list to descend by powers of two down to roughly one tile, and the decimated read to return in a few milliseconds because it pulls a single overview tile rather than the full array.

Edge Cases and Caveats

Very small or very large rasters. A scene under a few thousand pixels on its longest edge needs no overviews at all — the full image is already close to one tile — and forcing a deep pyramid just adds directory overhead. Conversely, imagery beyond ~4 GB crosses the classic TIFF offset limit; set BIGTIFF=IF_SAFER (or YES) so 64-bit offsets are used, or the tile-offset arrays overflow and the file corrupts.

Non-square or extreme aspect ratios. Strip-like rasters (a 100000×2000 coastline mosaic) waste space with square tiles because most blocks along the short axis are largely padding. Overviews still help zoom, but consider whether the data belongs in the mosaic-of-tiles pattern rather than one giant file, mirroring how upstream spatial partitioning breaks unwieldy extents into manageable pieces.

Block size versus codec interaction. Larger 512 tiles give each compressed block more context, so they usually compress slightly better than 256 tiles at the same codec — but they also raise the per-tile decode cost, which matters when a tile server decodes thousands per second. If decode latency dominates, keep tiles at 256 and lean on a fast codec; the ratio-versus-decode-speed side of this is covered in ZSTD vs DEFLATE for COG.

Frequently Asked Questions

Should I use 256 or 512 pixel blocks for a COG?

Use 256-pixel blocks when the file feeds a slippy web map, because the tile size matches the 256-pixel web mapping convention and keeps each range read small. Use 512-pixel blocks for analytics and batch windowed reads, where halving the tile count shrinks the offset directory and cuts per-request overhead. When one file must do both, 512 is the safer default because a client can still assemble 256-pixel display tiles from 512 blocks with little waste.

How many overview levels does a COG need?

Build power-of-two levels until the coarsest overview fits inside roughly one internal tile. A 16384-pixel-wide scene with 512 blocks needs levels 2, 4, 8, 16, and 32, at which point the top overview is about 512 pixels wide and a fully zoomed-out view reads a single tile. Stopping short leaves the smallest zooms decoding many full-resolution tiles.


← Back to Cloud-Optimized GeoTIFF for Raster Workloads