Skip to content

Fetch COG tiles concurrently in HTTP path to mask RTT #1480

@brendancol

Description

@brendancol

Reason or Problem

_read_cog_http in xrspatial/geotiff/_reader.py walks tiles sequentially. Each tile is fetched with its own HTTP range request, and each request blocks on the previous one before the next is sent.

For a COG with N tiles read over a high-latency link, total wall time is bounded by N × RTT. A 100-tile COG over a 50 ms RTT link spends 5 seconds on round trips alone before any data flows.

Proposal

Fetch tile ranges concurrently with a small thread pool. Each tile still issues its own HTTP range request, but the requests overlap on the wire so RTT is hidden behind concurrency.

Design:

Add a helper on _HTTPSource (or alongside _read_cog_http) that takes a list of (offset, byte_count) ranges and returns the bytes for each, in input order:

def read_ranges(self, ranges: list[tuple[int, int]], max_workers: int = 8) -> list[bytes]:
    ...

Implementation uses concurrent.futures.ThreadPoolExecutor. urllib3.PoolManager is already used by _HTTPSource and is thread-safe, so the existing pool handles connection reuse across worker threads.

In _read_cog_http, replace the sequential loop:

for tr in range(tiles_down):
    for tc in range(tiles_across):
        ...
        tile_data = source.read_range(off, bc)
        # decode + place tile

with two passes: collect all (offset, byte_count, placement) entries first, fetch them with read_ranges, then iterate the returned bytes and decode/place each tile.

_HTTPSource.read_range stays unchanged, so no other call sites are affected.

Usage:

No API change. Existing read_to_array(http_url) calls benefit automatically.

Value:

For 100 tiles × 50 ms RTT with 8 workers, total fetch drops from ~5 s to ~600 ms. Local file reads are unaffected; this only touches _read_cog_http.

Stakeholders and Impacts

Touches _HTTPSource and _read_cog_http. No public API change.

Drawbacks

  • Extra worker threads (small constant).
  • Servers without HTTP/1.1 keep-alive may see more connection setup. The urllib3 PoolManager already in use mitigates that.

Alternatives

  • Multipart Range requests (bytes=0-N1,N2-N3,...). Cuts request count further but server support is uneven; some CDNs return 200 with the full body, others reject with 400/416. Could be layered on top of the threadpool fallback later.
  • Adjacent-range coalescing (merge ranges within ~8 KB). Useful when tile offsets are nearly contiguous. Also a future addition.

Unresolved Questions

  • Worker pool size. 8 is a reasonable default; could be tunable via env var or kwarg if anyone needs it.

Additional Notes or Context

Found during the geotiff performance audit (P-1).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestperformancePR touches performance-sensitive code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions