COPC vs EPT for Web Delivery

TL;DR: Both move about the same bytes for the same query. COPC does it in a handful of range requests against one object; EPT does it in hundreds of requests against thousands of files. Choose COPC for publication and interactive reading, EPT when the dataset is revised in pieces by a system that owns its own storage.

# Context and Motivation

This guide is part of COPC and Cloud-Native Point Cloud Formats. Both formats solve the same problem — answering a spatial, level-of-detail query without downloading everything — and they solve it with the same data structure, an octree whose nodes each hold a spatially even sample of their subtree. The difference is entirely in how that octree is stored, and every practical consequence follows from that one choice.

EPT stores each node as its own file, with a JSON manifest describing the tree. COPC stores every node as a byte range inside one LAZ file, with the tree in a variable length record at the front. The first is a directory; the second is a file.

One octree, two ways to store it The same octree drawn twice. As EPT it becomes a directory containing a JSON manifest and one LAZ file per node, thousands of objects in total. As COPC it becomes a single LAZ file in which each node occupies a contiguous byte range, with the tree recorded in a header record. EPT — a directory COPC — a file ept.json — the manifest ept-data/0-0-0-0.laz ept-data/1-0-0-0.laz … 4,812 more node files ept-hierarchy/*.json block.copc.laz header · copc info VLR · chunks every node is a byte range inside that one object every operational difference below follows from this single storage choice

# Prerequisites and Assumptions

Requirement Detail
PDAL 2.4+ — readers.ept and readers.copc both ship in the standard build
A hosting target object storage or a static web server that honours range requests
An access pattern how often the data is read, and how often it changes
A client inventory which viewers and libraries your consumers already use

# Step-by-Step Implementation

# Step 1 — Characterise the access pattern

Two numbers decide this: reads per month and revisions per month. Their ratio picks the format more reliably than any feature comparison.

# Step 2 — Count the requests, not just the bytes

Over a wide-area network a request costs 30–80 ms of latency regardless of size. Two hundred requests is fifteen seconds of latency before a single byte of useful work.

# Step 3 — Check what your clients speak

Browser viewers increasingly read COPC natively over HTTPS with no server component. Desktop GIS support is broader for both. Legacy tooling reads neither and needs plain LAS.

# Step 4 — Test both against a real query

The comparison script below runs the same window and resolution against each and reports points, time and request count.

# Complete Working Example

python
"""Time the same spatial query against an EPT dataset and a COPC file."""
from __future__ import annotations

import json
import time

import pdal

WINDOW = "([512000, 513000], [4783000, 4784000])"
RESOLUTION = 2.0


def run(reader_type: str, filename: str) -> dict:
    spec = json.dumps({"pipeline": [{
        "type": reader_type,
        "filename": filename,
        "bounds": WINDOW,
        "resolution": RESOLUTION,
    }]})
    started = time.perf_counter()
    pipeline = pdal.Pipeline(spec)
    n = pipeline.execute()
    return {
        "reader": reader_type,
        "points": n,
        "seconds": round(time.perf_counter() - started, 2),
    }


def compare(ept_url: str, copc_url: str) -> dict:
    ept = run("readers.ept", ept_url)
    copc = run("readers.copc", copc_url)

    # The two formats sample the octree the same way, so the point counts should
    # be close. A large divergence means the two datasets were built differently.
    ratio = copc["points"] / max(ept["points"], 1)
    if not 0.8 < ratio < 1.25:
        raise AssertionError(
            f"point counts diverge too far to compare fairly: {ept['points']} vs {copc['points']}"
        )
    return {"ept": ept, "copc": copc,
            "speedup": round(ept["seconds"] / max(copc["seconds"], 1e-6), 2)}


if __name__ == "__main__":
    print(json.dumps(compare(
        ept_url="https://example.org/data/region/ept.json",
        copc_url="https://example.org/data/region.copc.laz",
    ), indent=2))

# Key Parameter Table

Dimension EPT COPC
Storage unit directory of thousands of files one file
Index ept.json plus hierarchy files a VLR inside the file
Requests for one query 100–300 4–8
Bytes for one query comparable comparable
Partial update rewrite affected node files rewrite the whole file
Copy or checksum directory sync one object
Signed URL or expiry per file, or a prefix policy one URL
Read by plain LAS tools no yes
One ratio decides it Four situations placed by reads against revisions. A published archive read constantly and revised yearly wants COPC. A working dataset revised daily and read occasionally wants EPT. A dataset both read and revised often needs EPT and a caching layer. A dataset neither read interactively nor revised should stay as plain tiles. read often, revised rarely COPC — one object, range reads revised often, read rarely EPT — rewrite a few node files both often EPT plus a cache in front neither leave it as tiles the last row is the one people skip, and it is the cheapest answer whenever it applies

# Verification

Both return comparable point counts. Asserted in the example. A large divergence means the two datasets were built with different octree parameters, and any timing comparison between them is meaningless.

Request counts match expectation. Capture them at the proxy or with the object store’s access log. If COPC is issuing hundreds of requests, resolution was probably not set.

The client actually works. A format that your viewer cannot open is not a candidate however good the numbers are.

Where the time goes on a remote query Time to answer one query over a wide-area network, split into request latency and transfer. EPT spends 5.9 seconds on 212 round trips and 0.5 seconds transferring. COPC spends 0.3 seconds on 5 round trips and 1.5 seconds transferring. The byte totals are similar; the round trips are not. round-trip latency transfer EPT · 212 requests 5.9 s waiting 0.5 s COPC · 5 requests 1.5 s 1.8 s total against 6.4 s on a local disk the two are within a few percent of each other — the gap is a network effect, so benchmark over the link your users will actually have. Both formats gain from HTTP/2 connection reuse; neither gains enough to close a 200-request gap.

# Gotchas and Edge Cases

A COPC file cannot be updated in place. The octree ordering is the file layout, so revising one region means rebuilding the object. For a dataset under continuous revision that is the argument for EPT, and it is a strong one.

EPT’s file count is an operational cost. Thousands of objects per dataset makes lifecycle policies, replication and cost attribution harder, and some object stores charge per request in a way that shows up.

Neither is an archival format. Both are derived products. Keep the source tiles — the conversion is reproducible, the original acquisition is not.

Do not benchmark over localhost. On a local disk the two formats are within a few percent. The entire difference is round trips, so measure over the link your users will use.

# Frequently Asked Questions

Do COPC and EPT transfer different amounts of data?

Not meaningfully. They store the same octree with the same node sampling, so a given query pulls a similar number of bytes from either. The difference is that EPT needs one request per node — often two or three hundred — while COPC needs a handful of range requests against one object.

When is EPT still the better choice?

When the dataset is revised in pieces. Because each node is its own file, updating a region rewrites a few small objects rather than a multi-gigabyte file. If your data changes more often than it is read, that outweighs the request-count penalty.

Can a browser read COPC without a server?

Yes, provided the host honours HTTP range requests. The client fetches the header, reads the index from a variable length record, and then requests the byte ranges it needs. That is the property that has made COPC the common choice for web delivery.

Should either format replace my archive?

No. Both are derived products optimised for reading. Keep the original tiles: the conversion to COPC or EPT is reproducible from them, and nothing reproduces the acquisition.