yum upgrades for production use, this is the repository for you.
Active subscription is required.
Zstandard (Zstd) compression delivers better ratios and faster speeds than gzip, making it an obvious upgrade for any performance-minded server operator. There is just one problem: not every client supports Zstd yet. Older browsers, legacy HTTP clients, monitoring probes, and many search engine crawlers still only understand gzip — or no compression at all. If your backend compresses responses with Zstd and a client without Zstd support connects, it receives garbled binary data instead of a readable page. You are stuck choosing between better compression for modern clients and compatibility for everyone else — unless you add NGINX Zstd decompression to the mix.
The NGINX unzstd module (ngx_http_unzstd_filter_module) eliminates this trade-off. It sits in the NGINX filter chain and automatically decompresses Zstd-encoded responses for clients that lack Zstd support, while passing compressed responses through untouched to clients that do support it. This lets you adopt Zstd compression unconditionally on your backends without breaking compatibility for anyone.
If you have used NGINX’s built-in gunzip module before, the unzstd module works on the same principle — but for Zstd instead of gzip.
How NGINX Zstd Decompression Works
The unzstd module operates as an HTTP output filter. When NGINX proxies a response from an upstream server, it inspects two things:
- The response: Does it carry
Content-Encoding: zstd? - The client request: Does the
Accept-Encodingheader includezstd?
If the response is Zstd-encoded and the client does not support Zstd, the module decompresses the response on the fly using the libzstd streaming API. It strips the Content-Encoding: zstd header and sends plain content to the client. If the client does support Zstd, the response passes through unmodified — no wasted CPU cycles on unnecessary decompression.
This architecture enables a powerful pattern: compress once on the backend, decompress selectively at the edge. Your upstream servers only need to produce Zstd-compressed responses, and NGINX handles client compatibility transparently. For background on how Accept-Encoding negotiation works in NGINX, see our dedicated guide.
Why Not Just Use gzip?
Zstd consistently outperforms gzip in both compression ratio and speed. In a representative test with a 12 KB JSON API response:
| Algorithm | Compressed Size | Reduction | Relative |
|---|---|---|---|
| Zstd (level 3) | 568 bytes | 95.5% | baseline |
| gzip (level 6) | 787 bytes | 93.8% | 39% larger |
Zstd achieved a 39% smaller compressed size than gzip on the same data. Moreover, Zstd decompression is significantly faster than gzip, which matters both for backend CPU usage and for client-side page load times.
The unzstd module for NGINX Zstd decompression lets you capture these benefits immediately, without waiting for universal Zstd client support.
Installation
The unzstd module is available as a pre-built dynamic module from the GetPageSpeed repository.
RHEL, CentOS, AlmaLinux, Rocky Linux
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-unzstd
Then load the module by adding the following at the top of /etc/nginx/nginx.conf, before the http {} block:
load_module modules/ngx_http_unzstd_filter_module.so;
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-unzstd
On Debian/Ubuntu, the package handles module loading automatically. No
load_moduledirective is needed.
Companion Module: Zstd Compression
For a complete Zstd pipeline, you will also want the Zstd compression module (nginx-module-zstd), which compresses responses with Zstd for clients that support it. Together, the two modules provide full Zstd support in NGINX — analogous to the built-in gzip and gunzip module pair. Similarly, the unbrotli module provides the same functionality for Brotli compression.
sudo dnf install nginx-module-zstd
Configuration
The unzstd module provides three directives. All three can be placed in the http, server, or location context.
unzstd
Syntax: unzstd on | off;
Default: unzstd off;
Context: http, server, location
Enables or disables NGINX Zstd decompression of proxied responses for clients that do not support Zstd. When enabled, the module checks the client’s Accept-Encoding header for zstd support.
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend;
proxy_set_header Accept-Encoding "zstd";
# Decompress Zstd responses for clients that lack support
unzstd on;
}
}
The proxy_set_header Accept-Encoding "zstd" line is important — it tells the backend to always send Zstd-compressed responses, regardless of what the original client requested. The unzstd module then handles decompression for clients that cannot decode Zstd.
unzstd_force
Syntax: unzstd_force string ...;
Default: —
Context: http, server, location
Forces Zstd decompression regardless of the client’s Accept-Encoding header. If at least one value in the string parameter evaluates to non-empty and not equal to “0”, decompression is performed unconditionally. The module will still only decompress responses that carry Content-Encoding: zstd — it does not attempt to decompress uncompressed responses.
This directive works the same way as predicates in other NGINX modules. You can use variables to control when forced decompression activates:
location / {
proxy_pass http://backend;
proxy_set_header Accept-Encoding "zstd";
unzstd on;
# Always decompress, even for clients that claim Zstd support
unzstd_force always;
}
A more practical example uses a variable to force decompression conditionally:
location / {
proxy_pass http://backend;
proxy_set_header Accept-Encoding "zstd";
unzstd on;
# Force decompression when a query parameter is present (useful for debugging)
unzstd_force $arg_raw;
}
With this configuration, requesting http://example.com/api/data?raw=1` forces decompression even if the client sendsAccept-Encoding: zstd`.
unzstd_buffers
Syntax: unzstd_buffers number size;
Default: unzstd_buffers 32 4k | 16 8k;
Context: http, server, location
Sets the number and size of buffers used for decompression. By default, the buffer size equals one memory page — 4 KB or 8 KB depending on the platform. The total buffer pool size is number × size, which determines how much decompressed data can be buffered before flushing to the client.
For most workloads, the defaults are sufficient. If you serve very large responses (multi-megabyte API payloads or large HTML pages), you may increase the buffer pool:
location /api/ {
proxy_pass http://backend;
proxy_set_header Accept-Encoding "zstd";
unzstd on;
unzstd_buffers 64 8k;
}
Complete Configuration Example
Here is a production-ready configuration that uses both the Zstd compression and decompression modules together:
load_module modules/ngx_http_zstd_filter_module.so;
load_module modules/ngx_http_zstd_static_module.so;
load_module modules/ngx_http_unzstd_filter_module.so;
http {
# ... other settings ...
# Upstream backend that produces Zstd-compressed responses
upstream backend {
server 127.0.0.1:8081;
}
# Backend server compressing responses with Zstd
server {
listen 8081;
root /var/www/html;
zstd on;
zstd_comp_level 3;
zstd_types text/plain text/css application/json application/javascript;
}
# Frontend proxy with Zstd decompression for compatibility
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend;
proxy_set_header Accept-Encoding "zstd";
unzstd on;
}
}
}
In this setup:
– The backend compresses all qualifying responses with Zstd
– The frontend proxy requests Zstd from the backend via proxy_set_header Accept-Encoding "zstd"
– Clients that support Zstd receive the compressed response directly
– Clients that do not support Zstd receive decompressed content, transparently
Testing Your Configuration
After configuring the module, verify that NGINX Zstd decompression works correctly with these tests.
Verify the Module Is Loaded
Test your NGINX configuration to confirm no errors:
nginx -t
If you see unknown directive "unzstd", the module is not loaded. Verify the load_module directive is present and the module file exists at /usr/lib64/nginx/modules/ngx_http_unzstd_filter_module.so.
Test Decompression Behavior
Send a request without Zstd support (simulating an old client) and verify you get readable content:
curl -s -D- http://localhost/api/data
You should see a response without Content-Encoding: zstd and with readable body content.
Then send a request with Zstd support and verify the compressed response passes through:
curl -s -D- -H "Accept-Encoding: zstd" http://localhost/api/data
You should see Content-Encoding: zstd in the response headers and binary content in the body.
Verify Content Integrity
Confirm that the decompressed response matches the original content:
# Get decompressed version (no Accept-Encoding)
curl -s http://localhost/api/data -o decompressed.txt
# Get and decompress the Zstd version manually
curl -s -H "Accept-Encoding: zstd" http://localhost/api/data | zstd -d -o original.txt
# Compare
diff decompressed.txt original.txt
If diff produces no output, the decompression is working correctly.
Performance Considerations
CPU Overhead
Zstd decompression is fast — significantly faster than gzip decompression. In benchmarks, Zstd decompresses at over 1,500 MB/s on modern hardware, roughly 3× faster than zlib. However, decompression still consumes CPU cycles. The unzstd module intelligently avoids this cost for Zstd-capable clients by passing compressed responses through untouched.
Monitor your NGINX worker CPU usage after enabling the module. If a large percentage of your traffic comes from clients that do not support Zstd, you will see increased CPU usage proportional to the volume of decompressed responses.
Memory Usage
The unzstd_buffers directive controls the decompression buffer pool. The default allocation of 32 × 4 KB = 128 KB per request is appropriate for typical web responses. Adjust only if you observe decompression-related errors in the NGINX error log or serve unusually large responses.
Network Savings
The primary benefit of using Zstd with unzstd is network efficiency. The data transfer between your backend and NGINX is compressed, reducing internal network bandwidth. Only the last mile — between NGINX and incompatible clients — carries uncompressed data. As Zstd adoption grows in browsers and HTTP clients, an increasing share of your traffic will benefit from end-to-end Zstd compression.
When to Avoid Decompression
If your backend already handles content negotiation and serves both Zstd and uncompressed responses based on client capabilities, you may not need the unzstd module. It is most valuable when:
- Your backend always serves Zstd-compressed responses
- You want to simplify backend logic by removing content negotiation
- You use pre-compressed storage (e.g., Zstd-compressed files on disk served via the
zstd_staticdirective)
Security Best Practices
Limit Decompression to Trusted Upstreams
Only enable unzstd on for locations that proxy to backends you control. If NGINX proxies to untrusted third-party servers, a malicious response with Content-Encoding: zstd containing a specially crafted decompression bomb could consume excessive memory or CPU.
# Good: decompression for a trusted internal backend
location /api/ {
proxy_pass http://internal-backend;
unzstd on;
}
# No unzstd for proxied external content
location /external/ {
proxy_pass http://third-party-api;
}
Buffer Limits
Keep unzstd_buffers at reasonable values. The default of 128 KB total is conservative. Do not set excessively large buffer sizes, as each concurrent request allocates its own buffer pool. For a server handling 1,000 concurrent decompressing requests with unzstd_buffers 256 16k, that would be 4 GB of buffer memory.
Troubleshooting
“unknown directive unzstd”
The module is not loaded. Add the load_module directive to the top of nginx.conf:
load_module modules/ngx_http_unzstd_filter_module.so;
Ensure the file exists:
ls /usr/lib64/nginx/modules/ngx_http_unzstd_filter_module.so
Responses Still Compressed for Non-Zstd Clients
Check that unzstd on; is set in the correct context (location, server, or http block). Also verify that the upstream response actually carries Content-Encoding: zstd. You can inspect the raw upstream response headers:
curl -s -D- -H "Accept-Encoding: zstd" http://your-backend:port/path
If the backend does not send Content-Encoding: zstd, the unzstd module has nothing to decompress.
Garbled Output or Decompression Errors
Check the NGINX error log for decompression failures:
tail -f /var/log/nginx/error.log
Errors like ZSTD_decompressStream() failed indicate corrupted Zstd data from the upstream. Verify that the backend produces valid Zstd-compressed output:
curl -s -H "Accept-Encoding: zstd" http://backend/path | zstd -d > /dev/null
Module Conflicts with SELinux
On RHEL-based systems with SELinux enforcing, you may see module loading failures like cannot make segment writable for relocation: Permission denied. Set the correct SELinux context for the module file:
sudo restorecon -v /usr/lib64/nginx/modules/ngx_http_unzstd_filter_module.so
If the issue persists, check the SELinux audit log:
sudo ausearch -m AVC -ts recent | grep nginx
Comparison with the Built-in gunzip Module
| Feature | gunzip (built-in) | unzstd (this module) |
|---|---|---|
| Compression format | gzip | Zstd |
| Decompression speed | ~500 MB/s | ~1,500 MB/s |
| Typical compression ratio | 70-80% | 80-95% |
| Force decompression | No dedicated directive | unzstd_force |
| Built into NGINX | Yes | No (dynamic module) |
| Buffer configuration | gunzip_buffers |
unzstd_buffers |
The unzstd module provides an additional unzstd_force directive that has no equivalent in the gunzip module, giving you more control over when decompression occurs.
Conclusion
The NGINX unzstd module enables a smooth transition to Zstd compression. By performing NGINX Zstd decompression transparently for clients that lack Zstd support, it removes the biggest barrier to adopting Zstd on your servers. Combined with the Zstd compression module, you get full Zstd support in NGINX — better compression ratios, faster decompression, and complete client compatibility. For proper Vary header handling with compressed and decompressed responses, consider the compression-vary module as well.
The module source code is available on GitHub.
