ZetCode

Linux gzip and xz Commands

last modified October 23, 2026

The gzip and xz commands compress single files. gzip is fast and available everywhere; it compresses log files, web content, and .tar.gz archives. xz compresses better at the cost of more time and memory and is used for software distribution, such as Linux kernel sources and .tar.xz packages.

Neither command creates archives of several files; that is the job of tar, which can call them. The examples were run on Debian 12 with gzip 1.12, XZ Utils 5.4.1, bzip2 1.0.8, and zstd 1.5.4. The main test file, access.log, is a 15 MB web server log with 120,000 lines.

Compressing a file

In its basic form, gzip replaces a file with its compressed version.

head -n 2 access.log
ls -l access.log
gzip access.log
ls -l access.log*

The original file is gone; access.log.gz takes its place, with a tenth of the size. Log files compress very well because they repeat the same addresses, paths, and browser names. The compressed file keeps the permissions and the modification time of the original. gzip prints nothing on success.

$ head -n 2 access.log
192.0.2.176 - - [21/Oct/2026:00:00:00 +0000] "GET /about HTTP/1.1" 200 32027 "-" "Mozilla/5.0 (Macintosh) Safari/605.1.15"
192.0.2.93 - - [21/Oct/2026:00:00:00 +0000] "GET /products/42 HTTP/1.1" 200 5537 "-" "Mozilla/5.0 (Macintosh) Safari/605.1.15"
$ ls -l access.log
-rw-r--r-- 1 jano jano 15139912 Oct 22 23:59 access.log
$ gzip access.log
$ ls -l access.log*
-rw-r--r-- 1 jano jano 1541139 Oct 22 23:59 access.log.gz

Decompressing

The gunzip command, or gzip -d, restores the original file and removes the compressed one.

gunzip access.log.gz
ls -l access.log*

The size and the timestamp are the same as before compression. Like compression, decompression works in place, which matters when disk space is tight: during the operation, both files exist.

$ gunzip access.log.gz
$ ls -l access.log*
-rw-r--r-- 1 jano jano 15139912 Oct 22 23:59 access.log

Keeping the original and verbose output

The -k option keeps the input file, and -v reports the compression ratio.

gzip -k -v access.log gpl.txt
ls -l access.log* gpl.txt*
gzip access.log

The ratio is the space saved: 89.8% for the log, 65.5% for the text of the GPL license, which is ordinary English prose. gzip refuses to overwrite an existing compressed file; in a terminal, it asks first. The -f option forces the overwrite.

$ gzip -k -v access.log gpl.txt
access.log:	 89.8% -- created access.log.gz
gpl.txt:	 65.5% -- created gpl.txt.gz
$ ls -l access.log* gpl.txt*
-rw-r--r-- 1 jano jano 15139912 Oct 22 23:59 access.log
-rw-r--r-- 1 jano jano  1541139 Oct 22 23:59 access.log.gz
-rw-r--r-- 1 jano jano    35149 Oct 20 10:00 gpl.txt
-rw-r--r-- 1 jano jano    12138 Oct 20 10:00 gpl.txt.gz
$ gzip access.log
gzip: access.log.gz already exists;	not overwritten

Writing to standard output

The -c option writes the result to standard output and leaves the input alone. Without a file name, gzip compresses its standard input.

gzip -c gpl.txt > license.gz
ls -l gpl.txt license.gz
ls -l /etc | gzip > etc-listing.gz
gzip -dc license.gz | head -n 3

With -c, we can choose the output name freely. Compressing a pipe is useful for output that would take much space, such as database dumps: pg_dump mydb | gzip > mydb.sql.gz. gzip -dc decompresses to standard output.

$ gzip -c gpl.txt > license.gz
$ ls -l gpl.txt license.gz
-rw-r--r-- 1 jano jano 35149 Oct 20 10:00 gpl.txt
-rw-r--r-- 1 jano jano 12138 Oct 23 21:59 license.gz
$ ls -l /etc | gzip > etc-listing.gz
$ gzip -dc license.gz | head -n 3
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

Listing compressed files

The -l option shows the compressed and uncompressed sizes without decompressing.

gzip -l access.log.gz gpl.txt.gz license.gz
gzip -lv access.log.gz
gzip -lN license.gz

The uncompressed names are derived from the names of the compressed files. The header, however, stores the original name: license.gz was created from gpl.txt, which -N shows. gunzip -N license.gz would restore it under that name. With -v, the method, the CRC checksum, and the stored time are shown too. The gzip format stores the uncompressed size in 32 bits, so for files of 4 GiB or more, the listed size is wrong.

$ gzip -l access.log.gz gpl.txt.gz license.gz
         compressed        uncompressed  ratio uncompressed_name
            1541139            15139912  89.8% access.log
              12138               35149  65.5% gpl.txt
              12138               35149  65.5% license
            1565415            15210210  89.7% (totals)
$ gzip -lv access.log.gz
method  crc     date  time           compressed        uncompressed  ratio uncompressed_name
defla 9993cffb Oct 22 23:59             1541139            15139912  89.8% access.log
$ gzip -lN license.gz
         compressed        uncompressed  ratio uncompressed_name
              12138               35149  65.5% gpl.txt

Reading compressed files directly

Tools with a z prefix work on compressed files without creating a decompressed copy: zcat, zless, zgrep, zdiff, and others.

zcat access.log.gz | head -n 2
zgrep -c '" 404 ' access.log.gz
zcat access.log.gz | awk '{print $9}' | sort | uniq -c | sort -rn
zdiff license.gz gpl.txt && echo "same content"

zgrep takes the same options as grep; here it counts the requests with status 404. Rotated logs in /var/log are kept compressed, and these tools search them in place. zcat also handles uncompressed files when given -f, and zgrep does so by default.

$ zcat access.log.gz | head -n 2
192.0.2.176 - - [21/Oct/2026:00:00:00 +0000] "GET /about HTTP/1.1" 200 32027 "-" "Mozilla/5.0 (Macintosh) Safari/605.1.15"
192.0.2.93 - - [21/Oct/2026:00:00:00 +0000] "GET /products/42 HTTP/1.1" 200 5537 "-" "Mozilla/5.0 (Macintosh) Safari/605.1.15"
$ zgrep -c '" 404 ' access.log.gz
10902
$ zcat access.log.gz | awk '{print $9}' | sort | uniq -c | sort -rn
  65452 200
  21892 304
  10902 404
  10880 301
  10874 500
$ zdiff license.gz gpl.txt && echo "same content"
same content

Compression levels

The options -1 to -9 set the compression level. -1 (or --fast) is the fastest, -9 (--best) compresses best; the default is 6.

for l in 1 6 9; do printf -- "-%s: " $l; gzip -$l -c gpl.txt | wc -c; done
zcat access.log.gz > plain.log
time gzip -1 -c plain.log > fast.gz
time gzip -9 -c plain.log > best.gz
ls -l fast.gz best.gz access.log.gz

For the license text, levels 6 and 9 give almost the same size. For the log, level 9 saves 12% compared with level 6 and takes more than five times longer than level 1. Decompression speed does not depend on the level. The default is a good compromise; use -9 for files that are compressed once and downloaded many times.

$ for l in 1 6 9; do printf -- "-%s: " $l; gzip -$l -c gpl.txt | wc -c; done
-1: 14229
-6: 12138
-9: 12132
$ zcat access.log.gz > plain.log
$ time gzip -1 -c plain.log > fast.gz

real	0m0.093s
user	0m0.090s
sys	0m0.003s
$ time gzip -9 -c plain.log > best.gz

real	0m0.535s
user	0m0.530s
sys	0m0.004s
$ ls -l fast.gz best.gz access.log.gz
-rw-r--r-- 1 jano jano 1541139 Oct 22 23:59 access.log.gz
-rw-r--r-- 1 jano jano 1359039 Oct 23 21:59 best.gz
-rw-r--r-- 1 jano jano 2187256 Oct 23 21:59 fast.gz

Multiple files and directories

Given several files, gzip compresses each of them separately. The -r option walks directories.

cp gpl.txt a.txt; cp gpl.txt b.txt
gzip a.txt b.txt
ls a.txt* b.txt*
gzip -r project
find project -type f
gunzip -r project

The result is one .gz file per input, not a single archive. gzip -r replaces every file in the tree with its compressed version, which is rarely what we want. To pack a directory into one file, use tar, as shown below.

$ cp gpl.txt a.txt; cp gpl.txt b.txt
$ gzip a.txt b.txt
$ ls a.txt* b.txt*
a.txt.gz
b.txt.gz
$ gzip -r project
$ find project -type f
project/LICENSE.gz
project/docs/services.txt.gz
project/src/f2.c.gz
project/src/f1.c.gz
project/src/f3.c.gz
$ gunzip -r project

Concatenated gzip files

A gzip file can consist of several members, and concatenating gzip files produces a valid gzip file.

echo "first part" | gzip > part1.gz
echo "second part" | gzip > part2.gz
cat part1.gz part2.gz > joined.gz
zcat joined.gz
gzip -l joined.gz

Decompression yields both parts in order. This lets programs append compressed chunks to a file. gzip -l, however, reports only the size of the last member, 12 bytes, and a meaningless negative ratio.

$ echo "first part" | gzip > part1.gz
$ echo "second part" | gzip > part2.gz
$ cat part1.gz part2.gz > joined.gz
$ zcat joined.gz
first part
second part
$ gzip -l joined.gz
         compressed        uncompressed  ratio uncompressed_name
                 63                  12 -425.0% joined

Testing integrity

Every gzip file contains a CRC-32 checksum of the data. The -t option decompresses in memory and checks it.

gzip -t access.log.gz; echo $?
head -c 300000 access.log.gz > broken.gz
gzip -t broken.gz; echo $?
zcat broken.gz | wc -l

A good file passes silently with status 0. The truncated copy, like an interrupted download, fails with unexpected end of file and status 1. zcat still outputs what it could decompress, 23,302 complete lines, so data before the damaged point can be recovered.

$ gzip -t access.log.gz; echo $?
0
$ head -c 300000 access.log.gz > broken.gz
$ gzip -t broken.gz; echo $?

gzip: broken.gz: unexpected end of file
1
$ zcat broken.gz | wc -l

gzip: broken.gz: unexpected end of file
23302

Data that does not compress

Compression works by finding repetition. Random data, and data that is already compressed, such as JPEG images, videos, or ZIP files, has none.

ls -l random.bin
gzip -k -v random.bin
xz -k -v random.bin
ls -l random.bin*

Both compressed files are slightly larger than the original, by the size of their headers. Compressing such files again only costs time.

$ ls -l random.bin
-rw-r--r-- 1 jano jano 1000000 Oct 23 21:59 random.bin
$ gzip -k -v random.bin
random.bin:	 -0.0% -- created random.bin.gz
$ xz -k -v random.bin
random.bin: 976.7 KiB / 976.6 KiB = 1.000
$ ls -l random.bin*
-rw-r--r-- 1 jano jano 1000000 Oct 23 21:59 random.bin
-rw-r--r-- 1 jano jano 1000184 Oct 23 21:59 random.bin.gz
-rw-r--r-- 1 jano jano 1000108 Oct 23 21:59 random.bin.xz

Name and timestamp in the header

By default, gzip stores the original file name and modification time in the header. The -n option leaves them out.

gzip -c gpl.txt | md5sum
touch gpl.txt
gzip -c gpl.txt | md5sum
gzip -cn gpl.txt | md5sum
touch -d '2020-01-01' gpl.txt
gzip -cn gpl.txt | md5sum

Touching the file changes the compressed output, although the content is the same. With -n, the output depends only on the content. This matters for reproducible builds and for caches that compare checksums of compressed files. When decompressing, -N restores the stored name and timestamp.

$ gzip -c gpl.txt | md5sum
ad7f4f078004f6b963f80cacc84d61af  -
$ touch gpl.txt
$ gzip -c gpl.txt | md5sum
8df03619d94d4763767c4cfd39ed0a9f  -
$ gzip -cn gpl.txt | md5sum
7aa57b7a02bdafacf2d797f6eda77f2c  -
$ touch -d '2020-01-01' gpl.txt
$ gzip -cn gpl.txt | md5sum
7aa57b7a02bdafacf2d797f6eda77f2c  -

Compressing with xz

The xz command works like gzip: it replaces the file with a .xz version, and accepts -k, -c, -d, -t, -v, and -l.

zcat access.log.gz > access.log
xz -k -v access.log
ls -l access.log access.log.xz access.log.gz
xz -l access.log.xz

The xz file is 22% smaller than the gzip one, but compression took five seconds instead of a fraction of a second. The ratio in xz output is the compressed size divided by the original, 0.080, the opposite of gzip's space saved. xz protects the data with a CRC64 checksum.

$ zcat access.log.gz > access.log
$ xz -k -v access.log
access.log: 1,176.1 KiB / 14.4 MiB = 0.080, 2.7 MiB/s, 0:05
$ ls -l access.log access.log.xz access.log.gz
-rw-r--r-- 1 jano jano 15139912 Oct 23 21:59 access.log
-rw-r--r-- 1 jano jano  1541139 Oct 22 23:59 access.log.gz
-rw-r--r-- 1 jano jano  1204348 Oct 23 21:59 access.log.xz
$ xz -l access.log.xz
Strms  Blocks   Compressed Uncompressed  Ratio  Check   Filename
    1       1  1,176.1 KiB     14.4 MiB  0.080  CRC64   access.log.xz

Decompressing and reading xz files

unxz is the same as xz -d, and xzcat as xz -dc. The xzgrep, xzless, and xzdiff commands correspond to their z counterparts.

xzcat access.log.xz | tail -n 1
xzgrep -c 'curl/' access.log.xz
xz -t access.log.xz && echo OK
mv access.log access.log.orig
unxz access.log.xz
cmp access.log access.log.orig && echo identical

The round trip restores the file byte for byte, as cmp confirms. Decompressing xz is much faster than compressing it.

$ xzcat access.log.xz | tail -n 1
198.51.100.254 - - [22/Oct/2026:09:23:28 +0000] "POST /static/style.css HTTP/1.1" 200 40020 "-" "Mozilla/5.0 (Macintosh) Safari/605.1.15"
$ xzgrep -c 'curl/' access.log.xz
30099
$ xz -t access.log.xz && echo OK
OK
$ mv access.log access.log.orig
$ unxz access.log.xz
$ cmp access.log access.log.orig && echo identical
identical

xz levels, extreme mode, and threads

xz has levels 0 to 9 (default 6) and the -e (extreme) modifier, which tries harder at the same memory use. The -T option sets the number of threads; -T0 uses as many as there are processor cores.

time xz -6 -T1 -c access.log > a6.xz
time xz -9e -T1 -c access.log > a9e.xz
time xz -6 -T0 -c access.log > a6t.xz
time xz -6 -T0 --block-size=2MiB -c access.log > a6b.xz
ls -l a6.xz a9e.xz a6t.xz a6b.xz
xz -lvv a9e.xz | grep -i memory

-9e saves another 6% and takes twice as long. -T0 alone did not help: in threaded mode, xz splits the input into blocks and compresses them in parallel, but the default block size at level 6 is 24 MiB, three times the dictionary size, so our 14 MiB file is a single block. With 2 MiB blocks, compression ran almost six times faster on eight cores, and the file grew by less than 1%, because each block is compressed independently. Files compressed in blocks can also be decompressed in parallel. Level 9 needs a 64 MiB dictionary; the decompressor needs 65 MiB of memory, and the compressor several hundred MiB per thread.

$ time xz -6 -T1 -c access.log > a6.xz

real	0m5.264s
user	0m5.226s
sys	0m0.037s
$ time xz -9e -T1 -c access.log > a9e.xz

real	0m9.872s
user	0m9.809s
sys	0m0.062s
$ time xz -6 -T0 -c access.log > a6t.xz

real	0m5.236s
user	0m5.195s
sys	0m0.048s
$ time xz -6 -T0 --block-size=2MiB -c access.log > a6b.xz

real	0m0.909s
user	0m5.835s
sys	0m0.099s
$ ls -l a6.xz a9e.xz a6t.xz a6b.xz
-rw-r--r-- 1 jano jano 1212608 Oct 23 21:59 a6b.xz
-rw-r--r-- 1 jano jano 1204356 Oct 23 21:59 a6t.xz
-rw-r--r-- 1 jano jano 1204348 Oct 23 21:59 a6.xz
-rw-r--r-- 1 jano jano 1129112 Oct 23 21:59 a9e.xz
$ xz -lvv a9e.xz | grep -i memory
  Memory needed:     65 MiB

Comparing compressors

The following script compresses the log with several tools and levels, and measures the size and the compression and decompression times.

cat compare.sh
bash compare.sh access.log

TIMEFORMAT=%R makes the time keyword print only the elapsed seconds. The results depend heavily on the data. For this log, bzip2 compresses best, as its block-sorting algorithm suits text with many repeated fields, but it decompresses slowly. zstd, the newest of the tools, is the fastest at both ends at its default level, and at level 19 compresses better than xz -6 while decompressing about five times faster. gzip remains the most compatible choice. For a single file, the choice is a trade-off between size, speed, and what the recipients have installed.

$ cat compare.sh
#!/bin/bash

file=$1
size=$(stat -c %s "$file")
TIMEFORMAT=%R

printf '%-10s %10s %7s %8s %8s\n' tool bytes ratio "comp s" "decomp s"
for cmd in "gzip -6" "gzip -9" "bzip2 -9" "zstd -3" "zstd -19" "xz -6"; do
    tool=${cmd%% *}
    ctime=$( { time $cmd -c "$file" > out.tmp; } 2>&1 )
    csize=$(stat -c %s out.tmp)
    dtime=$( { time $tool -dc out.tmp > /dev/null; } 2>&1 )
    ratio=$(awk -v a=$csize -v b=$size 'BEGIN { printf "%.1f%%", 100 * a / b }')
    printf '%-10s %10d %7s %8s %8s\n' "$cmd" "$csize" "$ratio" "$ctime" "$dtime"
done
rm -f out.tmp
$ bash compare.sh access.log
tool            bytes   ratio   comp s decomp s
gzip -6       1541139   10.2%    0.200    0.043
gzip -9       1359040    9.0%    0.537    0.043
bzip2 -9       868070    5.7%    1.405    0.259
zstd -3       1921824   12.7%    0.037    0.014
zstd -19      1157314    7.6%    8.186    0.013
xz -6         1204348    8.0%    5.406    0.061

Compressed tar archives

tar calls gzip with the -z option and xz with -J. When extracting or listing, GNU tar detects the compression automatically.

tar -czf project.tar.gz project
tar -cJf project.tar.xz project
ls -l project.tar.*
tar -tf project.tar.xz
tar -cf - project | xz -9 -T0 > project-max.tar.xz
ls -l project-max.tar.xz

For options that tar does not pass on, such as levels and threads, pipe the archive through the compressor. With -9 and many threads, xz would need more memory than its default limit, which is a quarter of the RAM for threaded mode, so it reduced the number of threads by itself. For this small project, the extra effort gained nothing. See the tar command article for more.

$ tar -czf project.tar.gz project
$ tar -cJf project.tar.xz project
$ ls -l project.tar.*
-rw-r--r-- 1 jano jano 17822 Oct 23 22:00 project.tar.gz
-rw-r--r-- 1 jano jano 16452 Oct 23 22:00 project.tar.xz
$ tar -tf project.tar.xz
project/
project/LICENSE
project/docs/
project/docs/services.txt
project/src/
project/src/f3.c
project/src/f1.c
project/src/f2.c
$ tar -cf - project | xz -9 -T0 > project-max.tar.xz
xz: Reduced the number of threads from 8 to 3 to not exceed the memory usage limit of 3,947 MiB
$ ls -l project-max.tar.xz
-rw-r--r-- 1 jano jano 16460 Oct 23 22:00 project-max.tar.xz

Common errors

The following commands show typical error messages.

gzip -d gpl.txt
gunzip README
echo "plain text" > notes.gz
gunzip notes.gz; echo $?
gzip gpl.txt
gzip -f gpl.txt
ls gpl.txt*

gzip -d only accepts known suffixes such as .gz, .tgz, and .z; for others, decompress with -c to a file of our choice. Given a name without a suffix, gunzip looks for the .gz file. A file with a .gz suffix is not necessarily compressed; gzip checks the magic number at the start of the file. -f overwrites the existing compressed file.

$ gzip -d gpl.txt
gzip: gpl.txt: unknown suffix -- ignored
$ gunzip README
gzip: README.gz: No such file or directory
$ echo "plain text" > notes.gz
$ gunzip notes.gz; echo $?

gzip: notes.gz: not in gzip format
1
$ gzip gpl.txt
gzip: gpl.txt.gz already exists;	not overwritten
$ gzip -f gpl.txt
$ ls gpl.txt*
gpl.txt.gz

Summary

Taskgzipxz
Compress, replacing the filegzip filexz file
Keep the originalgzip -k filexz -k file
Decompressgunzip file.gzunxz file.xz
To standard outputgzip -c, zcatxz -c, xzcat
List sizesgzip -lxz -l
Testgzip -txz -t
Searchzgrepxzgrep
Level-1 to -9-0 to -9, -e
Threadsnone (use pigz)-T0
With tartar -czftar -cJf

Source

GNU gzip manual, XZ Utils

In this article, we have compressed and decompressed files with gzip and xz, read compressed files with zcat and zgrep, compared compression levels, threads, and tools, tested integrity, and created compressed tar archives. See also the zip command article.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Linux tutorials.