yum upgrades for production use, this is the repository for you.
Active subscription is required.
Technical Briefing: zlib-ng vs Stock zlib Under NGINX — Measured, Not Assumed
The Problem
Vendor benchmarks for zlib-ng typically compress one big buffer in a single call against a synthetic corpus. NGINX does not work that way. It feeds the compressor in small chunks, opens a fresh deflate stream per response, and wraps output in a gzip header. The question this evaluation answers is whether zlib-ng actually beats stock zlib under NGINX’s real compression pattern — and the answer required building the benchmark rather than trusting the vendor numbers.
The Test Setup
- Libraries: zlib-ng 2.3.3 built in compat mode (
-DZLIB_COMPAT=ON, exporting the classic zlib API and thelibz.so.1SONAME) versus stock zlib 1.2.11. - Platform: Rocky Linux 9, on both x86_64 and aarch64, same host and container, back to back, with only the shared library changing.
- Payloads: a reproducible 4000-entry JSON file (generated with a one-liner
awkscript), a 1 MiBrandom.binfrom/dev/urandom, and an HTML page. - Harness parameters:
windowBits = 15 + 16(gzip wrapper, matchingContent-Encoding: gzip) andmemLevel = 8(matching the NGINX gzip filter default). - Two harnesses:
- A one-shot
deflate(Z_FINISH)loop, run for 2 seconds onCLOCK_MONOTONIC, reporting MB/s plus output size. - A chunked harness mimicking NGINX’s real pattern — 32 KB chunks with
Z_NO_FLUSH, finishing withZ_FINISH, and a fresh stream per iteration. 8 KB chunks changed nothing materially.
- A one-shot
- A/B method: because the compat build exports the same
libz.so.1SONAME, one compiled binary is reused and onlyLD_LIBRARY_PATH=/opt/zng/libchanges which library the dynamic linker loads.lddwas printed for every mode, and the same trick switched NGINX.
Results
One-shot harness: +39% to +88% at level 6. At level 9, +33% on HTML and +52% on JSON at ratio parity. The Arm gain is larger than x86_64 because zlib-ng has dedicated NEON and ARMv8 CRC32 paths while stock zlib 1.2.11 runs generic C on Arm — the bigger story on Graviton or Ampere.
Inflate matters more than expected. The gunzip filter decompresses for clients that don’t accept gzip, and proxy setups that unpack gzipped upstream responses pay inflate cost per request. The inflate-heavy gunzip path went from 7,649 to 13,731 RPS (+79%).
End-to-end NGINX test: a minimal server block with gzip on, gzip_comp_level 6, gzip_min_length 20, gzip_http_version 1.0, gzip_types text/html application/json application/octet-stream, plus a /gs/ location with gzip_static always and gunzip on holding only a pre-compressed page.html.gz. Load was ApacheBench with keep-alive, 8,000 requests at concurrency 4; worker CPU sampled with ps -o time=.
On a CPU-bound box the library speedup shows up almost directly as RPS, with JSON gaining most. On the 24-core x86_64 host with only 4 concurrent clients, gzip RPS was latency-bound and mostly flat, but CPU accounting still showed the saving (HTML worker CPU dropped from 8 s to 7 s per 8,000 requests). Incompressible data stays roughly flat, as expected.
The Benchmarking Trap
gzip_http_version 1.0 is not optional when load-testing with ab. ab sends HTTP/1.0 requests and NGINX’s default gzip_http_version is 1.1, so without the override NGINX silently skips compression, still returns 200, and you end up benchmarking plain file serving — both libraries look identical because neither is called.
Verify with:
curl -s -o /dev/null -w '%{size_download}\n' --http1.0 -H 'Accept-Encoding: gzip'
If the size equals the uncompressed file, the benchmark is measuring the wrong thing. wrk and h2load speak HTTP/1.1 or newer and don’t hit this.
Correctness Checks
In both modes on both architectures:
curl --compressedoutputcmp-identical to the original.gzip -tpasses and stockgzipCLI decompresses it.- The
gunzipmodule correctly inflates.gzfiles.
Because ldd only reports what the linker would resolve, the real proof is the running worker’s memory map:
grep libz /proc/$pid/maps | awk '{print $6}' | sort -u
This printed the zlib-ng path and nothing else in zlib-ng mode.
Production data point: this site’s own production NGINX has run on zlib-ng since April 2026, months before this evaluation, with no compression-related issues.
The One Place zlib-ng Is Not a Pure Win
At level 1 it switches to a deflate-quick strategy. Output is about 5 percentage points larger than stock zlib level 1, and on incompressible input it can expand the payload by around 5%, where stock zlib stays essentially flat.
Since gzip_comp_level defaults to 1 when unset, add gzip_comp_level 2; when installing zlib-ng. Level 2 gives a better ratio than stock level 1 and is still roughly 2–3x faster. Levels 3 and higher need no change.
EL10/Fedora Caveat
On EL10 and current Fedora the distro’s own libz.so.1 is already zlib-ng (zlib-ng-compat, version 2.2.3 on Rocky Linux 10.1). On a Rocky Linux 10.1 aarch64 VM, the distro’s 2.2.3 at level 1 produced byte-identical output to zlib-ng 2.3.3 at level 2 (21,558 bytes from a 75,838-byte HTML page), while 2.3.3 at level 1 is deflate-quick (26,581 bytes).
So on EL10 the level-2 advice is not optional: installing the newer library with gzip_comp_level unset makes gzipped responses about 23% larger. No speed numbers were published for 2.2.3 vs 2.3.3 because run-to-run noise on the laptop-hosted VM exceeded the difference.
What Is Not Affected
gzip_static and brotli_static send pre-compressed files as-is with no runtime zlib work, so the library is irrelevant on that path. zlib-ng and Brotli are not competitors — Brotli needs a module and per-site config and only reaches clients that ask for it; zlib-ng is zero-config and benefits every existing gzip on; site, including API clients that only send Accept-Encoding: gzip, plus every inflate path. Run both.
Build Recipe
Inside a stock Rocky Linux 9 container:
- Install
gcc make cmake nginx httpd-tools procps-ng gzip zlib-devel diffutils. - Fetch the zlib-ng 2.3.3 tarball.
- Configure:
cmake .. -DZLIB_COMPAT=ON -DCMAKE_BUILD_TYPE=Release -DZLIB_ENABLE_TESTS=OFF -DWITH_GTEST=OFF - Build:
make -j"$(nproc)". - Copy
libz.so*into/opt/zng/lib/.
Compile harnesses with gcc -O2 bench.c -o bench -lz and run with and without LD_LIBRARY_PATH=/opt/zng/lib.
Three Practical Gotchas
- Set
gzip_http_version 1.0when load-testing withab. - If the mount point has a
0750home directory, the NGINX worker running asnginxgets 403 for everything and you benchmark error pages —chmod 755the mount point and verify a 200 first. ps -o time=has one-second resolution, so use enough requests that CPU time lands in tens of seconds, or read/proc/<pid>/statfor finer ticks.
Installation
No custom NGINX build needed. The package installs into an isolated directory and is activated through /etc/ld.so.conf.d, so every program linking libz.so.1 — NGINX included — picks it up after a restart.
Supported: RHEL, CentOS Stream, Rocky Linux and AlmaLinux 7 to 10, Amazon Linux 2 and 2023, Fedora 42 and 43, and SLES 16, on x86_64 and aarch64.
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install zlib-ng
systemctl restart nginx
Post-Install Verification (Four Things)
ldd $(command -v nginx) | grep libzsystemctl is-active nginxgrep libz /proc/$(pgrep -f 'nginx: worker' | head -1)/maps | awk '{print $6}' | sort -ucurl -sS -o /dev/null -D- -H 'Accept-Encoding: gzip' http://localhost/ | grep -i content-encoding
This exact sequence was run on a fresh Rocky Linux 10 VM before publishing — both ldd and the worker map pointed at /usr/lib64/zlib-ng/libz.so.1, NGINX restarted cleanly, and responses came back with Content-Encoding: gzip and decompressed byte-for-byte identical.
Don’t forget gzip_comp_level 2 if your config doesn’t set a level.
FAQ Points
- zlib-ng emits standard gzip/deflate streams any zlib can decode, though output bytes differ slightly from stock zlib at the same level (compressed sizes moved by 0.1%).
- No NGINX rebuild is needed because compat keeps the
libz.so.1ABI. - On a lightly loaded server you get lower CPU use and slightly lower latency on large responses rather than more RPS.
- Brotli plus zlib-ng is the right combination, not either/or.
Read the full article: zlib-ng vs zlib in NGINX: Benchmark and Methodology
