LAZ vs COPC vs EPT for Point Cloud Delivery
This page chooses between the three ways a point cloud gets delivered from object storage — plain LAZ, COPC, and EPT — on the axes that actually decide it: whether a client can read a spatial subset without downloading everything, whether the payload is one file or a directory of thousands, and what each costs to build and to keep current. All three hold the same points; they differ entirely in what a reader can do without reading it all.
Why you hit this
A survey delivered as plain LAZ is a sealed box. Reading the points inside a 200 m radius means downloading the whole file, decompressing it, and discarding 99% of what you fetched — which is fine at 200 MB and untenable at 40 GB. Both COPC and EPT solve that, in different ways with different operational consequences, and the choice tends to be made by whichever tool the team already had rather than by what the access pattern needs.
The wider container landscape is mapped in 3D format standards comparison; this page is specifically about delivery over HTTP.
Prerequisites
- PDAL 2.6+ (COPC reader and writer are built in),
untwineorentwinefor EPT, andlaspy>=2.5withlazrs. - Object storage that honours HTTP range requests. S3, GCS, Azure Blob and any standards-compliant CDN do; some older proxies do not, and COPC degrades to a full download when they do not.
- A cloud in a projected metric CRS with classification already applied — all three formats preserve whatever you give them and none of them improves it.
Step-by-Step
1. Understand what each format actually is
LAZ is LASzip-compressed LAS: a header, variable-length records, then compressed point chunks. Chunks are independently decompressible, which is what makes partial reads possible, but nothing in the file says which chunk covers which ground, so finding the right chunk means reading them all.
COPC is a valid LAZ file with an octree baked into it. The points are reordered so that each octree node’s points are contiguous, and a VLR holds the node hierarchy with each node’s byte offset and length. A reader fetches the hierarchy with one range request, decides which nodes it needs, and fetches exactly those byte ranges. It is still one file, and any LAZ reader can open it and see all the points.
EPT is a directory: a JSON manifest plus one file per octree node, in LAZ or binary. A reader fetches the manifest, then the node files it needs, as ordinary whole-file GETs. No range-request support is required, and the node files can be served by anything.
2. Build a COPC from an existing LAZ
One PDAL invocation, and the output remains a readable LAZ.
import json
import pdal
spec = {"pipeline": [
"survey_utm33n.laz",
{"type": "writers.copc", "filename": "survey.copc.laz",
"forward": "all"}
]}
n = pdal.Pipeline(json.dumps(spec)).execute()
print(f"{n:,} points written to COPC")
# Any LAZ reader still opens it; a COPC reader also sees the hierarchy.
pdal info survey.copc.laz --summary | head -20
The reordering is the expensive part: the writer has to build the octree and sort the points into node order, which for a billion-point survey means an external sort and roughly the same wall clock as reading the file twice.
3. Read a spatial subset without downloading the file
This is the whole point, and it works directly against a URL.
import json
import pdal
spec = {"pipeline": [
{"type": "readers.copc",
"filename": "https://storage.example.com/surveys/survey.copc.laz",
"bounds": "([598000, 598400], [6643800, 6644200])",
"resolution": 0.5},
{"type": "writers.las", "filename": "subset.laz", "forward": "all"},
]}
p = pdal.Pipeline(json.dumps(spec))
print(p.execute(), "points fetched")
resolution is the parameter that makes COPC genuinely useful for a viewer: it stops the octree descent at the level whose spacing matches the value, so a wide overview costs a few hundred kilobytes rather than the whole extent at full density. Combined with bounds it is a level-of-detail query over a point cloud, served from static object storage.
4. Build an EPT when range requests are not available
untwine is the current builder and it is considerably faster than the older entwine for the same job.
untwine --files survey_utm33n.laz --output_dir ept_survey/
ls ept_survey/
# ept.json ept-data/ ept-hierarchy/ ept-sources/
python - <<'PY'
import json
m = json.load(open("ept_survey/ept.json"))
print("points:", f"{m['points']:,}", "| span:", m["span"], "| srs:", m["srs"]["authority"], m["srs"]["horizontal"])
PY
The directory structure is the trade. It serves from anything, it caches well because each node is an immutable whole file, and it turns one artifact into hundreds of thousands of small objects — which matters for storage cost, for listing operations, and for anything that syncs the bucket.
5. Verify the index actually works
An index that exists and is not used is the failure mode to check for.
import json
import time
import pdal
def timed_subset(reader, url, bounds, resolution=None):
stage = {"type": reader, "filename": url, "bounds": bounds}
if resolution:
stage["resolution"] = resolution
t0 = time.perf_counter()
p = pdal.Pipeline(json.dumps({"pipeline": [stage]}))
n = p.execute()
return n, time.perf_counter() - t0
bounds = "([598000, 598400], [6643800, 6644200])"
n_full, t_full = timed_subset("readers.las", "survey_utm33n.laz", bounds)
n_copc, t_copc = timed_subset("readers.copc", "survey.copc.laz", bounds)
print(f"LAZ {n_full:,} points in {t_full:6.1f}s")
print(f"COPC {n_copc:,} points in {t_copc:6.1f}s ({t_full / t_copc:.0f}x faster)")
assert n_full == n_copc, "the two readers disagree about which points are in the window"
The equality assertion is the one that matters. A speedup with a different point count means the COPC’s octree does not agree with the coordinates, which happens when a file is reordered without rebuilding the hierarchy.
Expected Output & Verification
A representative comparison on a 40 GB municipal survey:
1,204,882,340 points written to COPC
LAZ 2,140,118 points in 412.7s
COPC 2,140,118 points in 3.9s (106x faster)
points: 1,204,882,340 | span: 256 | srs: EPSG 25832
Two things to check beyond the timing. The point counts must match exactly, and the CRS in the COPC header and the EPT manifest must be the one you put in — both formats forward it, and both will happily forward an absent one.
Common Errors
COPC reads are as slow as plain LAZ. The server does not honour range requests, so the reader falls back to fetching the whole file. Test with curl -r 0-1023 and check for a 206 Partial Content response.
readers.copc reports zero points in a window you can see data in. The bounds are in a different CRS from the file. COPC bounds are in the file’s own CRS, with no reprojection.
The EPT directory is enormous in object count. That is inherent — a billion-point survey produces hundreds of thousands of node files. Budget for the per-object storage cost and avoid operations that list the prefix.
A COPC built from a reordered LAZ returns wrong subsets. The octree hierarchy was carried forward from the source while the point order changed. Always rebuild the COPC from the source rather than patching one.
Frequently Asked Questions
Which should a new pipeline use?
COPC, unless you cannot rely on range requests. It keeps the single-file operational model, any LAZ reader can still open it, and it is now the format PDAL, QGIS and the browser viewers read natively.
Is COPC lossy relative to LAZ?
No. It is a valid LAZ file with the points in a particular order and one extra VLR. Every point, attribute and header field survives.
Can I keep the archive as plain LAZ and derive COPC for delivery?
Yes, and it is a reasonable arrangement: the archive stays in the form the surveyor delivered, and the COPC is a regenerable derivative. It costs the rebuild time whenever the source changes, which for an archive is rarely.
Does either format help with writing?
Neither is designed for partial writes. Both are built once and read many times, so a workflow that appends points continuously wants a database rather than either of these.
One operational note that decides more migrations than the format comparison does. COPC’s single-file model means the archive, the delivery copy and the thing a viewer reads are the same object, so there is nothing to keep in sync. EPT’s directory model separates them, and a directory of half a million small files behaves differently from one large one in every system that touches it — backup, replication, lifecycle rules, cost reporting and any operation that lists a prefix. That difference is usually a stronger argument than the request-count comparison.
The second is that neither format changes what is in the cloud. A survey delivered without a CRS, or with vegetation misclassified, is exactly as wrong after conversion. The indexing is about access, and the quality work belongs upstream of it.
Related Guides
- 3D Format Standards Comparison — the wider container trade-offs
- Point Cloud Density Standards — the density figures a resolution query depends on
- Computing Point Density from LAZ with PDAL — measuring what a subset actually returned
Back to 3D Format Standards Comparison.