← back to latest

Check whether a compressed backup is corrupt before you need it

gzip -tv *.gz

anatomy

gzip
The same binary that compressed the file. Every gzip file ends with an 8-byte trailer holding a CRC-32 checksum and the original length, so gzip can tell you whether the bytes it decompresses are the bytes that went in.
-t
Test. Decompress the whole file, throw the output away, and compare the result against the CRC-32 and length in the trailer. Nothing is written to disk and the original file is left alone. It costs the same CPU time as a full decompression, because that is what it is.
-v
Verbose. Without it a passing file prints nothing at all, so a clean run and a run that skipped your files look identical. With it every good file gets an explicit `OK` and every bad one gets its name next to the error.
*.gz
Every gzip file in the directory, expanded by the shell before gzip starts. Read the Gotchas before trusting this on a large set: one truncated file can end the run early.

Sample output

backup-2026-10-12.sql.gz:	 OK
backup-2026-10-13.sql.gz:	
gzip: backup-2026-10-13.sql.gz: invalid compressed data--crc error

gzip: backup-2026-10-13.sql.gz: invalid compressed data--length error
backup-2026-10-14.sql.gz:	
gzip: backup-2026-10-14.sql.gz: unexpected end of file

Three different stories. The 12th is fine. The 13th is the right size but a byte inside changed, so the checksum no longer matches: bit rot, a bad disk, or a flaky copy. The 14th is truncated, which almost always means the job that wrote it died or the transfer was cut off. And the 15th never appears at all, because gzip stopped at the truncated file. The exit status is 1, so a script notices; you might not.

When you would reach for it

Before you delete the uncompressed original, after you copy a backup off a server, or on a schedule against the directory you plan to restore from someday. A gzip file that is cut short still has the right name and a plausible size, and nothing about it looks wrong until the day you need it. Testing reads every byte now, while the source still exists to make a new copy from.

Gotchas

  • GNU gzip treats “unexpected end of file” as fatal and quits the whole run, so every file after the truncated one goes unchecked. In the sample, backup-2026-10-15.sql.gz is fine, but this command never says so. Test files one at a time with the loop in Variants when the result matters.
  • A file with junk appended after a valid stream still decompresses, but gzip prints decompression OK, trailing garbage ignored and exits 2 instead of 0. A script checking $? -eq 0 calls that a failure, one checking $? -ne 1 calls it a pass. Decide which you want on purpose.
  • -t checks the gzip layer only. A .tar.gz passes as long as the compressed stream matches its checksum, even if the tar inside was written by a broken tool. To check the archive structure too, read it all the way through with tar -tzf backup.tar.gz and check its exit status.
  • macOS ships a BSD-derived gzip. -t and -v work and the exit status is reliable, but the message wording differs, so do not grep for GNU’s exact error text in a portable script. On Alpine, BusyBox’s gzip applet is minimal; apk add gzip gets you the GNU behavior shown here.

Variants

$ for f in *.gz; do gzip -t "$f" && echo "ok   $f" || echo "BAD  $f"; done

One gzip per file, so a truncated file cannot end the run early. gzip’s own error lands right above each BAD line, and trailing garbage counts as BAD because exit 2 is not 0

$ xz -tv backup.sql.xz && bzip2 -tv backup.sql.bz2

The other compressors have the same flag. Both formats store their own checksum, so the test means the same thing

$ find /srv/backups -name '*.gz' -mtime -7 -exec gzip -t {} \; -print

Prints only the files from the last week that pass. Because -exec ... \; runs gzip once per file, -print fires only when that file’s test exits 0

lineage

The checksum that makes this work is written into the format itself. RFC 1952, the gzip file format specification by L. Peter Deutsch, published in May 1996, fixes the layout: a 10-byte header, the DEFLATE stream, then a trailer of exactly two fields, CRC32 and ISIZE. CRC32 is a checksum of the uncompressed data and ISIZE is its length modulo 2^32. That modulo is why `gzip -l` spent decades reporting nonsense sizes for files over 4 GiB, and why GNU gzip 1.12, released in April 2022, changed `-l` to decompress the entire file and count the bytes. The trailer is also why a test needs no reference copy. The original file is long gone, but its fingerprint travelled with it in the last eight bytes.