Site icon GetPageSpeed

NGINX Zstd Decompression with the unzstd Module

NGINX Zstd Decompression: Transparent Compatibility with the unzstd Module

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:

  1. The response: Does it carry Content-Encoding: zstd?
  2. The client request: Does the Accept-Encoding header include zstd?

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_module directive 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:

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.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Exit mobile version