Detecting Bit Rot in Long-Term Raster Archives

Bit rot is the failure archivists worry about most and encounter least — but when it does occur in imagery it is uniquely insidious, because a raster with a handful of flipped bits opens cleanly, renders plausibly, and reports no error anywhere. This walkthrough is for the GIS archivist responsible for a decade-scale imagery holding who needs to distinguish genuine storage-level corruption from the far more common causes of a bad file, and to have a defensible response when a mismatch is confirmed.

Distinguishing Rot from Everything Else

A checksum mismatch is a symptom, not a diagnosis. Four causes produce one, and they call for different responses — so the first job after a mismatch is triage, not recovery.

Triaging a checksum mismatch on an archived raster Four causes of a mismatch — media corruption, truncated upload, overwrite by a job, and a transient bad read — each with the distinguishing evidence and the correct response. causedistinguishing evidenceresponse media corruption (true bit rot) size matches, few bytes differ, reproduces on re-read restore from a good copy truncated upload size smaller than recorded; file ends mid-strip re-ingest from source overwritten by a job newer LastModified; file is valid but different recover the prior version transient bad read does not reproduce on a second read log it; no data action needed

Only the first row is bit rot, and in audited archives it accounts for well under a twentieth of mismatches. Running the triage before the recovery avoids restoring from a replica to fix a problem the replica shares — which is exactly what happens when an overwrite is misdiagnosed as corruption.

Step-by-Step Procedure

Step 1 — Re-read before believing the mismatch

Transient read errors are common enough that a single mismatch should never trigger a response. Re-read the object, ideally through a different client and network path.

KEY=archive/imagery/2014/ortho_n5432_e0871.tif
for i in 1 2; do
  aws s3api get-object --bucket spatial-archive --key "$KEY" /dev/stdout \
    | sha256sum | awk -v n=$i '{print "read " n ": " $1}'
done

Two identical hashes that both differ from the recorded value confirm the object on disk has changed. Two differing hashes indicate a transport problem, not a stored one.

Step 2 — Compare size and last-modified against the manifest

These two fields separate three of the four causes without reading a byte.

aws s3api head-object --bucket spatial-archive --key "$KEY" \
  --query '{size:ContentLength,modified:LastModified,version:VersionId}'

A smaller size means truncation. A last-modified date after the manifest’s ingest timestamp means something wrote over it. Matching size and date with a differing hash is the signature of genuine corruption.

Step 3 — Localise the damage inside the raster

For a confirmed corruption, finding where the bytes changed tells you whether the file is salvageable. A GeoTIFF’s damage may sit in a single tile, in an overview, or in the header — and only the last makes the file unopenable.

# Compare the archived object against a known-good copy, tile by tile
from osgeo import gdal
import hashlib

def tile_digests(path, band=1):
    ds = gdal.Open(path); b = ds.GetRasterBand(band)
    bx, by = b.GetBlockSize()
    out = {}
    for y in range(0, b.YSize, by):
        for x in range(0, b.XSize, bx):
            w, h = min(bx, b.XSize - x), min(by, b.YSize - y)
            buf = b.ReadRaster(x, y, w, h)
            out[(x, y)] = hashlib.sha256(buf).hexdigest()
    return out

bad, good = tile_digests("/vsis3/spatial-archive/" + KEY), tile_digests("replica_copy.tif")
damaged = [k for k in good if bad.get(k) != good[k]]
print(f"{len(damaged)} of {len(good)} tiles differ: {damaged[:5]}")
Damage localised to three tiles of a 256-tile raster A sixteen by sixteen tile grid with three highlighted tiles in the lower-left quadrant differing from a known-good copy, while the header and overviews are intact. ortho_n5432_e0871.tif · 256 internal tiles · 3 differ What the localisation tells you Header and IFDs intact — the file opens and reports correct metadata. Overviews intact — a thumbnail or low-zoom view looks perfectly normal. Three adjacent full-resolution tiles differ: about 0.4 km² of ground. Contiguity points at a storage-level event rather than a processing bug. Salvage decision: replace the whole object from the replica — patching individual tiles produces a file whose checksum matches nothing.

Step 4 — Replace, do not patch

Restore the whole object from a verified copy and re-record its checksum. Rebuilding a damaged file tile by tile produces a new artefact that matches neither the original checksum nor the replica’s, which destroys the audit trail the archive exists to maintain.

Validation & Verification

After replacement, confirm three things: the object’s hash matches the manifest, its structure validates, and the version history records what happened.

aws s3api get-object --bucket spatial-archive --key "$KEY" /dev/stdout | sha256sum
rio cogeo validate "/vsis3/spatial-archive/$KEY"
aws s3api list-object-versions --bucket spatial-archive --prefix "$KEY" \
  --query 'Versions[].[VersionId,LastModified,Size]' --output table

Expected output is a hash matching the manifest, a valid verdict from the COG validator, and a version list showing both the damaged version and its replacement — the damaged one retained deliberately, because an incident record that deletes its own evidence is not a record.

Troubleshooting

Symptom Root cause Fix
Mismatch that never reproduces Transient read or client-side error Log and move on; do not restore
Every object in a prefix mismatches Manifest hashes computed before a reprojection step Fix the manifest, not the data; re-record from the current objects with a noted caveat
Damage in the header only Failed metadata rewrite by an older tool Restore from replica; check what tool wrote metadata in place
Replica has the same damage Corruption preceded replication Recover from the retained source; if none, quarantine and document
Validator passes but pixels differ Bit flips inside a compressed tile, decoding to plausible values Compare tile digests against a known-good copy — validation cannot see this

The final row is the reason a structural validator is not a substitute for fixity. A compressed tile with altered bytes usually still decompresses, producing pixels that are wrong and entirely plausible.

Operational Execution Checklist

Recording the Incident So It Stays Explicable

A confirmed corruption is a provenance event. The object changes, the archive’s record of it must change too, and a future reader comparing the file against a published checksum needs to find an explanation rather than a discrepancy.

What a corruption incident record contains Six fields of an incident record: object and version, detection context, evidence, triage conclusion, remedy, and the retention of both versions. object + versionarchive/imagery/2014/ortho_n5432_e0871.tif @ v.3f9c detected2026-08-11, quarterly fixity pass, slice 2 evidenceexpected 1220a3f1… observed 1220b74e… · 3 of 256 tiles differ triagesize and last-modified unchanged; reproduces on re-read → media corruption remedyreplaced from the eu-west-1 replica, verified · new version v.8a1d retaineddamaged version kept and marked — evidence, not clutter

Store the record with the archive rather than in a ticketing system, because tickets are retired and archives are not. Where the object is publicly catalogued, link the record from the catalogue entry: a reader who fetched the file before the repair has a copy whose checksum no longer matches anything, and the incident record is the only thing that will ever explain why.

Frequently Asked Questions

How often does genuine bit rot actually occur?

Rarely enough that a single-digit number of confirmed cases across a multi-petabyte archive over a decade is typical, and often enough that an archive with no detection would never know. The reason to run the audit is not the expected rate but the asymmetry: detection is nearly free from inventory metadata, and an undetected corruption in a preservation archive is permanent.

Does compression make rot worse?

It concentrates the consequences. In an uncompressed raster a flipped bit alters one pixel; inside a compressed tile it can alter the whole tile’s decoded output, because the corrupted symbol changes everything after it in that block. That argues for smaller internal tiles in preservation copies rather than against compression — the ratio saving remains worth far more than the blast-radius difference.

Should imagery be stored with per-tile checksums?

For archives where partial recovery is genuinely useful, yes — but note that neither GeoTIFF nor COG carries per-tile digests natively, so it means a sidecar. The pragmatic middle ground is a sidecar listing per-tile digests generated at ingest, which costs a few kilobytes per scene and turns “this file is damaged” into “these three tiles are damaged”, which is exactly the information the salvage decision needs.

Does the same triage apply to vector and point-cloud objects?

The four causes are identical; only the localisation step differs. For a GeoParquet object, the equivalent of comparing tiles is comparing per-row-group statistics and column checksums against a known-good copy, which narrows the damage to specific row groups. For a COPC file it is comparing octree node digests. In every case the principle holds: find where the difference is before deciding whether the object is salvageable.

Should a corrupted object be deleted once it is replaced?

No — retain it as a noncurrent version and mark it. It is the evidence that the incident happened, it is what a future auditor will ask to see, and its storage cost is trivial. Expire it on the same schedule as other noncurrent versions rather than deleting it by hand, so the retention is a policy rather than a decision.

How often should a known-good copy be re-verified?

On the same rotating schedule as everything else, and with one addition: after any object is used as a repair source, verify it immediately. A replica that supplied a repair has just been read in full, which is the cheapest possible moment to confirm it is itself intact, and the worst possible outcome is repairing one object from another that shares the defect.

Up one level: Archive Integrity Verification.