Choosing Block Size and Overview Levels for COGs

Pick the block size from the dominant read, and add overviews until the smallest fits in a block or two:

rio cogeo create input.tif output.tif \
    --blocksize 512 \
    --overview-level 5 \
    --overview-resampling average \
    --cog-profile zstd

Those two numbers decide how many HTTP requests every read costs for the life of the file. This page belongs to understanding Cloud-Optimized GeoTIFF structure in Core Raster Fundamentals & STAC Mapping.


Block Size Is a Trade Between Requests and Waste

The same window against three block sizes A 256 pixel read window aligned with the grid reads exactly one 256 pixel block. The same window against 512 pixel blocks reads one block that is four times larger than needed. Against 1024 pixel blocks it reads sixteen times more data than it uses. Larger blocks mean fewer requests for big reads and more waste for small ones. A 256 px tile request against three layouts 256 blocks: read 1x 512 blocks: read 4x 1024 blocks: read 16x Green: the pixels actually wanted. Orange and red: bytes fetched and thrown away.

The opposite case — a large analytical read of, say, 4096 pixels square — flips the argument: with 256-pixel blocks it touches 256 blocks, and even with HTTP range coalescing that is many more requests than the 16 it needs with 1024-pixel blocks. There is no universally correct size, only a correct size for the reads the file will actually receive.


Environment & Setup

Package Version pin Used for
rio-cogeo >=5.0 Creating and validating COGs with chosen blocks and overviews
rasterio >=1.3.0 Inspecting block shapes and overview factors
numpy >=1.23 The request-count model
pip install "rio-cogeo>=5.0" "rasterio>=1.3.0" "numpy>=1.23"

Complete Working Example

import math

import rasterio
from rio_cogeo.cogeo import cog_translate
from rio_cogeo.profiles import cog_profiles


def overview_levels(width: int, height: int, block: int) -> int:
    """Levels needed until the smallest overview fits within two blocks."""
    levels, w, h = 0, width, height
    while max(w, h) > 2 * block:
        w, h = math.ceil(w / 2), math.ceil(h / 2)
        levels += 1
    return levels


def requests_for(read_px: int, block: int) -> int:
    """Blocks touched by an aligned square read of read_px pixels."""
    return math.ceil(read_px / block) ** 2


def make_cog(src_path: str, dst_path: str, *, use: str = "analysis",
             categorical: bool = False) -> dict:
    block = {"tiles": 256, "analysis": 512, "bulk": 1024}[use]
    with rasterio.open(src_path) as src:
        levels = overview_levels(src.width, src.height, block)

    profile = cog_profiles.get("zstd") | {"blockxsize": block, "blockysize": block}
    cog_translate(
        src_path, dst_path, profile,
        overview_level=levels,
        overview_resampling="mode" if categorical else "average",
        in_memory=False, quiet=True,
    )
    return {"block": block, "overview_levels": levels,
            "tile_read_requests": requests_for(256, block),
            "4k_window_requests": requests_for(4096, block)}


if __name__ == "__main__":
    print(make_cog("S2A_36NYF_B08.tif", "S2A_36NYF_B08_cog.tif", use="analysis"))

For a 10,980-pixel band with 512-pixel blocks, overview_levels returns five, giving a smallest level of 344 pixels — a single block. With 256-pixel blocks it returns six. Letting the size of the image and the block determine the depth, rather than hard-coding it, keeps every file in an archive equally efficient at its smallest zoom.


Overview Depth and What It Buys

Blocks read for a whole-scene preview With no overviews, a whole-scene preview of a 10,980 pixel band with 512 pixel blocks reads all 484 blocks. Each overview level quarters that: 121, 30, 8, 2 and finally one block at five levels. Beyond that point additional levels change nothing, because the smallest already fits in a single block. Whole-scene preview, 512 px blocks 484 121 30 8 2 1 none 1 2 3 4 5 Overview levels. Five levels costs a third more storage and makes every preview a single request.

The storage cost of a full pyramid is about a third of the base image, and the benefit is that every zoomed-out read — a thumbnail, a tile at low zoom, a whole-scene statistic computed from an overview — becomes one or two requests instead of hundreds. That trade is favourable in almost every case; the exception is a file that will only ever be read at full resolution in its entirety, where overviews are pure overhead.

Overview resampling is the other half of the decision and is covered in detail in adding internal overviews with the right resampling: average for continuous data, mode for categorical, never bilinear on classes.


Matching the Archive to Its Consumers

An archive is usually read by more than one kind of consumer, and the block size is best chosen from the one that reads most often. A product that will be browsed on a web map far more than it is analysed benefits from 256-pixel blocks that align with map tiles, as discussed in generating Web Mercator tile pyramids. A product feeding model inference in 512- or 1024-pixel windows benefits from blocks of the same size, so each window is exactly one or four blocks.

When the consumers genuinely conflict, 512 is the compromise that serves both acceptably: a map tile reads one block four times its size, and a 2048-pixel analysis window reads sixteen blocks. Neither is optimal, both are fine, and the archive stays uniform. Mixing block sizes within one archive is worth avoiding, since tools and caches tuned for one size behave inconsistently on another.


Verification

Structure as intended After creating the file, confirm the block shape reported for band one matches the chosen size, the overview factors are successive powers of two, and the smallest overview fits within two blocks. rio-cogeo's validator then confirms the byte layout is cloud-optimized. Four properties to assert block shape as chosen factors 2, 4, 8, 16, … smallest level within two blocks valid COG cog_validate passes A file can be a valid COG with the wrong block size; validation alone does not check intent.
import rasterio
from rio_cogeo.cogeo import cog_validate

with rasterio.open("S2A_36NYF_B08_cog.tif") as src:
    assert src.block_shapes[0] == (512, 512), src.block_shapes[0]
    factors = src.overviews(1)
    assert factors == [2 ** i for i in range(1, len(factors) + 1)], factors
    smallest = max(src.width, src.height) / factors[-1]
    assert smallest <= 1024, f"smallest overview {smallest:.0f} px — add a level"

valid, errors, warnings = cog_validate("S2A_36NYF_B08_cog.tif")
assert valid, errors

Validation confirms the layout is cloud-optimized; it does not confirm the block size and overview depth match your intent. Asserting both explicitly is what catches a conversion step that silently fell back to defaults. The CI integration of these checks is covered in validating COG structure in CI.


Common Errors

Tile serving is slow despite a valid COG

Blocks are 1024 pixels, so each 256-pixel tile over-reads sixteen-fold. Rewrite with 256 or 512.

Low-zoom reads touch hundreds of blocks

Too few overview levels. Add levels until the smallest fits in a block or two.

The file grew far more than a third

Overviews were stored uncompressed or with a weaker compressor than the base. Use the same profile for all levels.

Block shape reports as strips, not tiles

The file was written untiled and never converted. Create it with rio cogeo create rather than a plain copy.


Frequently Asked Questions

Q: Is 512 always the right block size? It is the right default for mixed analytical use. For a file served mostly as web tiles, 256 aligns each block with a tile and halves the over-read; for files read almost entirely in large windows or whole, 1024 reduces request count.

Q: How many overview levels should a COG have? Enough that the smallest level fits in one or two internal tiles — for a 10,980 pixel Sentinel-2 band with 512 pixel blocks, five levels. Fewer leaves low-zoom reads touching many tiles; more adds nothing useful.

Q: How much do overviews add to file size? About a third for a power-of-two pyramid, because each level is a quarter of the one above and the series one quarter plus one sixteenth and so on sums to one third. Compression changes the absolute size but not that proportion.

Q: Can block size be changed without rewriting the file? No. Block layout is fixed at write time; changing it means reading and rewriting every byte. Choosing it correctly at creation is far cheaper than a later migration across an archive.