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.gzis 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 ignoredand exits 2 instead of 0. A script checking$? -eq 0calls that a failure, one checking$? -ne 1calls it a pass. Decide which you want on purpose. -tchecks the gzip layer only. A.tar.gzpasses 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 withtar -tzf backup.tar.gzand check its exit status.- macOS ships a BSD-derived gzip.
-tand-vwork 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 gzipgets 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