<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en_US"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://m-compressors.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://m-compressors.github.io/" rel="alternate" type="text/html" hreflang="en_US" /><updated>2026-10-22T13:28:20+03:00</updated><id>https://m-compressors.github.io/feed.xml</id><title type="html">M-Compressors</title><subtitle>Engineering blog of M-Compressors: Brotli and Zstandard rewritten in safe Rust, byte-identical to the C reference encoders, plus NGINX modules that compress on thread pools. Benchmarks, design notes and release write-ups.</subtitle><author><name>Mikhail Panfilov</name></author><entry><title type="html">zstd on NGINX thread pools: ngx_mzstd vs zstd-nginx-module</title><link href="https://m-compressors.github.io/2026/ngx-mzstd-thread-pool-benchmarks/" rel="alternate" type="text/html" title="zstd on NGINX thread pools: ngx_mzstd vs zstd-nginx-module" /><published>2026-10-22T13:00:00+03:00</published><updated>2026-10-22T13:00:00+03:00</updated><id>https://m-compressors.github.io/2026/ngx-mzstd-thread-pool-benchmarks</id><content type="html" xml:base="https://m-compressors.github.io/2026/ngx-mzstd-thread-pool-benchmarks/"><![CDATA[<p><strong>ngx_mzstd</strong> is an NGINX module for on-the-fly Zstandard (zstd) compression,
written in Rust on top of the <a href="https://github.com/M-Compressors/mzstd">mzstd</a>
encoder. It is a drop-in replacement for
<a href="https://github.com/tokers/zstd-nginx-module">tokers/zstd-nginx-module</a>, with the
same directives, and needs no libzstd. Unlike tokers/zstd-nginx-module, it can
compress on NGINX thread pools, so that compressing a large response does not stop
the worker from serving anything else.</p>

<p>This post measures what that changes, and what it costs. One NGINX worker served
large files compressed on the fly while a stream of small uncompressed requests went
to the same worker. With tokers/zstd-nginx-module at level 19, the 99th percentile
latency of the small requests rose from 1.9 ms to 198 ms, and to 1.5 s when 8 large
requests arrived together. With ngx_mzstd on a 4-thread pool it stayed at 1.7–2.0 ms.</p>

<figure class="chart" id="zstd-latency-burst" aria-labelledby="zstd-latency-burst-title">
  <p class="chart-title" id="zstd-latency-burst-title">p99 latency of small requests while large responses are compressed</p>
  <p class="chart-sub">Large requests arrive 8 at a time. ms, logarithmic scale.</p>
  <ul class="chart-legend"><li><span class="key  c-stock"></span>tokers/zstd-nginx-module</li><li><span class="key  c-loop"></span>ngx_mzstd, event loop</li><li><span class="key  c-pool1"></span>ngx_mzstd, pool, 1 task</li><li><span class="key  c-pool4"></span>ngx_mzstd, pool, 4 tasks</li><li><span class="key ref"></span>no compression, 1.9 ms</li></ul>
  <div class="chart-body">
    <div class="chart-grid" aria-hidden="true"><span style="--x: 0.0000"></span><span style="--x: 0.2876"></span><span style="--x: 0.5752"></span><span style="--x: 0.8628"></span><span class="ref" style="--x: 0.0802"></span></div>
    <div class="chart-axis" aria-hidden="true"><span></span><span class="plot"><span class="tick first" style="--x: 0.0000">1</span><span class="tick" style="--x: 0.2876">10</span><span class="tick" style="--x: 0.5752">100</span><span class="tick" style="--x: 0.8628">1,000</span></span></div>
    <div class="chart-row"><span class="chart-label">Level 3, react-dom.development.js (1.03 MB), 178 per second</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.2739"><i title="tokers/zstd-nginx-module: 9.0 ms"></i><b>9.0</b></span><span class="bar c-loop" style="--x: 0.2414"><i title="ngx_mzstd, event loop: 6.9 ms"></i><b>6.9</b></span><span class="bar c-pool1" style="--x: 0.0685"><i title="ngx_mzstd, pool, 1 task: 1.7 ms"></i><b>1.7</b></span><span class="bar c-pool4" style="--x: 0.0755"><i title="ngx_mzstd, pool, 4 tasks: 1.8 ms"></i><b>1.8</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 19, react-dom.development.js (1.03 MB), 2 per second</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.9159"><i title="tokers/zstd-nginx-module: 1,530 ms"></i><b>1,530</b></span><span class="bar c-loop" style="--x: 0.8856"><i title="ngx_mzstd, event loop: 1,200 ms"></i><b>1,200</b></span><span class="bar c-pool1" style="--x: 0.0834"><i title="ngx_mzstd, pool, 1 task: 1.9 ms"></i><b>1.9</b></span><span class="bar c-pool4" style="--x: 0.0847"><i title="ngx_mzstd, pool, 4 tasks: 2.0 ms"></i><b>2.0</b></span></span></div>
  </div>
  <figcaption>Small uncompressed requests (1.9 KB, 1,000 per second) on the same worker as the large compressed ones. wrk2 with coordinated omission corrected, median of three 30-second runs. NGINX 1.30.6, one worker on 4 CPUs, a pool of 4 threads; 20 October 2026.</figcaption>
</figure>

<p>The numbers come from a reduced run under WSL2 on 20 October 2026. They are good for
comparing the modules with each other, less so as absolute figures for your hardware.
<a href="#how-it-was-measured">How it was measured</a> has the details and what was not
measured.</p>

<h2 id="why-compression-delays-other-requests">Why compression delays other requests</h2>

<p>An NGINX worker process serves all of its connections from one thread, in an event
loop that runs a short piece of work for each ready socket. A filter that runs for
200 ms holds every other connection of that worker for 200 ms.</p>

<p>tokers/zstd-nginx-module compresses in the event loop, as the stock gzip module
does. zstd at level 3 is fast: 2.8 ms of CPU time for a 1 MB JavaScript bundle. At
level 19 the same file takes about 200 ms. A small request that arrives on that
worker meanwhile waits for compression to end. When 8 large requests arrive together,
the worker compresses them one after another, and the small request waits about
8 × 200 ms.</p>

<p>NGINX has had thread pools since version 1.7.11, released on 24 March 2015
(<a href="https://nginx.org/en/CHANGES">CHANGES</a>), and uses them for blocking file I/O
(<code class="language-plaintext highlighter-rouge">aio threads</code>). ngx_mzstd uses them for compression.</p>

<h2 id="how-ngx_mzstd-compresses-on-a-thread-pool">How ngx_mzstd compresses on a thread pool</h2>

<p>With <code class="language-plaintext highlighter-rouge">zstd_thread_pool</code>, the module cuts a response into segments of
<code class="language-plaintext highlighter-rouge">zstd_parallel_segment_size</code>, 1 MiB by default. Each segment becomes an NGINX thread
task and is compressed on one of the pool’s threads; up to <code class="language-plaintext highlighter-rouge">zstd_parallel_tasks</code>
segments of one response, 4 by default, are compressed at once. The completion
handler runs in the event loop, sends the compressed bytes in order and wakes the
connection. Meanwhile the event loop serves other connections. The response is still
one standard zstd frame.</p>

<p>Unlike Brotli segments in <a href="/2026/ngx-mbrotli-thread-pool-benchmarks/">ngx_mbrotli</a>,
zstd segments are not fully independent. Every segment but the first starts by
running through the end of the input before it, so that its matches can reach back across the
boundary. That keeps the compression ratio close to the event loop’s, and it costs
CPU time, as shown <a href="#where-the-extra-cpu-time-goes">below</a>.</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">load_module</span> <span class="nc">modules/ngx</span><span class="s">_mzstd.so</span><span class="p">;</span>

<span class="k">thread_pool</span> <span class="s">zstd</span> <span class="s">threads=4</span><span class="p">;</span>

<span class="k">http</span> <span class="p">{</span>
    <span class="kn">zstd</span> <span class="no">on</span><span class="p">;</span>
    <span class="kn">zstd_comp_level</span> <span class="mi">3</span><span class="p">;</span>
    <span class="kn">zstd_types</span> <span class="nc">text/plain</span> <span class="nc">text/css</span> <span class="nc">application/javascript</span> <span class="nc">application/json</span>
               <span class="nc">image/svg</span><span class="s">+xml</span><span class="p">;</span>

    <span class="kn">zstd_thread_pool</span> <span class="s">zstd</span><span class="p">;</span>
    <span class="kn">zstd_parallel_segment_size</span> <span class="mi">4m</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>NGINX must be built with <code class="language-plaintext highlighter-rouge">--with-threads</code>, as nginx.org’s packages are. The pool
named <code class="language-plaintext highlighter-rouge">default</code> exists without being declared, with 32 threads per worker, and is
shared with <code class="language-plaintext highlighter-rouge">aio threads</code>. The reason for <code class="language-plaintext highlighter-rouge">4m</code> is in
<a href="#how-to-configure-it">How to configure it</a>.</p>

<h2 id="how-long-do-other-requests-wait">How long do other requests wait?</h2>

<p>Small uncompressed requests (<code class="language-plaintext highlighter-rouge">small.json</code>, 1.9 KB) were sent at 1,000 per second over
32 connections. A second stream requested <code class="language-plaintext highlighter-rouge">react-dom.development.js</code> (1.03 MB)
compressed, at a constant rate of about half of what tokers/zstd-nginx-module can
sustain. All variants kept up with both streams.</p>

<figure class="chart" id="zstd-latency-even" aria-labelledby="zstd-latency-even-title">
  <p class="chart-title" id="zstd-latency-even-title">p99 latency of small requests, large responses one at a time</p>
  <p class="chart-sub">Large requests arrive on one connection at a constant interval. ms, logarithmic scale.</p>
  <ul class="chart-legend"><li><span class="key  c-stock"></span>tokers/zstd-nginx-module</li><li><span class="key  c-loop"></span>ngx_mzstd, event loop</li><li><span class="key  c-pool1"></span>ngx_mzstd, pool, 1 task</li><li><span class="key  c-pool4"></span>ngx_mzstd, pool, 4 tasks</li><li><span class="key ref"></span>no compression, 1.9 ms</li></ul>
  <div class="chart-body">
    <div class="chart-grid" aria-hidden="true"><span style="--x: 0.0000"></span><span style="--x: 0.2876"></span><span style="--x: 0.5752"></span><span style="--x: 0.8628"></span><span class="ref" style="--x: 0.0802"></span></div>
    <div class="chart-axis" aria-hidden="true"><span></span><span class="plot"><span class="tick first" style="--x: 0.0000">1</span><span class="tick" style="--x: 0.2876">10</span><span class="tick" style="--x: 0.5752">100</span><span class="tick" style="--x: 0.8628">1,000</span></span></div>
    <div class="chart-row"><span class="chart-label">Level 3, react-dom.development.js (1.03 MB), 178 per second</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.1792"><i title="tokers/zstd-nginx-module: 4.2 ms"></i><b>4.2</b></span><span class="bar c-loop" style="--x: 0.1667"><i title="ngx_mzstd, event loop: 3.8 ms"></i><b>3.8</b></span><span class="bar c-pool1" style="--x: 0.0734"><i title="ngx_mzstd, pool, 1 task: 1.8 ms"></i><b>1.8</b></span><span class="bar c-pool4" style="--x: 0.0663"><i title="ngx_mzstd, pool, 4 tasks: 1.7 ms"></i><b>1.7</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 19, react-dom.development.js (1.03 MB), 2 per second</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.6605"><i title="tokers/zstd-nginx-module: 198 ms"></i><b>198</b></span><span class="bar c-loop" style="--x: 0.6299"><i title="ngx_mzstd, event loop: 155 ms"></i><b>155</b></span><span class="bar c-pool1" style="--x: 0.0734"><i title="ngx_mzstd, pool, 1 task: 1.8 ms"></i><b>1.8</b></span><span class="bar c-pool4" style="--x: 0.0802"><i title="ngx_mzstd, pool, 4 tasks: 1.9 ms"></i><b>1.9</b></span></span></div>
  </div>
  <figcaption>The same test as the chart at the top, with one 30-second run. NGINX 1.30.6, one worker on 4 CPUs, a pool of 4 threads; 20 October 2026.</figcaption>
</figure>

<details class="numbers">
  <summary>Numbers as a table</summary>

  <table>
    <thead>
      <tr>
        <th>Large responses</th>
        <th>Arrival</th>
        <th style="text-align: right">No compression</th>
        <th style="text-align: right">tokers/zstd</th>
        <th style="text-align: right">ngx_mzstd, event loop</th>
        <th style="text-align: right">Pool, 1 task</th>
        <th style="text-align: right">Pool, 4 tasks</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Level 3, 178 per second</td>
        <td>8 at once</td>
        <td style="text-align: right">1.9</td>
        <td style="text-align: right">9.0</td>
        <td style="text-align: right">6.9</td>
        <td style="text-align: right">1.7</td>
        <td style="text-align: right">1.8</td>
      </tr>
      <tr>
        <td>Level 3, 178 per second</td>
        <td>One at a time</td>
        <td style="text-align: right">1.9</td>
        <td style="text-align: right">4.2</td>
        <td style="text-align: right">3.8</td>
        <td style="text-align: right">1.8</td>
        <td style="text-align: right">1.7</td>
      </tr>
      <tr>
        <td>Level 19, 2 per second</td>
        <td>8 at once</td>
        <td style="text-align: right">1.9</td>
        <td style="text-align: right">1,530</td>
        <td style="text-align: right">1,200</td>
        <td style="text-align: right">2.0</td>
        <td style="text-align: right">2.0</td>
      </tr>
      <tr>
        <td>Level 19, 2 per second</td>
        <td>One at a time</td>
        <td style="text-align: right">1.9</td>
        <td style="text-align: right">198</td>
        <td style="text-align: right">155</td>
        <td style="text-align: right">1.8</td>
        <td style="text-align: right">1.9</td>
      </tr>
    </tbody>
  </table>

</details>

<p>“8 at once” is the large stream’s 8 connections, which wrk2 starts together and which
stay in phase, as when several clients ask for large files at the same moment. “One at
a time” is a single connection at a constant interval, the lower bound of the effect.
On the pool, no small request took more than 3.6 ms one at a time.</p>

<p>Without a thread pool, ngx_mzstd lowers p99 by 10–22% against
tokers/zstd-nginx-module, only because mzstd compresses faster. It blocks the event
loop all the same. The difference in tail latency comes from the thread pool.</p>

<p>The large responses gained too. With 8 arriving together at level 19, each took a
median 319 ms with 4 tasks, 465 ms with 1 task and 789 ms with
tokers/zstd-nginx-module.</p>

<h2 id="how-many-requests-does-one-worker-serve">How many requests does one worker serve?</h2>

<p>The same worker under closed-loop load, with wrk and 8 connections:</p>

<figure class="chart" id="zstd-rps" aria-labelledby="zstd-rps-title">
  <p class="chart-title" id="zstd-rps-title">Requests per second, relative to tokers/zstd-nginx-module</p>
  <p class="chart-sub">8 connections. 1 task per response serves about the same as 4. Logarithmic scale.</p>
  <ul class="chart-legend"><li><span class="key round c-loop"></span>ngx_mzstd, event loop</li><li><span class="key round c-pool4"></span>ngx_mzstd, pool, 4 tasks</li><li><span class="key ref"></span>tokers/zstd-nginx-module = 1×</li></ul>
  <div class="chart-body">
    <div class="chart-grid" aria-hidden="true"><span style="--x: 0.0000"></span><span style="--x: 0.3758"></span><span style="--x: 0.5637"></span><span style="--x: 0.7516"></span><span style="--x: 0.9395"></span><span class="ref" style="--x: 0.1879"></span></div>
    <div class="chart-axis" aria-hidden="true"><span></span><span class="plot"><span class="tick first" style="--x: 0.0000">0.5×</span><span class="tick" style="--x: 0.1879">1×</span><span class="tick" style="--x: 0.3758">2×</span><span class="tick" style="--x: 0.5637">4×</span><span class="tick" style="--x: 0.7516">8×</span><span class="tick" style="--x: 0.9395">16×</span></span></div>
    <p class="chart-group">jquery.min.js, 85 KB</p>
    <div class="chart-row"><span class="chart-label">Level 3</span><span class="plot dots"><span class="span" style="--a: 0.2350; --b: 0.5399"></span><span class="dot c-loop" style="--x: 0.2350" title="ngx_mzstd, event loop: 1.2×"></span><span class="dot c-pool4" style="--x: 0.5399" title="ngx_mzstd, pool, 4 tasks: 3.7×"></span><b class="dot-value" style="--x: 0.5399">3.7×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot dots"><span class="span" style="--a: 0.1792; --b: 0.5396"></span><span class="dot c-loop" style="--x: 0.1792" title="ngx_mzstd, event loop: 0.97×"></span><span class="dot c-pool4" style="--x: 0.5396" title="ngx_mzstd, pool, 4 tasks: 3.7×"></span><b class="dot-value" style="--x: 0.5396">3.7×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 15</span><span class="plot dots"><span class="span" style="--a: 0.5603; --b: 0.9239"></span><span class="dot c-loop" style="--x: 0.5603" title="ngx_mzstd, event loop: 4.0×"></span><span class="dot c-pool4" style="--x: 0.9239" title="ngx_mzstd, pool, 4 tasks: 15.1×"></span><b class="dot-value" style="--x: 0.9239">15.1×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 19</span><span class="plot dots"><span class="span" style="--a: 0.4556; --b: 0.8214"></span><span class="dot c-loop" style="--x: 0.4556" title="ngx_mzstd, event loop: 2.7×"></span><span class="dot c-pool4" style="--x: 0.8214" title="ngx_mzstd, pool, 4 tasks: 10.3×"></span><b class="dot-value" style="--x: 0.8214">10.3×</b></span></div>
    <p class="chart-group">react-dom.development.js, 1.03 MB</p>
    <div class="chart-row"><span class="chart-label">Level 3</span><span class="plot dots"><span class="span" style="--a: 0.2277; --b: 0.5335"></span><span class="dot c-loop" style="--x: 0.2277" title="ngx_mzstd, event loop: 1.2×"></span><span class="dot c-pool4" style="--x: 0.5335" title="ngx_mzstd, pool, 4 tasks: 3.6×"></span><b class="dot-value" style="--x: 0.5335">3.6×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot dots"><span class="span" style="--a: 0.1865; --b: 0.4553"></span><span class="dot c-loop" style="--x: 0.1865" title="ngx_mzstd, event loop: 0.99×"></span><span class="dot c-pool4" style="--x: 0.4553" title="ngx_mzstd, pool, 4 tasks: 2.7×"></span><b class="dot-value" style="--x: 0.4553">2.7×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 15</span><span class="plot dots"><span class="span" style="--a: 0.2634; --b: 0.5517"></span><span class="dot c-loop" style="--x: 0.2634" title="ngx_mzstd, event loop: 1.3×"></span><span class="dot c-pool4" style="--x: 0.5517" title="ngx_mzstd, pool, 4 tasks: 3.8×"></span><b class="dot-value" style="--x: 0.5517">3.8×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 19</span><span class="plot dots"><span class="span" style="--a: 0.2562; --b: 0.5096"></span><span class="dot c-loop" style="--x: 0.2562" title="ngx_mzstd, event loop: 1.3×"></span><span class="dot c-pool4" style="--x: 0.5096" title="ngx_mzstd, pool, 4 tasks: 3.3×"></span><b class="dot-value" style="--x: 0.5096">3.3×</b></span></div>
    <p class="chart-group">libraries.json, 2.2 MB</p>
    <div class="chart-row"><span class="chart-label">Level 3</span><span class="plot dots"><span class="span" style="--a: 0.2316; --b: 0.5486"></span><span class="dot c-loop" style="--x: 0.2316" title="ngx_mzstd, event loop: 1.2×"></span><span class="dot c-pool4" style="--x: 0.5486" title="ngx_mzstd, pool, 4 tasks: 3.8×"></span><b class="dot-value" style="--x: 0.5486">3.8×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot dots"><span class="span" style="--a: 0.1914; --b: 0.4159"></span><span class="dot c-loop" style="--x: 0.1914" title="ngx_mzstd, event loop: 1.0×"></span><span class="dot c-pool4" style="--x: 0.4159" title="ngx_mzstd, pool, 4 tasks: 2.3×"></span><b class="dot-value" style="--x: 0.4159">2.3×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 15</span><span class="plot dots"><span class="span" style="--a: 0.2241; --b: 0.4054"></span><span class="dot c-loop" style="--x: 0.2241" title="ngx_mzstd, event loop: 1.1×"></span><span class="dot c-pool4" style="--x: 0.4054" title="ngx_mzstd, pool, 4 tasks: 2.2×"></span><b class="dot-value" style="--x: 0.4054">2.2×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 19</span><span class="plot dots"><span class="span" style="--a: 0.2473; --b: 0.4541"></span><span class="dot c-loop" style="--x: 0.2473" title="ngx_mzstd, event loop: 1.2×"></span><span class="dot c-pool4" style="--x: 0.4541" title="ngx_mzstd, pool, 4 tasks: 2.7×"></span><b class="dot-value" style="--x: 0.4541">2.7×</b></span></div>
  </div>
  <figcaption>wrk, median of three runs. NGINX 1.30.6, one worker on 4 CPUs, a pool of 4 threads; 20 October 2026.</figcaption>
</figure>

<details class="numbers">
  <summary>Numbers as a table</summary>

  <table>
    <thead>
      <tr>
        <th>Response</th>
        <th style="text-align: right">Level</th>
        <th style="text-align: right">tokers/zstd</th>
        <th style="text-align: right">ngx_mzstd, event loop</th>
        <th style="text-align: right">Difference</th>
        <th style="text-align: right">Pool, 1 task</th>
        <th style="text-align: right">Difference</th>
        <th style="text-align: right">Pool, 4 tasks</th>
        <th style="text-align: right">Difference</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>jquery.min.js, 85 KB</td>
        <td style="text-align: right">3</td>
        <td style="text-align: right">3,469</td>
        <td style="text-align: right">4,127</td>
        <td style="text-align: right">+19%</td>
        <td style="text-align: right">12,850</td>
        <td style="text-align: right">3.7×</td>
        <td style="text-align: right">12,710</td>
        <td style="text-align: right">3.7×</td>
      </tr>
      <tr>
        <td>jquery.min.js, 85 KB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">841</td>
        <td style="text-align: right">814</td>
        <td style="text-align: right">−3%</td>
        <td style="text-align: right">3,162</td>
        <td style="text-align: right">3.8×</td>
        <td style="text-align: right">3,077</td>
        <td style="text-align: right">3.7×</td>
      </tr>
      <tr>
        <td>jquery.min.js, 85 KB</td>
        <td style="text-align: right">15</td>
        <td style="text-align: right">50.7</td>
        <td style="text-align: right">200</td>
        <td style="text-align: right">4.0×</td>
        <td style="text-align: right">764</td>
        <td style="text-align: right">15.1×</td>
        <td style="text-align: right">765</td>
        <td style="text-align: right">15.1×</td>
      </tr>
      <tr>
        <td>jquery.min.js, 85 KB</td>
        <td style="text-align: right">19</td>
        <td style="text-align: right">31.3</td>
        <td style="text-align: right">83.9</td>
        <td style="text-align: right">2.7×</td>
        <td style="text-align: right">323</td>
        <td style="text-align: right">10.3×</td>
        <td style="text-align: right">324</td>
        <td style="text-align: right">10.3×</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">3</td>
        <td style="text-align: right">357</td>
        <td style="text-align: right">414</td>
        <td style="text-align: right">+16%</td>
        <td style="text-align: right">1,283</td>
        <td style="text-align: right">3.6×</td>
        <td style="text-align: right">1,278</td>
        <td style="text-align: right">3.6×</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">91.1</td>
        <td style="text-align: right">90.6</td>
        <td style="text-align: right">−1%</td>
        <td style="text-align: right">251</td>
        <td style="text-align: right">2.8×</td>
        <td style="text-align: right">244</td>
        <td style="text-align: right">2.7×</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">15</td>
        <td style="text-align: right">12.0</td>
        <td style="text-align: right">15.8</td>
        <td style="text-align: right">+32%</td>
        <td style="text-align: right">47.7</td>
        <td style="text-align: right">4.0×</td>
        <td style="text-align: right">45.9</td>
        <td style="text-align: right">3.8×</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">19</td>
        <td style="text-align: right">4.99</td>
        <td style="text-align: right">6.42</td>
        <td style="text-align: right">+29%</td>
        <td style="text-align: right">16.5</td>
        <td style="text-align: right">3.3×</td>
        <td style="text-align: right">16.4</td>
        <td style="text-align: right">3.3×</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">3</td>
        <td style="text-align: right">174</td>
        <td style="text-align: right">204</td>
        <td style="text-align: right">+17%</td>
        <td style="text-align: right">656</td>
        <td style="text-align: right">3.8×</td>
        <td style="text-align: right">658</td>
        <td style="text-align: right">3.8×</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">48.0</td>
        <td style="text-align: right">48.6</td>
        <td style="text-align: right">+1%</td>
        <td style="text-align: right">112</td>
        <td style="text-align: right">2.3×</td>
        <td style="text-align: right">111</td>
        <td style="text-align: right">2.3×</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">15</td>
        <td style="text-align: right">6.99</td>
        <td style="text-align: right">7.99</td>
        <td style="text-align: right">+14%</td>
        <td style="text-align: right">18.7</td>
        <td style="text-align: right">2.7×</td>
        <td style="text-align: right">15.6</td>
        <td style="text-align: right">2.2×</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">19</td>
        <td style="text-align: right">2.45</td>
        <td style="text-align: right">3.05</td>
        <td style="text-align: right">+24%</td>
        <td style="text-align: right">6.54</td>
        <td style="text-align: right">2.7×</td>
        <td style="text-align: right">6.54</td>
        <td style="text-align: right">2.7×</td>
      </tr>
    </tbody>
  </table>

</details>

<p>The pool’s 4 threads use the 4 CPUs that one worker in the event loop cannot. On the
1–2.2 MB files at levels 9–19 the pool gains only 2.1–3.0× over the event loop, because of the extra CPU
time explained below.</p>

<p><strong>Small responses at high levels.</strong> On <code class="language-plaintext highlighter-rouge">small.json</code> (1.9 KB) at levels 15 and 19,
tokers/zstd-nginx-module served 46–58 requests per second, spending 18–22 ms of CPU
time and up to 85 MiB on each response. ngx_mzstd served 5,100–9,200 in the event
loop and 17,000–29,000 on the pool. tokers/zstd-nginx-module probably sizes its
encoder for the level’s full window because it does not pass the response length to
zstd; that was not verified.</p>

<h2 id="how-fast-is-one-large-response">How fast is one large response?</h2>

<p>Time to serve one <code class="language-plaintext highlighter-rouge">libraries.json</code> (2.2 MB, three 1 MiB segments) over one
connection:</p>

<figure class="chart" id="zstd-single" aria-labelledby="zstd-single-title">
  <p class="chart-title" id="zstd-single-title">Time to serve one 2.2 MB response</p>
  <p class="chart-sub">libraries.json, three 1 MiB segments, one connection. ms, logarithmic scale.</p>
  <ul class="chart-legend"><li><span class="key  c-stock"></span>tokers/zstd-nginx-module</li><li><span class="key  c-loop"></span>ngx_mzstd, event loop</li><li><span class="key  c-pool1"></span>ngx_mzstd, pool, 1 task</li><li><span class="key  c-pool4"></span>ngx_mzstd, pool, 4 tasks</li></ul>
  <div class="chart-body">
    <div class="chart-grid" aria-hidden="true"><span style="--x: 0.0000"></span><span style="--x: 0.2876"></span><span style="--x: 0.5752"></span><span style="--x: 0.8628"></span></div>
    <div class="chart-axis" aria-hidden="true"><span></span><span class="plot"><span class="tick first" style="--x: 0.0000">1</span><span class="tick" style="--x: 0.2876">10</span><span class="tick" style="--x: 0.5752">100</span><span class="tick" style="--x: 0.8628">1,000</span></span></div>
    <div class="chart-row"><span class="chart-label">Level 3</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.2196"><i title="tokers/zstd-nginx-module: 5.8 ms"></i><b>5.8</b></span><span class="bar c-loop" style="--x: 0.1985"><i title="ngx_mzstd, event loop: 4.9 ms"></i><b>4.9</b></span><span class="bar c-pool1" style="--x: 0.2152"><i title="ngx_mzstd, pool, 1 task: 5.6 ms"></i><b>5.6</b></span><span class="bar c-pool4" style="--x: 0.1529"><i title="ngx_mzstd, pool, 4 tasks: 3.4 ms"></i><b>3.4</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.3785"><i title="tokers/zstd-nginx-module: 20.7 ms"></i><b>20.7</b></span><span class="bar c-loop" style="--x: 0.3803"><i title="ngx_mzstd, event loop: 21.0 ms"></i><b>21.0</b></span><span class="bar c-pool1" style="--x: 0.4293"><i title="ngx_mzstd, pool, 1 task: 31.1 ms"></i><b>31.1</b></span><span class="bar c-pool4" style="--x: 0.3568"><i title="ngx_mzstd, pool, 4 tasks: 17.4 ms"></i><b>17.4</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 15</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.6199"><i title="tokers/zstd-nginx-module: 143 ms"></i><b>143</b></span><span class="bar c-loop" style="--x: 0.6000"><i title="ngx_mzstd, event loop: 122 ms"></i><b>122</b></span><span class="bar c-pool1" style="--x: 0.6291"><i title="ngx_mzstd, pool, 1 task: 154 ms"></i><b>154</b></span><span class="bar c-pool4" style="--x: 0.5578"><i title="ngx_mzstd, pool, 4 tasks: 87 ms"></i><b>87</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 19</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.7535"><i title="tokers/zstd-nginx-module: 417 ms"></i><b>417</b></span><span class="bar c-loop" style="--x: 0.7258"><i title="ngx_mzstd, event loop: 334 ms"></i><b>334</b></span><span class="bar c-pool1" style="--x: 0.7897"><i title="ngx_mzstd, pool, 1 task: 557 ms"></i><b>557</b></span><span class="bar c-pool4" style="--x: 0.6901"><i title="ngx_mzstd, pool, 4 tasks: 251 ms"></i><b>251</b></span></span></div>
  </div>
  <figcaption>wrk, median of three runs. NGINX 1.30.6, one worker on 4 CPUs, a pool of 4 threads; 20 October 2026.</figcaption>
</figure>

<details class="numbers">
  <summary>Numbers as a table</summary>

  <table>
    <thead>
      <tr>
        <th style="text-align: right">Level</th>
        <th style="text-align: right">tokers/zstd</th>
        <th style="text-align: right">ngx_mzstd, event loop</th>
        <th style="text-align: right">Pool, 1 task</th>
        <th style="text-align: right">Pool, 4 tasks</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td style="text-align: right">3</td>
        <td style="text-align: right">5.8</td>
        <td style="text-align: right">4.9</td>
        <td style="text-align: right">5.6</td>
        <td style="text-align: right">3.4</td>
      </tr>
      <tr>
        <td style="text-align: right">9</td>
        <td style="text-align: right">20.7</td>
        <td style="text-align: right">21.0</td>
        <td style="text-align: right">31.1</td>
        <td style="text-align: right">17.4</td>
      </tr>
      <tr>
        <td style="text-align: right">15</td>
        <td style="text-align: right">143</td>
        <td style="text-align: right">122</td>
        <td style="text-align: right">154</td>
        <td style="text-align: right">87</td>
      </tr>
      <tr>
        <td style="text-align: right">19</td>
        <td style="text-align: right">417</td>
        <td style="text-align: right">334</td>
        <td style="text-align: right">557</td>
        <td style="text-align: right">251</td>
      </tr>
    </tbody>
  </table>

</details>

<p>4 tasks served it 1.6–2.2 times as fast as 1 task, and 1.2–1.7 times as fast as
tokers/zstd-nginx-module. With 1 task, the pool was 26–67% slower than the event loop
at levels 9–19: that is the segment overlap. Responses up to 1 MiB fit in one segment,
so they do not depend on the number of tasks.</p>

<h2 id="where-the-extra-cpu-time-goes">Where the extra CPU time goes</h2>

<figure class="chart" id="zstd-cpu" aria-labelledby="zstd-cpu-title">
  <p class="chart-title" id="zstd-cpu-title">CPU time per request</p>
  <p class="chart-sub">Worker and pool threads together, 8 connections. ms, logarithmic scale.</p>
  <ul class="chart-legend"><li><span class="key  c-stock"></span>tokers/zstd-nginx-module</li><li><span class="key  c-loop"></span>ngx_mzstd, event loop</li><li><span class="key  c-pool1"></span>ngx_mzstd, pool, 1 task</li><li><span class="key  c-pool4"></span>ngx_mzstd, pool, 4 tasks</li></ul>
  <div class="chart-body">
    <div class="chart-grid" aria-hidden="true"><span style="--x: 0.0000"></span><span style="--x: 0.2876"></span><span style="--x: 0.5752"></span><span style="--x: 0.8628"></span></div>
    <div class="chart-axis" aria-hidden="true"><span></span><span class="plot"><span class="tick first" style="--x: 0.0000">1</span><span class="tick" style="--x: 0.2876">10</span><span class="tick" style="--x: 0.5752">100</span><span class="tick" style="--x: 0.8628">1,000</span></span></div>
    <p class="chart-group">react-dom.development.js, 1.03 MB</p>
    <div class="chart-row"><span class="chart-label">Level 3</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.1286"><i title="tokers/zstd-nginx-module: 2.8 ms"></i><b>2.8</b></span><span class="bar c-loop" style="--x: 0.1093"><i title="ngx_mzstd, event loop: 2.4 ms"></i><b>2.4</b></span><span class="bar c-pool1" style="--x: 0.1372"><i title="ngx_mzstd, pool, 1 task: 3.0 ms"></i><b>3.0</b></span><span class="bar c-pool4" style="--x: 0.1413"><i title="ngx_mzstd, pool, 4 tasks: 3.1 ms"></i><b>3.1</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.2995"><i title="tokers/zstd-nginx-module: 11.0 ms"></i><b>11.0</b></span><span class="bar c-loop" style="--x: 0.3006"><i title="ngx_mzstd, event loop: 11.1 ms"></i><b>11.1</b></span><span class="bar c-pool1" style="--x: 0.3455"><i title="ngx_mzstd, pool, 1 task: 15.9 ms"></i><b>15.9</b></span><span class="bar c-pool4" style="--x: 0.3494"><i title="ngx_mzstd, pool, 4 tasks: 16.4 ms"></i><b>16.4</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 19</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.6624"><i title="tokers/zstd-nginx-module: 201 ms"></i><b>201</b></span><span class="bar c-loop" style="--x: 0.6315"><i title="ngx_mzstd, event loop: 157 ms"></i><b>157</b></span><span class="bar c-pool1" style="--x: 0.6861"><i title="ngx_mzstd, pool, 1 task: 243 ms"></i><b>243</b></span><span class="bar c-pool4" style="--x: 0.6881"><i title="ngx_mzstd, pool, 4 tasks: 247 ms"></i><b>247</b></span></span></div>
    <p class="chart-group">libraries.json, 2.2 MB</p>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.3791"><i title="tokers/zstd-nginx-module: 20.8 ms"></i><b>20.8</b></span><span class="bar c-loop" style="--x: 0.3779"><i title="ngx_mzstd, event loop: 20.6 ms"></i><b>20.6</b></span><span class="bar c-pool1" style="--x: 0.4458"><i title="ngx_mzstd, pool, 1 task: 35.5 ms"></i><b>35.5</b></span><span class="bar c-pool4" style="--x: 0.4479"><i title="ngx_mzstd, pool, 4 tasks: 36.1 ms"></i><b>36.1</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 15</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.6216"><i title="tokers/zstd-nginx-module: 145 ms"></i><b>145</b></span><span class="bar c-loop" style="--x: 0.6041"><i title="ngx_mzstd, event loop: 126 ms"></i><b>126</b></span><span class="bar c-pool1" style="--x: 0.6714"><i title="ngx_mzstd, pool, 1 task: 216 ms"></i><b>216</b></span><span class="bar c-pool4" style="--x: 0.6979"><i title="ngx_mzstd, pool, 4 tasks: 267 ms"></i><b>267</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 19</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.7517"><i title="tokers/zstd-nginx-module: 411 ms"></i><b>411</b></span><span class="bar c-loop" style="--x: 0.7247"><i title="ngx_mzstd, event loop: 331 ms"></i><b>331</b></span><span class="bar c-pool1" style="--x: 0.8023"><i title="ngx_mzstd, pool, 1 task: 616 ms"></i><b>616</b></span><span class="bar c-pool4" style="--x: 0.8041"><i title="ngx_mzstd, pool, 4 tasks: 625 ms"></i><b>625</b></span></span></div>
  </div>
  <figcaption>Median of three runs. NGINX 1.30.6, one worker on 4 CPUs, a pool of 4 threads; 20 October 2026.</figcaption>
</figure>

<details class="numbers">
  <summary>Numbers as a table</summary>

  <table>
    <thead>
      <tr>
        <th>Response</th>
        <th style="text-align: right">Level</th>
        <th style="text-align: right">tokers/zstd</th>
        <th style="text-align: right">ngx_mzstd, event loop</th>
        <th style="text-align: right">Pool, 1 task</th>
        <th style="text-align: right">Pool, 4 tasks</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">3</td>
        <td style="text-align: right">2.8</td>
        <td style="text-align: right">2.4</td>
        <td style="text-align: right">3.0</td>
        <td style="text-align: right">3.1</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">11.0</td>
        <td style="text-align: right">11.1</td>
        <td style="text-align: right">15.9</td>
        <td style="text-align: right">16.4</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">19</td>
        <td style="text-align: right">201</td>
        <td style="text-align: right">157</td>
        <td style="text-align: right">243</td>
        <td style="text-align: right">247</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">20.8</td>
        <td style="text-align: right">20.6</td>
        <td style="text-align: right">35.5</td>
        <td style="text-align: right">36.1</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">15</td>
        <td style="text-align: right">145</td>
        <td style="text-align: right">126</td>
        <td style="text-align: right">216</td>
        <td style="text-align: right">267</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">19</td>
        <td style="text-align: right">411</td>
        <td style="text-align: right">331</td>
        <td style="text-align: right">616</td>
        <td style="text-align: right">625</td>
      </tr>
    </tbody>
  </table>

</details>

<p>At levels 9–19 the pool needed 1.4–2.1 times the CPU time of the event loop, and in
most cases more than tokers/zstd-nginx-module. Two causes were measured.</p>

<p><strong>Segment overlap.</strong> Before compressing its own input, every segment but the first
compresses and throws away the end of the previous segment, so that its matches can
reach back. At levels that use the lazy match finders, that is 1/8 of the window: on
<code class="language-plaintext highlighter-rouge">libraries.json</code>, 512 KiB for every 1 MiB segment. At levels 19 and above, a segment
loads the whole window as a prefix. Measured in the library alone, on one thread, with
1 MiB segments, <code class="language-plaintext highlighter-rouge">libraries.json</code> took 1.21–1.69 times the CPU time of the event loop
at levels 9–19, and 0.79–0.98 times without the overlap.</p>

<details class="numbers">
  <summary>The library measurements as a table</summary>

  <table>
    <thead>
      <tr>
        <th>Measurement</th>
        <th style="text-align: right">Level 9</th>
        <th style="text-align: right">Level 15</th>
        <th style="text-align: right">Level 19</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>ngx_mzstd, event loop, 8 connections</td>
        <td style="text-align: right">20.6</td>
        <td style="text-align: right">126</td>
        <td style="text-align: right">331</td>
      </tr>
      <tr>
        <td>mzstd, 1 MiB segments</td>
        <td style="text-align: right">30.1 (×1.45)</td>
        <td style="text-align: right">156 (×1.21)</td>
        <td style="text-align: right">559 (×1.69)</td>
      </tr>
      <tr>
        <td>mzstd, 1 MiB segments without overlap</td>
        <td style="text-align: right">20.3 (×0.98)</td>
        <td style="text-align: right">101 (×0.79)</td>
        <td style="text-align: right">315 (×0.95)</td>
      </tr>
      <tr>
        <td>ngx_mzstd, pool, 1 task, 1 connection</td>
        <td style="text-align: right">31.4</td>
        <td style="text-align: right">156</td>
        <td style="text-align: right">558</td>
      </tr>
      <tr>
        <td>ngx_mzstd, pool, 1 task, 8 connections</td>
        <td style="text-align: right">35.5</td>
        <td style="text-align: right">216</td>
        <td style="text-align: right">616</td>
      </tr>
    </tbody>
  </table>

  <p>CPU time per <code class="language-plaintext highlighter-rouge">libraries.json</code> response, in ms.</p>

</details>

<p>Without overlap, compressing in segments costs no more than compressing in one piece.
At 1 connection, ngx_mzstd on the pool costs what the library costs, within 0.1×, so
handing segments to the pool costs almost nothing. The overlap buys compression
ratio: without it, the output would be 2.5–6.2% larger instead of 0.04–1.4%.</p>

<p><strong>Encoders allocated anew.</strong> A worker keeps encoders for <code class="language-plaintext highlighter-rouge">zstd_parallel_compressors</code>
compressors, 4 by default. With 8 responses in flight, the others allocate their
tables for every response. At level 15, that showed up as 14,000 page faults and
23 ms of system time per response with 1 task, and 35,000 and 64 ms with 4 tasks.
<code class="language-plaintext highlighter-rouge">zstd_parallel_compressors 8</code> lowered CPU time per request from 211 to 185 ms with
1 task and from 272 to 185 ms with 4 tasks, and served 14% and 46% more requests.</p>

<p>About 30 ms per response at level 15 remains unexplained (185 ms against 156 ms at
1 connection). It is not threads competing for cache: a pool of one thread needed the
same 181 ms.</p>

<h2 id="what-the-thread-pool-costs">What the thread pool costs</h2>

<p><strong>Memory.</strong> On a pool, every response being sent holds encoders for its segments
until it is sent, so peak memory grows with the number of responses in flight and
the number of tasks per response.</p>

<figure class="chart" id="zstd-memory" aria-labelledby="zstd-memory-title">
  <p class="chart-title" id="zstd-memory-title">Peak memory of the worker</p>
  <p class="chart-sub">VmHWM at 8 connections, MiB, logarithmic scale. The labelled value is the pool with 4 tasks.</p>
  <ul class="chart-legend"><li><span class="key round c-stock"></span>tokers/zstd-nginx-module</li><li><span class="key round c-loop"></span>ngx_mzstd, event loop</li><li><span class="key round c-pool1"></span>ngx_mzstd, pool, 1 task</li><li><span class="key round c-pool4"></span>ngx_mzstd, pool, 4 tasks</li></ul>
  <div class="chart-body">
    <div class="chart-grid" aria-hidden="true"><span style="--x: 0.0000"></span><span style="--x: 0.2222"></span><span style="--x: 0.4444"></span><span style="--x: 0.6667"></span><span style="--x: 0.8889"></span></div>
    <div class="chart-axis" aria-hidden="true"><span></span><span class="plot"><span class="tick first" style="--x: 0.0000">4</span><span class="tick" style="--x: 0.2222">16</span><span class="tick" style="--x: 0.4444">64</span><span class="tick" style="--x: 0.6667">256</span><span class="tick" style="--x: 0.8889">1,024</span></span></div>
    <p class="chart-group">react-dom.development.js, 1.03 MB</p>
    <div class="chart-row"><span class="chart-label">Level 3</span><span class="plot dots"><span class="span" style="--a: 0.0650; --b: 0.4494"></span><span class="dot c-stock" style="--x: 0.0650" title="tokers/zstd-nginx-module: 6 MiB"></span><span class="dot c-loop" style="--x: 0.0897" title="ngx_mzstd, event loop: 7 MiB"></span><span class="dot c-pool1" style="--x: 0.4112" title="ngx_mzstd, pool, 1 task: 52 MiB"></span><span class="dot c-pool4" style="--x: 0.4494" title="ngx_mzstd, pool, 4 tasks: 66 MiB"></span><b class="dot-value" style="--x: 0.4494">66 MiB</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot dots"><span class="span" style="--a: 0.2222; --b: 0.6438"></span><span class="dot c-stock" style="--x: 0.2222" title="tokers/zstd-nginx-module: 16 MiB"></span><span class="dot c-loop" style="--x: 0.2222" title="ngx_mzstd, event loop: 16 MiB"></span><span class="dot c-pool1" style="--x: 0.5842" title="ngx_mzstd, pool, 1 task: 153 MiB"></span><span class="dot c-pool4" style="--x: 0.6438" title="ngx_mzstd, pool, 4 tasks: 222 MiB"></span><b class="dot-value" style="--x: 0.6438">222 MiB</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 15</span><span class="plot dots"><span class="span" style="--a: 0.3609; --b: 0.7958"></span><span class="dot c-stock" style="--x: 0.4588" title="tokers/zstd-nginx-module: 70 MiB"></span><span class="dot c-loop" style="--x: 0.3609" title="ngx_mzstd, event loop: 38 MiB"></span><span class="dot c-pool1" style="--x: 0.7456" title="ngx_mzstd, pool, 1 task: 419 MiB"></span><span class="dot c-pool4" style="--x: 0.7958" title="ngx_mzstd, pool, 4 tasks: 573 MiB"></span><b class="dot-value" style="--x: 0.7958">573 MiB</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 19</span><span class="plot dots"><span class="span" style="--a: 0.3650; --b: 0.7927"></span><span class="dot c-stock" style="--x: 0.4918" title="tokers/zstd-nginx-module: 86 MiB"></span><span class="dot c-loop" style="--x: 0.3650" title="ngx_mzstd, event loop: 39 MiB"></span><span class="dot c-pool1" style="--x: 0.7691" title="ngx_mzstd, pool, 1 task: 485 MiB"></span><span class="dot c-pool4" style="--x: 0.7927" title="ngx_mzstd, pool, 4 tasks: 562 MiB"></span><b class="dot-value" style="--x: 0.7927">562 MiB</b></span></div>
    <p class="chart-group">libraries.json, 2.2 MB</p>
    <div class="chart-row"><span class="chart-label">Level 3</span><span class="plot dots"><span class="span" style="--a: 0.0897; --b: 0.4973"></span><span class="dot c-stock" style="--x: 0.0897" title="tokers/zstd-nginx-module: 7 MiB"></span><span class="dot c-loop" style="--x: 0.1111" title="ngx_mzstd, event loop: 8 MiB"></span><span class="dot c-pool1" style="--x: 0.4419" title="ngx_mzstd, pool, 1 task: 63 MiB"></span><span class="dot c-pool4" style="--x: 0.4973" title="ngx_mzstd, pool, 4 tasks: 89 MiB"></span><b class="dot-value" style="--x: 0.4973">89 MiB</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot dots"><span class="span" style="--a: 0.2319; --b: 0.6942"></span><span class="dot c-stock" style="--x: 0.2319" title="tokers/zstd-nginx-module: 17 MiB"></span><span class="dot c-loop" style="--x: 0.2319" title="ngx_mzstd, event loop: 17 MiB"></span><span class="dot c-pool1" style="--x: 0.6120" title="ngx_mzstd, pool, 1 task: 182 MiB"></span><span class="dot c-pool4" style="--x: 0.6942" title="ngx_mzstd, pool, 4 tasks: 304 MiB"></span><b class="dot-value" style="--x: 0.6942">304 MiB</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 15</span><span class="plot dots"><span class="span" style="--a: 0.4588; --b: 0.9031"></span><span class="dot c-stock" style="--x: 0.4588" title="tokers/zstd-nginx-module: 70 MiB"></span><span class="dot c-loop" style="--x: 0.4611" title="ngx_mzstd, event loop: 71 MiB"></span><span class="dot c-pool1" style="--x: 0.7815" title="ngx_mzstd, pool, 1 task: 524 MiB"></span><span class="dot c-pool4" style="--x: 0.9031" title="ngx_mzstd, pool, 4 tasks: 1,119 MiB"></span><b class="dot-value" style="--x: 0.9031">1,119 MiB</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 19</span><span class="plot dots"><span class="span" style="--a: 0.4230; --b: 0.8787"></span><span class="dot c-stock" style="--x: 0.4937" title="tokers/zstd-nginx-module: 87 MiB"></span><span class="dot c-loop" style="--x: 0.4230" title="ngx_mzstd, event loop: 56 MiB"></span><span class="dot c-pool1" style="--x: 0.7781" title="ngx_mzstd, pool, 1 task: 513 MiB"></span><span class="dot c-pool4" style="--x: 0.8787" title="ngx_mzstd, pool, 4 tasks: 961 MiB"></span><b class="dot-value" style="--x: 0.8787">961 MiB</b></span></div>
  </div>
  <figcaption>Highest of three runs. NGINX 1.30.6, one worker on 4 CPUs, a pool of 4 threads; 20 October 2026.</figcaption>
</figure>

<details class="numbers">
  <summary>Numbers as a table</summary>

  <table>
    <thead>
      <tr>
        <th>Response</th>
        <th style="text-align: right">Level</th>
        <th style="text-align: right">tokers/zstd</th>
        <th style="text-align: right">ngx_mzstd, event loop</th>
        <th style="text-align: right">Pool, 1 task</th>
        <th style="text-align: right">Pool, 4 tasks</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">3</td>
        <td style="text-align: right">6</td>
        <td style="text-align: right">7</td>
        <td style="text-align: right">52</td>
        <td style="text-align: right">66</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">16</td>
        <td style="text-align: right">16</td>
        <td style="text-align: right">153</td>
        <td style="text-align: right">222</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">15</td>
        <td style="text-align: right">70</td>
        <td style="text-align: right">38</td>
        <td style="text-align: right">419</td>
        <td style="text-align: right">573</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">19</td>
        <td style="text-align: right">86</td>
        <td style="text-align: right">39</td>
        <td style="text-align: right">485</td>
        <td style="text-align: right">562</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">3</td>
        <td style="text-align: right">7</td>
        <td style="text-align: right">8</td>
        <td style="text-align: right">63</td>
        <td style="text-align: right">89</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">17</td>
        <td style="text-align: right">17</td>
        <td style="text-align: right">182</td>
        <td style="text-align: right">304</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">15</td>
        <td style="text-align: right">70</td>
        <td style="text-align: right">71</td>
        <td style="text-align: right">524</td>
        <td style="text-align: right">1,119</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">19</td>
        <td style="text-align: right">87</td>
        <td style="text-align: right">56</td>
        <td style="text-align: right">513</td>
        <td style="text-align: right">961</td>
      </tr>
    </tbody>
  </table>

</details>

<p>In the event loop, ngx_mzstd needs about as much as tokers/zstd-nginx-module, and less
at levels 15 and 19. On the pool it needs 6–18 times as much, up to 1.1 GiB.</p>

<p><strong>Compression ratio.</strong> With 1m segments, responses longer than one segment came out
0–1.3% larger than in the event loop. With 4m, the files here were the same size.</p>

<h2 id="how-to-configure-it">How to configure it</h2>

<ul>
  <li><strong>Set <code class="language-plaintext highlighter-rouge">zstd_parallel_segment_size 4m</code>.</strong> Both files here then fit in one segment,
so a response is compressed by one task with no overlap. With 1 task at
8 connections, CPU time per request was 1.04–1.21 times the event loop’s instead of
1.20–1.97, a worker served 15–69% more requests, peak memory was up to 31% lower and
the output was as small as in the event loop. One response at a time was served
within 6% of the event loop’s time. The cost: only responses longer than 4 MiB are
split across threads.</li>
</ul>

<figure class="chart" id="zstd-segment" aria-labelledby="zstd-segment-title">
  <p class="chart-title" id="zstd-segment-title">CPU time per request on the pool, relative to the event loop</p>
  <p class="chart-sub">Pool with 1 task, 8 connections.</p>
  <ul class="chart-legend"><li><span class="key round ring c-pool1"></span>1m segments (default)</li><li><span class="key round c-pool1"></span>4m segments</li><li><span class="key ref"></span>event loop = 1×</li></ul>
  <div class="chart-body">
    <div class="chart-grid" aria-hidden="true"><span style="--x: 0.5625"></span><span style="--x: 0.8750"></span><span class="ref" style="--x: 0.2500"></span></div>
    <div class="chart-axis" aria-hidden="true"><span></span><span class="plot"><span class="tick" style="--x: 0.2500">1.0×</span><span class="tick" style="--x: 0.5625">1.5×</span><span class="tick" style="--x: 0.8750">2.0×</span></span></div>
    <p class="chart-group">react-dom.development.js, 1.03 MB</p>
    <div class="chart-row"><span class="chart-label">Level 3</span><span class="plot dots"><span class="span" style="--a: 0.2760; --b: 0.4062"></span><span class="dot ring c-pool1" style="--x: 0.4062" title="1m segments: 1.25×"></span><span class="dot c-pool1" style="--x: 0.2760" title="4m segments: 1.04×"></span><b class="dot-value" style="--x: 0.4062">1.25×</b><b class="dot-value left" style="--x: 0.2760">1.04×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot dots"><span class="span" style="--a: 0.3288; --b: 0.5203"></span><span class="dot ring c-pool1" style="--x: 0.5203" title="1m segments: 1.43×"></span><span class="dot c-pool1" style="--x: 0.3288" title="4m segments: 1.13×"></span><b class="dot-value" style="--x: 0.5203">1.43×</b><b class="dot-value left" style="--x: 0.3288">1.13×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 15</span><span class="plot dots"><span class="span" style="--a: 0.3822; --b: 0.5076"></span><span class="dot ring c-pool1" style="--x: 0.5076" title="1m segments: 1.41×"></span><span class="dot c-pool1" style="--x: 0.3822" title="4m segments: 1.21×"></span><b class="dot-value" style="--x: 0.5076">1.41×</b><b class="dot-value left" style="--x: 0.3822">1.21×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 19</span><span class="plot dots"><span class="span" style="--a: 0.3018; --b: 0.6242"></span><span class="dot ring c-pool1" style="--x: 0.6242" title="1m segments: 1.60×"></span><span class="dot c-pool1" style="--x: 0.3018" title="4m segments: 1.08×"></span><b class="dot-value" style="--x: 0.6242">1.60×</b><b class="dot-value left" style="--x: 0.3018">1.08×</b></span></div>
    <p class="chart-group">libraries.json, 2.2 MB</p>
    <div class="chart-row"><span class="chart-label">Level 3</span><span class="plot dots"><span class="span" style="--a: 0.2755; --b: 0.3776"></span><span class="dot ring c-pool1" style="--x: 0.3776" title="1m segments: 1.20×"></span><span class="dot c-pool1" style="--x: 0.2755" title="4m segments: 1.04×"></span><b class="dot-value" style="--x: 0.3776">1.20×</b><b class="dot-value left" style="--x: 0.2755">1.04×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot dots"><span class="span" style="--a: 0.3501; --b: 0.7172"></span><span class="dot ring c-pool1" style="--x: 0.7172" title="1m segments: 1.75×"></span><span class="dot c-pool1" style="--x: 0.3501" title="4m segments: 1.16×"></span><b class="dot-value" style="--x: 0.7172">1.75×</b><b class="dot-value left" style="--x: 0.3501">1.16×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 15</span><span class="plot dots"><span class="span" style="--a: 0.5972; --b: 0.7262"></span><span class="dot ring c-pool1" style="--x: 0.7262" title="1m segments: 1.76×"></span><span class="dot c-pool1" style="--x: 0.5972" title="4m segments: 1.56×"></span><b class="dot-value" style="--x: 0.7262">1.76×</b><b class="dot-value left" style="--x: 0.5972">1.56×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 19</span><span class="plot dots"><span class="span" style="--a: 0.3520; --b: 0.8580"></span><span class="dot ring c-pool1" style="--x: 0.8580" title="1m segments: 1.97×"></span><span class="dot c-pool1" style="--x: 0.3520" title="4m segments: 1.16×"></span><b class="dot-value" style="--x: 0.8580">1.97×</b><b class="dot-value left" style="--x: 0.3520">1.16×</b></span></div>
  </div>
  <figcaption>Median of three runs. NGINX 1.30.6, one worker on 4 CPUs, a pool of 4 threads; 20 October 2026.</figcaption>
</figure>

<details class="numbers">
  <summary>Numbers as a table</summary>

  <table>
    <thead>
      <tr>
        <th>Response</th>
        <th style="text-align: right">Level</th>
        <th style="text-align: right">CPU, event loop</th>
        <th style="text-align: right">CPU, 1m</th>
        <th style="text-align: right">CPU, 4m</th>
        <th style="text-align: right">Peak memory, 1m</th>
        <th style="text-align: right">Peak memory, 4m</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">3</td>
        <td style="text-align: right">2.4 ms</td>
        <td style="text-align: right">3.0 ms</td>
        <td style="text-align: right">2.5 ms</td>
        <td style="text-align: right">55 MiB</td>
        <td style="text-align: right">43 MiB</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">11.1 ms</td>
        <td style="text-align: right">15.9 ms</td>
        <td style="text-align: right">12.5 ms</td>
        <td style="text-align: right">171 MiB</td>
        <td style="text-align: right">139 MiB</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">15</td>
        <td style="text-align: right">63.8 ms</td>
        <td style="text-align: right">90.1 ms</td>
        <td style="text-align: right">77.3 ms</td>
        <td style="text-align: right">423 MiB</td>
        <td style="text-align: right">312 MiB</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">19</td>
        <td style="text-align: right">157 ms</td>
        <td style="text-align: right">251 ms</td>
        <td style="text-align: right">170 ms</td>
        <td style="text-align: right">456 MiB</td>
        <td style="text-align: right">314 MiB</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">3</td>
        <td style="text-align: right">4.9 ms</td>
        <td style="text-align: right">5.9 ms</td>
        <td style="text-align: right">5.1 ms</td>
        <td style="text-align: right">59 MiB</td>
        <td style="text-align: right">60 MiB</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">20.6 ms</td>
        <td style="text-align: right">36.0 ms</td>
        <td style="text-align: right">23.9 ms</td>
        <td style="text-align: right">177 MiB</td>
        <td style="text-align: right">161 MiB</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">15</td>
        <td style="text-align: right">126 ms</td>
        <td style="text-align: right">222 ms</td>
        <td style="text-align: right">196 ms</td>
        <td style="text-align: right">527 MiB</td>
        <td style="text-align: right">492 MiB</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">19</td>
        <td style="text-align: right">331 ms</td>
        <td style="text-align: right">653 ms</td>
        <td style="text-align: right">385 ms</td>
        <td style="text-align: right">505 MiB</td>
        <td style="text-align: right">414 MiB</td>
      </tr>
    </tbody>
  </table>

</details>

<p>Pool with 1 task, 8 connections. Level 15 on the 2.2 MB file stays at 1.56 times the
event loop’s CPU time, because of the encoders allocated anew.</p>

<ul>
  <li><strong>Raise <code class="language-plaintext highlighter-rouge">zstd_parallel_compressors</code></strong> to about the number of responses a worker
compresses at once. With 8 connections, <code class="language-plaintext highlighter-rouge">8</code> instead of the default 4 cut CPU time at
level 15 by 12–32%. What it does to peak memory was not measured.</li>
  <li><strong>Declare a pool of your own</strong> with <code class="language-plaintext highlighter-rouge">thread_pool zstd threads=N;</code>. The benchmark
used 4 threads on 4 CPUs; pool sizes were not compared.</li>
  <li><strong>Lower <code class="language-plaintext highlighter-rouge">zstd_parallel_tasks</code> to save memory.</strong> On the 2.2 MB file, 4 tasks needed up
to 2.1 times the memory of 1, and under load served no more requests.</li>
  <li><strong>Bound encoder memory</strong> between responses with `zstd_retention_policy bounded
    <size>` or `release_all`.

</size>
  </li>
</ul>
<p>The <a href="https://github.com/M-Compressors/ngx_mzstd#configuration-directives">ngx_mzstd README</a>
documents every directive.</p>

<h2 id="when-you-do-not-need-it">When you do not need it</h2>

<ul>
  <li><strong>Static files</strong> are better compressed once, at build time, and served with
<code class="language-plaintext highlighter-rouge">zstd_static on;</code>.</li>
  <li><strong>Low levels on small responses.</strong> At level 3 a 1 MB response took 2.8 ms of CPU
time with tokers/zstd-nginx-module, and other requests waited up to 4.2–9.0 ms at
p99. If that is acceptable, the pool mostly adds memory.</li>
  <li><strong>A CDN that compresses</strong> for you takes the work off NGINX.</li>
</ul>

<h2 id="how-it-was-measured">How it was measured</h2>

<p><strong>Hardware.</strong> Intel Core i7-13700KF, Windows 10.0.26300.9711, WSL 2.7.14.0 with kernel
6.18.33.2-microsoft-standard-WSL2, Ubuntu 22.04.5. WSL2 shows 12 identical cores with
2 threads each and does not tell which are performance cores, so some of the pinned
CPUs may be efficiency cores. Traffic went over loopback without TLS, and files were
served from tmpfs (<code class="language-plaintext highlighter-rouge">/dev/shm</code>).</p>

<p><strong>Build.</strong> NGINX 1.30.6, <code class="language-plaintext highlighter-rouge">./configure --with-compat --with-threads
--without-http_rewrite_module</code>, gcc 11.4.0, both modules dynamic.
tokers/zstd-nginx-module at commit <code class="language-plaintext highlighter-rouge">057a7d3</code> with libzstd 1.5.7. ngx_mzstd 0.2.2
(commit <code class="language-plaintext highlighter-rouge">ccd2bcf</code>) with mzstd 0.2.7, built with rustc 1.99.0. The current release is
ngx_mzstd 0.2.3; the run has not been repeated on it.</p>

<p><strong>Configuration.</strong> <code class="language-plaintext highlighter-rouge">worker_processes 1</code>, with the worker and its pool pinned to 4
physical cores, and <code class="language-plaintext highlighter-rouge">thread_pool cp threads=4</code>. On the pool, <code class="language-plaintext highlighter-rouge">zstd_thread_pool cp</code>
with <code class="language-plaintext highlighter-rouge">zstd_parallel_tasks 1</code> or the default 4. Everything else was left at its
default, including 1m segments, <code class="language-plaintext highlighter-rouge">zstd_parallel_compressors 4</code> and encoder reuse,
except where a section says otherwise.</p>

<p><strong>Load.</strong> Requests per second and CPU time: wrk 4.1.0 on 4 other physical cores, 2
threads and 8 connections, 4 seconds per run (8 seconds for files over 1 MB, 5 times
as long at level 19) after a 1-second warm-up. NGINX was restarted for every run, and
each run started once the worker had been idle for 300 ms. Three runs, in an order
shuffled between runs; tables give the median. CPU time is the worker’s user and
system time from <code class="language-plaintext highlighter-rouge">/proc/&lt;pid&gt;/stat</code>, including the pool’s threads. Latency: two wrk2
streams (commit <code class="language-plaintext highlighter-rouge">44a94c1</code>) at once, open loop, with coordinated omission corrected;
8 at once is the median of three 30-second runs, one at a time is a single 30-second
run. The library measurements ran on one core, the median of 5–7 iterations.</p>

<p><strong>Files.</strong> jQuery 3.7.1 (<code class="language-plaintext highlighter-rouge">jquery.min.js</code>, 85 KB) and React DOM 18.3.1
(<code class="language-plaintext highlighter-rouge">react-dom.development.js</code>, 1.03 MB) from cdnjs, the full cdnjs API listing
(<code class="language-plaintext highlighter-rouge">libraries.json</code>, 2.2 MB) and its first 10 entries (<code class="language-plaintext highlighter-rouge">small.json</code>, 1.9 KB), all
fetched on 20 October 2026.</p>

<p><strong>Correctness.</strong> Before the runs, every response in every combination of module,
variant, level and file was decoded with curl’s zstd decoder and compared with the
original by SHA-256. All matched.</p>

<p><strong>Not measured.</strong> gzip, more than one worker process, pool sizes other than 4
threads, a response-size threshold for the pool, HTTP/2 and HTTP/3, slow clients,
long runs, responses longer than 4 MiB and bare-metal Linux. Runs were 3 per cell and
4–40 seconds long, shorter than a full benchmark.</p>

<h2 id="install-it">Install it</h2>

<p>ngx_mzstd has packages for Debian, Ubuntu, RHEL and its derivatives, and Amazon Linux:
<a href="/2026/install-ngx-mzstd-packages/">install it from packages</a>, or
<a href="/2026/build-ngx-mzstd-from-source/">build it from source</a>. Existing
tokers/zstd-nginx-module configuration keeps working; add <code class="language-plaintext highlighter-rouge">zstd_thread_pool</code> to move
compression off the event loop.</p>

<p>The same measurements for Brotli, with ngx_mbrotli against google/ngx_brotli, are in
<a href="/2026/ngx-mbrotli-thread-pool-benchmarks/">Brotli on NGINX thread pools</a>.</p>]]></content><author><name>Mikhail Panfilov</name></author><category term="nginx" /><category term="zstd" /><category term="zstandard" /><category term="thread-pool" /><category term="latency" /><category term="benchmarks" /><category term="performance" /><summary type="html"><![CDATA[On-the-fly zstd blocks the NGINX event loop. ngx_mzstd on a thread pool against tokers/zstd-nginx-module: p99 latency, requests per second, CPU time and memory.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://m-compressors.github.io/assets/img/og/ngx-mzstd-thread-pool-benchmarks.png" /><media:content medium="image" url="https://m-compressors.github.io/assets/img/og/ngx-mzstd-thread-pool-benchmarks.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Brotli on NGINX thread pools: ngx_mbrotli vs ngx_brotli</title><link href="https://m-compressors.github.io/2026/ngx-mbrotli-thread-pool-benchmarks/" rel="alternate" type="text/html" title="Brotli on NGINX thread pools: ngx_mbrotli vs ngx_brotli" /><published>2026-10-22T12:50:00+03:00</published><updated>2026-10-22T12:50:00+03:00</updated><id>https://m-compressors.github.io/2026/ngx-mbrotli-thread-pool-benchmarks</id><content type="html" xml:base="https://m-compressors.github.io/2026/ngx-mbrotli-thread-pool-benchmarks/"><![CDATA[<p><strong>ngx_mbrotli</strong> is an NGINX module for on-the-fly Brotli compression, written in
Rust on top of the <a href="https://github.com/M-Compressors/mbrotli">mbrotli</a> encoder. It
is a drop-in replacement for <a href="https://github.com/google/ngx_brotli">google/ngx_brotli</a>,
with the same directives, defaults and <code class="language-plaintext highlighter-rouge">$brotli_ratio</code> variable. Unlike
google/ngx_brotli, it can compress on NGINX thread pools, so that compressing a large
response does not stop the worker from serving anything else.</p>

<p>This post measures what that changes. One NGINX worker served large files compressed
on the fly while a second stream of small uncompressed requests went to the same
worker. With google/ngx_brotli, the 99th percentile latency of the small requests
rose from 1.9 ms to as much as 514 ms. With ngx_mbrotli on a 4-thread pool it stayed
at 1.7–3.3 ms.</p>

<figure class="chart" id="br-latency-burst" aria-labelledby="br-latency-burst-title">
  <p class="chart-title" id="br-latency-burst-title">p99 latency of small requests while large responses are compressed</p>
  <p class="chart-sub">Large requests arrive 8 at a time. ms, logarithmic scale.</p>
  <ul class="chart-legend"><li><span class="key  c-stock"></span>google/ngx_brotli</li><li><span class="key  c-loop"></span>ngx_mbrotli, event loop</li><li><span class="key  c-pool1"></span>ngx_mbrotli, pool, 1 task</li><li><span class="key  c-pool4"></span>ngx_mbrotli, pool, 4 tasks</li><li><span class="key ref"></span>no compression, 1.9 ms</li></ul>
  <div class="chart-body">
    <div class="chart-grid" aria-hidden="true"><span style="--x: 0.0000"></span><span style="--x: 0.1590"></span><span style="--x: 0.3333"></span><span style="--x: 0.4924"></span><span style="--x: 0.6667"></span><span style="--x: 0.8257"></span><span style="--x: 1.0000"></span><span class="ref" style="--x: 0.0929"></span></div>
    <div class="chart-axis" aria-hidden="true"><span></span><span class="plot"><span class="tick first" style="--x: 0.0000">1</span><span class="tick" style="--x: 0.1590">3</span><span class="tick" style="--x: 0.3333">10</span><span class="tick" style="--x: 0.4924">30</span><span class="tick" style="--x: 0.6667">100</span><span class="tick" style="--x: 0.8257">300</span><span class="tick" style="--x: 1.0000">1000</span></span></div>
    <div class="chart-row"><span class="chart-label">Level 6, react-dom.development.js (1.03 MB), 31 per second</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.6863"><i title="google/ngx_brotli: 115 ms"></i><b>115</b></span><span class="bar c-loop" style="--x: 0.6576"><i title="ngx_mbrotli, event loop: 94.0 ms"></i><b>94.0</b></span><span class="bar c-pool1" style="--x: 0.1737"><i title="ngx_mbrotli, pool, 1 task: 3.3 ms"></i><b>3.3</b></span><span class="bar c-pool4" style="--x: 0.1464"><i title="ngx_mbrotli, pool, 4 tasks: 2.8 ms"></i><b>2.8</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 11, jquery.min.js (85 KB), 7 per second</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.9037"><i title="google/ngx_brotli: 514 ms"></i><b>514</b></span><span class="bar c-loop" style="--x: 0.8284"><i title="ngx_mbrotli, event loop: 306 ms"></i><b>306</b></span><span class="bar c-pool1" style="--x: 0.1003"><i title="ngx_mbrotli, pool, 1 task: 2.0 ms"></i><b>2.0</b></span><span class="bar c-pool4" style="--x: 0.0929"><i title="ngx_mbrotli, pool, 4 tasks: 1.9 ms"></i><b>1.9</b></span></span></div>
  </div>
  <figcaption>Small uncompressed requests (1.9 KB, 1,000 per second) on the same worker as the large compressed ones. wrk2 with coordinated omission corrected, median of three 30-second runs. NGINX 1.30.6, one worker on 4 CPUs, a pool of 4 threads; 20 October 2026.</figcaption>
</figure>

<p>The numbers come from a reduced run under WSL2 on 20 October 2026. They are good for
comparing the modules with each other, less so as absolute figures for your hardware.
<a href="#how-it-was-measured">How it was measured</a> has the details and what was not
measured.</p>

<h2 id="why-compression-delays-other-requests">Why compression delays other requests</h2>

<p>An NGINX worker process serves thousands of connections from one thread. Its event
loop waits for sockets to become ready, runs a short piece of work for each one, and
goes back to waiting. That works because each piece of work is short. A filter that
runs for 100 ms holds every other connection of that worker for 100 ms.</p>

<p>Compression is such a filter. google/ngx_brotli and the stock gzip module compress
in the event loop. When a 1 MB JavaScript bundle is compressed at Brotli level 6,
it takes about 16 ms of CPU time, and at level 11 about 1.2 s. A small request that
arrives on the same worker meanwhile waits for compression to end. When several
large responses arrive at once, the worker compresses them back to back, and the
small request waits for all of them.</p>

<p>NGINX has had a way out since version 1.7.11, released on 24 March 2015 with
“experimental thread pools support” (<a href="https://nginx.org/en/CHANGES">CHANGES</a>).
NGINX itself uses thread pools for blocking file I/O (<code class="language-plaintext highlighter-rouge">aio threads</code>). ngx_mbrotli
uses them for compression.</p>

<h2 id="how-ngx_mbrotli-compresses-on-a-thread-pool">How ngx_mbrotli compresses on a thread pool</h2>

<p>With <code class="language-plaintext highlighter-rouge">brotli_thread_pool</code>, the module cuts a response into segments of
<code class="language-plaintext highlighter-rouge">brotli_parallel_segment_size</code>, 1 MiB by default. Each segment becomes an NGINX
thread task (<code class="language-plaintext highlighter-rouge">ngx_thread_task_t</code>) and is compressed on one of the pool’s threads.
Up to <code class="language-plaintext highlighter-rouge">brotli_parallel_tasks</code> segments of one response, 4 by default, are
compressed at once. A response that fits in one segment is compressed by one
thread, in one piece.</p>

<p>When a task is done, its completion handler runs in the event loop. It passes the
compressed bytes down the filter chain in order and wakes the connection, as a write
event would. While tasks are in flight, the request stays marked as blocked and
buffered, so NGINX neither finalizes it early nor frees memory a thread is still
using. Meanwhile the event loop serves other connections.</p>

<p>Segments are compressed independently: Brotli back-references do not cross segment
boundaries. That makes segments safe to compress in parallel. It also makes a
response longer than one segment compress slightly worse, as the numbers below
show.</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">load_module</span> <span class="nc">modules/ngx</span><span class="s">_mbrotli.so</span><span class="p">;</span>

<span class="k">thread_pool</span> <span class="s">brotli</span> <span class="s">threads=4</span><span class="p">;</span>

<span class="k">http</span> <span class="p">{</span>
    <span class="kn">brotli</span> <span class="no">on</span><span class="p">;</span>
    <span class="kn">brotli_comp_level</span> <span class="mi">6</span><span class="p">;</span>
    <span class="kn">brotli_types</span> <span class="nc">text/plain</span> <span class="nc">text/css</span> <span class="nc">application/javascript</span> <span class="nc">application/json</span>
                 <span class="nc">image/svg</span><span class="s">+xml</span><span class="p">;</span>

    <span class="kn">brotli_thread_pool</span> <span class="s">brotli</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>NGINX must be built with <code class="language-plaintext highlighter-rouge">--with-threads</code>; <code class="language-plaintext highlighter-rouge">nginx -V</code> shows it. nginx.org’s
packages are. The pool named <code class="language-plaintext highlighter-rouge">default</code> exists without being declared, with 32
threads per worker, and is shared with <code class="language-plaintext highlighter-rouge">aio threads</code>.</p>

<h2 id="how-long-do-other-requests-wait">How long do other requests wait?</h2>

<p>Small uncompressed requests (<code class="language-plaintext highlighter-rouge">small.json</code>, 1.9 KB) were sent at 1,000 per second
over 32 connections. At the same time, a second stream requested large compressed
files at a constant rate of about half of what google/ngx_brotli can sustain. All
variants kept up with both streams.</p>

<figure class="chart" id="br-latency-even" aria-labelledby="br-latency-even-title">
  <p class="chart-title" id="br-latency-even-title">p99 latency of small requests, large responses one at a time</p>
  <p class="chart-sub">Large requests arrive on one connection at a constant interval. ms, logarithmic scale.</p>
  <ul class="chart-legend"><li><span class="key  c-stock"></span>google/ngx_brotli</li><li><span class="key  c-loop"></span>ngx_mbrotli, event loop</li><li><span class="key  c-pool1"></span>ngx_mbrotli, pool, 1 task</li><li><span class="key  c-pool4"></span>ngx_mbrotli, pool, 4 tasks</li><li><span class="key ref"></span>no compression, 1.9 ms</li></ul>
  <div class="chart-body">
    <div class="chart-grid" aria-hidden="true"><span style="--x: 0.0000"></span><span style="--x: 0.1590"></span><span style="--x: 0.3333"></span><span style="--x: 0.4924"></span><span style="--x: 0.6667"></span><span style="--x: 0.8257"></span><span style="--x: 1.0000"></span><span class="ref" style="--x: 0.0929"></span></div>
    <div class="chart-axis" aria-hidden="true"><span></span><span class="plot"><span class="tick first" style="--x: 0.0000">1</span><span class="tick" style="--x: 0.1590">3</span><span class="tick" style="--x: 0.3333">10</span><span class="tick" style="--x: 0.4924">30</span><span class="tick" style="--x: 0.6667">100</span><span class="tick" style="--x: 0.8257">300</span><span class="tick" style="--x: 1.0000">1000</span></span></div>
    <div class="chart-row"><span class="chart-label">Level 6, react-dom.development.js (1.03 MB), 31 per second</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.4101"><i title="google/ngx_brotli: 17.0 ms"></i><b>17.0</b></span><span class="bar c-loop" style="--x: 0.3891"><i title="ngx_mbrotli, event loop: 14.7 ms"></i><b>14.7</b></span><span class="bar c-pool1" style="--x: 0.0768"><i title="ngx_mbrotli, pool, 1 task: 1.7 ms"></i><b>1.7</b></span><span class="bar c-pool4" style="--x: 0.0851"><i title="ngx_mbrotli, pool, 4 tasks: 1.8 ms"></i><b>1.8</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 11, jquery.min.js (85 KB), 7 per second</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.6175"><i title="google/ngx_brotli: 71.2 ms"></i><b>71.2</b></span><span class="bar c-loop" style="--x: 0.5533"><i title="ngx_mbrotli, event loop: 45.7 ms"></i><b>45.7</b></span><span class="bar c-pool1" style="--x: 0.0851"><i title="ngx_mbrotli, pool, 1 task: 1.8 ms"></i><b>1.8</b></span><span class="bar c-pool4" style="--x: 0.0929"><i title="ngx_mbrotli, pool, 4 tasks: 1.9 ms"></i><b>1.9</b></span></span></div>
  </div>
  <figcaption>The same test as the chart at the top, with one 30-second run. NGINX 1.30.6, one worker on 4 CPUs, a pool of 4 threads; 20 October 2026.</figcaption>
</figure>

<details class="numbers">
  <summary>Numbers as a table</summary>

  <table>
    <thead>
      <tr>
        <th>Large responses</th>
        <th>Arrival</th>
        <th style="text-align: right">No compression</th>
        <th style="text-align: right">google/ngx_brotli</th>
        <th style="text-align: right">ngx_mbrotli, event loop</th>
        <th style="text-align: right">Pool, 1 task</th>
        <th style="text-align: right">Pool, 4 tasks</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>react-dom.development.js, level 6, 31 per second</td>
        <td>8 at once</td>
        <td style="text-align: right">1.9</td>
        <td style="text-align: right">115</td>
        <td style="text-align: right">94</td>
        <td style="text-align: right">3.3</td>
        <td style="text-align: right">2.8</td>
      </tr>
      <tr>
        <td>react-dom.development.js, level 6, 31 per second</td>
        <td>One at a time</td>
        <td style="text-align: right">1.9</td>
        <td style="text-align: right">17.0</td>
        <td style="text-align: right">14.7</td>
        <td style="text-align: right">1.7</td>
        <td style="text-align: right">1.8</td>
      </tr>
      <tr>
        <td>jquery.min.js, level 11, 7 per second</td>
        <td>8 at once</td>
        <td style="text-align: right">1.9</td>
        <td style="text-align: right">514</td>
        <td style="text-align: right">306</td>
        <td style="text-align: right">2.0</td>
        <td style="text-align: right">1.9</td>
      </tr>
      <tr>
        <td>jquery.min.js, level 11, 7 per second</td>
        <td>One at a time</td>
        <td style="text-align: right">1.9</td>
        <td style="text-align: right">71.2</td>
        <td style="text-align: right">45.7</td>
        <td style="text-align: right">1.8</td>
        <td style="text-align: right">1.9</td>
      </tr>
    </tbody>
  </table>

</details>

<p>“8 at once” is the large stream’s 8 connections, which wrk2 starts together and which
stay in phase. It is what happens when several clients ask for large files at the same
moment. In the event loop, a small request then waits for up to 8 compressions. “One
at a time” is a single connection at a constant interval, where a small request waits
for at most one. It is the lower bound of the effect: the pool cut p99 by 9–40 times
there. On the pool, the maximum latency of a small request one at a time was 3.6 ms.</p>

<p>Without a thread pool, ngx_mbrotli lowers p99 by 14–41% against google/ngx_brotli,
only because mbrotli compresses faster. It blocks the event loop all the same. The
difference in tail latency comes from moving compression off the event loop, not from
a faster encoder.</p>

<h2 id="how-many-requests-does-one-worker-serve">How many requests does one worker serve?</h2>

<p>The same worker under closed-loop load, with wrk and 8 connections:</p>

<figure class="chart" id="br-rps" aria-labelledby="br-rps-title">
  <p class="chart-title" id="br-rps-title">Requests per second, relative to google/ngx_brotli</p>
  <p class="chart-sub">8 connections. 1 task per response serves within 3% of 4, except one case. Logarithmic scale.</p>
  <ul class="chart-legend"><li><span class="key round c-loop"></span>ngx_mbrotli, event loop</li><li><span class="key round c-pool4"></span>ngx_mbrotli, pool, 4 tasks</li><li><span class="key ref"></span>google/ngx_brotli = 1×</li></ul>
  <div class="chart-body">
    <div class="chart-grid" aria-hidden="true"><span style="--x: 0.0000"></span><span style="--x: 0.3758"></span><span style="--x: 0.5637"></span><span style="--x: 0.7516"></span><span style="--x: 0.9395"></span><span class="ref" style="--x: 0.1879"></span></div>
    <div class="chart-axis" aria-hidden="true"><span></span><span class="plot"><span class="tick first" style="--x: 0.0000">0.5×</span><span class="tick" style="--x: 0.1879">1×</span><span class="tick" style="--x: 0.3758">2×</span><span class="tick" style="--x: 0.5637">4×</span><span class="tick" style="--x: 0.7516">8×</span><span class="tick" style="--x: 0.9395">16×</span></span></div>
    <p class="chart-group">jquery.min.js, 85 KB</p>
    <div class="chart-row"><span class="chart-label">Level 4</span><span class="plot dots"><span class="span" style="--a: 0.2263; --b: 0.5780"></span><span class="dot c-loop" style="--x: 0.2263" title="ngx_mbrotli, event loop: 1.2×"></span><span class="dot c-pool4" style="--x: 0.5780" title="ngx_mbrotli, pool, 4 tasks: 4.2×"></span><b class="dot-value" style="--x: 0.5780">4.2×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 6</span><span class="plot dots"><span class="span" style="--a: 0.2009; --b: 0.5483"></span><span class="dot c-loop" style="--x: 0.2009" title="ngx_mbrotli, event loop: 1.0×"></span><span class="dot c-pool4" style="--x: 0.5483" title="ngx_mbrotli, pool, 4 tasks: 3.8×"></span><b class="dot-value" style="--x: 0.5483">3.8×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot dots"><span class="span" style="--a: 0.5875; --b: 0.9298"></span><span class="dot c-loop" style="--x: 0.5875" title="ngx_mbrotli, event loop: 4.4×"></span><span class="dot c-pool4" style="--x: 0.9298" title="ngx_mbrotli, pool, 4 tasks: 15.4×"></span><b class="dot-value" style="--x: 0.9298">15.4×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 11</span><span class="plot dots"><span class="span" style="--a: 0.3215; --b: 0.6905"></span><span class="dot c-loop" style="--x: 0.3215" title="ngx_mbrotli, event loop: 1.6×"></span><span class="dot c-pool4" style="--x: 0.6905" title="ngx_mbrotli, pool, 4 tasks: 6.4×"></span><b class="dot-value" style="--x: 0.6905">6.4×</b></span></div>
    <p class="chart-group">react-dom.development.js, 1.03 MB</p>
    <div class="chart-row"><span class="chart-label">Level 4</span><span class="plot dots"><span class="span" style="--a: 0.2253; --b: 0.5512"></span><span class="dot c-loop" style="--x: 0.2253" title="ngx_mbrotli, event loop: 1.1×"></span><span class="dot c-pool4" style="--x: 0.5512" title="ngx_mbrotli, pool, 4 tasks: 3.8×"></span><b class="dot-value" style="--x: 0.5512">3.8×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 6</span><span class="plot dots"><span class="span" style="--a: 0.2350; --b: 0.5872"></span><span class="dot c-loop" style="--x: 0.2350" title="ngx_mbrotli, event loop: 1.2×"></span><span class="dot c-pool4" style="--x: 0.5872" title="ngx_mbrotli, pool, 4 tasks: 4.4×"></span><b class="dot-value" style="--x: 0.5872">4.4×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot dots"><span class="span" style="--a: 0.3217; --b: 0.6356"></span><span class="dot c-loop" style="--x: 0.3217" title="ngx_mbrotli, event loop: 1.6×"></span><span class="dot c-pool4" style="--x: 0.6356" title="ngx_mbrotli, pool, 4 tasks: 5.2×"></span><b class="dot-value" style="--x: 0.6356">5.2×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 11</span><span class="plot dots"><span class="span" style="--a: 0.3099; --b: 0.8007"></span><span class="dot c-loop" style="--x: 0.3099" title="ngx_mbrotli, event loop: 1.6×"></span><span class="dot c-pool4" style="--x: 0.8007" title="ngx_mbrotli, pool, 4 tasks: 9.6×"></span><b class="dot-value" style="--x: 0.8007">9.6×</b></span></div>
    <p class="chart-group">libraries.json, 2.2 MB</p>
    <div class="chart-row"><span class="chart-label">Level 4</span><span class="plot dots"><span class="span" style="--a: 0.2237; --b: 0.5585"></span><span class="dot c-loop" style="--x: 0.2237" title="ngx_mbrotli, event loop: 1.1×"></span><span class="dot c-pool4" style="--x: 0.5585" title="ngx_mbrotli, pool, 4 tasks: 3.9×"></span><b class="dot-value" style="--x: 0.5585">3.9×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 6</span><span class="plot dots"><span class="span" style="--a: 0.2274; --b: 0.5863"></span><span class="dot c-loop" style="--x: 0.2274" title="ngx_mbrotli, event loop: 1.2×"></span><span class="dot c-pool4" style="--x: 0.5863" title="ngx_mbrotli, pool, 4 tasks: 4.3×"></span><b class="dot-value" style="--x: 0.5863">4.3×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot dots"><span class="span" style="--a: 0.2700; --b: 0.5809"></span><span class="dot c-loop" style="--x: 0.2700" title="ngx_mbrotli, event loop: 1.4×"></span><span class="dot c-pool4" style="--x: 0.5809" title="ngx_mbrotli, pool, 4 tasks: 4.3×"></span><b class="dot-value" style="--x: 0.5809">4.3×</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 11</span><span class="plot dots"><span class="span" style="--a: 0.3122; --b: 0.8000"></span><span class="dot c-loop" style="--x: 0.3122" title="ngx_mbrotli, event loop: 1.6×"></span><span class="dot c-pool4" style="--x: 0.8000" title="ngx_mbrotli, pool, 4 tasks: 9.6×"></span><b class="dot-value" style="--x: 0.8000">9.6×</b></span></div>
  </div>
  <figcaption>wrk, median of three runs. NGINX 1.30.6, one worker on 4 CPUs, a pool of 4 threads; 20 October 2026.</figcaption>
</figure>

<details class="numbers">
  <summary>Numbers as a table</summary>

  <table>
    <thead>
      <tr>
        <th>Response</th>
        <th style="text-align: right">Level</th>
        <th style="text-align: right">google/ngx_brotli</th>
        <th style="text-align: right">ngx_mbrotli, event loop</th>
        <th style="text-align: right">Difference</th>
        <th style="text-align: right">Pool, 1 task</th>
        <th style="text-align: right">Difference</th>
        <th style="text-align: right">Pool, 4 tasks</th>
        <th style="text-align: right">Difference</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>jquery.min.js, 85 KB</td>
        <td style="text-align: right">4</td>
        <td style="text-align: right">985</td>
        <td style="text-align: right">1,135</td>
        <td style="text-align: right">+15%</td>
        <td style="text-align: right">4,180</td>
        <td style="text-align: right">4.2×</td>
        <td style="text-align: right">4,154</td>
        <td style="text-align: right">4.2×</td>
      </tr>
      <tr>
        <td>jquery.min.js, 85 KB</td>
        <td style="text-align: right">6</td>
        <td style="text-align: right">674</td>
        <td style="text-align: right">706</td>
        <td style="text-align: right">+5%</td>
        <td style="text-align: right">2,528</td>
        <td style="text-align: right">3.8×</td>
        <td style="text-align: right">2,545</td>
        <td style="text-align: right">3.8×</td>
      </tr>
      <tr>
        <td>jquery.min.js, 85 KB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">108</td>
        <td style="text-align: right">472</td>
        <td style="text-align: right">4.4×</td>
        <td style="text-align: right">1,655</td>
        <td style="text-align: right">15.3×</td>
        <td style="text-align: right">1,669</td>
        <td style="text-align: right">15.4×</td>
      </tr>
      <tr>
        <td>jquery.min.js, 85 KB</td>
        <td style="text-align: right">11</td>
        <td style="text-align: right">14.9</td>
        <td style="text-align: right">24.4</td>
        <td style="text-align: right">+64%</td>
        <td style="text-align: right">94.5</td>
        <td style="text-align: right">6.4×</td>
        <td style="text-align: right">95.0</td>
        <td style="text-align: right">6.4×</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">4</td>
        <td style="text-align: right">134</td>
        <td style="text-align: right">154</td>
        <td style="text-align: right">+15%</td>
        <td style="text-align: right">512</td>
        <td style="text-align: right">3.8×</td>
        <td style="text-align: right">512</td>
        <td style="text-align: right">3.8×</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">6</td>
        <td style="text-align: right">63.3</td>
        <td style="text-align: right">75.3</td>
        <td style="text-align: right">+19%</td>
        <td style="text-align: right">282</td>
        <td style="text-align: right">4.5×</td>
        <td style="text-align: right">276</td>
        <td style="text-align: right">4.4×</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">24.5</td>
        <td style="text-align: right">40.1</td>
        <td style="text-align: right">+64%</td>
        <td style="text-align: right">110</td>
        <td style="text-align: right">4.5×</td>
        <td style="text-align: right">128</td>
        <td style="text-align: right">5.2×</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">11</td>
        <td style="text-align: right">1.02</td>
        <td style="text-align: right">1.60</td>
        <td style="text-align: right">+57%</td>
        <td style="text-align: right">9.76</td>
        <td style="text-align: right">9.6×</td>
        <td style="text-align: right">9.78</td>
        <td style="text-align: right">9.6×</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">4</td>
        <td style="text-align: right">68.4</td>
        <td style="text-align: right">78.1</td>
        <td style="text-align: right">+14%</td>
        <td style="text-align: right">263</td>
        <td style="text-align: right">3.8×</td>
        <td style="text-align: right">268</td>
        <td style="text-align: right">3.9×</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">6</td>
        <td style="text-align: right">32.7</td>
        <td style="text-align: right">37.8</td>
        <td style="text-align: right">+16%</td>
        <td style="text-align: right">146</td>
        <td style="text-align: right">4.5×</td>
        <td style="text-align: right">142</td>
        <td style="text-align: right">4.3×</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">14.8</td>
        <td style="text-align: right">20.1</td>
        <td style="text-align: right">+35%</td>
        <td style="text-align: right">63.3</td>
        <td style="text-align: right">4.3×</td>
        <td style="text-align: right">63.3</td>
        <td style="text-align: right">4.3×</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">11</td>
        <td style="text-align: right">0.55</td>
        <td style="text-align: right">0.87</td>
        <td style="text-align: right">+58%</td>
        <td style="text-align: right">5.11</td>
        <td style="text-align: right">9.3×</td>
        <td style="text-align: right">5.26</td>
        <td style="text-align: right">9.6×</td>
      </tr>
    </tbody>
  </table>

</details>

<p>The pool’s 4 threads use the 4 CPUs that google/ngx_brotli, with one worker, cannot.
That explains about 4× at levels 4–6. The rest comes from the encoder. At level 9
the C encoder allocates a 32 MiB hash table for every response over 64 KB and frees
it afterwards. That cost does not depend on the response size, so mbrotli’s lead is
largest on the 85 KB file. At level 11 a 1 MiB segment compresses faster than a whole
file, so the pool gains more than its 4 threads.</p>

<p>With 8 connections, 1 and 4 tasks per response served within 3% of each other, with
one exception (+17%). The pool’s threads were already busy with different responses.</p>

<h2 id="how-fast-is-one-large-response">How fast is one large response?</h2>

<p>More tasks per response help when a response is longer than one segment and nothing
else competes for the threads:</p>

<figure class="chart" id="br-single" aria-labelledby="br-single-title">
  <p class="chart-title" id="br-single-title">Time to serve one 2.2 MB response</p>
  <p class="chart-sub">libraries.json, three 1 MiB segments, one connection. ms, logarithmic scale.</p>
  <ul class="chart-legend"><li><span class="key  c-stock"></span>google/ngx_brotli</li><li><span class="key  c-loop"></span>ngx_mbrotli, event loop</li><li><span class="key  c-pool1"></span>ngx_mbrotli, pool, 1 task</li><li><span class="key  c-pool4"></span>ngx_mbrotli, pool, 4 tasks</li></ul>
  <div class="chart-body">
    <div class="chart-grid" aria-hidden="true"><span style="--x: 0.0000"></span><span style="--x: 0.2876"></span><span style="--x: 0.5752"></span><span style="--x: 0.8628"></span></div>
    <div class="chart-axis" aria-hidden="true"><span></span><span class="plot"><span class="tick first" style="--x: 0.0000">1</span><span class="tick" style="--x: 0.2876">10</span><span class="tick" style="--x: 0.5752">100</span><span class="tick" style="--x: 0.8628">1,000</span></span></div>
    <div class="chart-row"><span class="chart-label">Level 4</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.3349"><i title="google/ngx_brotli: 14.6 ms"></i><b>14.6</b></span><span class="bar c-loop" style="--x: 0.3194"><i title="ngx_mbrotli, event loop: 12.9 ms"></i><b>12.9</b></span><span class="bar c-pool1" style="--x: 0.3213"><i title="ngx_mbrotli, pool, 1 task: 13.1 ms"></i><b>13.1</b></span><span class="bar c-pool4" style="--x: 0.2357"><i title="ngx_mbrotli, pool, 4 tasks: 6.6 ms"></i><b>6.6</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 6</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.4273"><i title="google/ngx_brotli: 30.6 ms"></i><b>30.6</b></span><span class="bar c-loop" style="--x: 0.4098"><i title="ngx_mbrotli, event loop: 26.6 ms"></i><b>26.6</b></span><span class="bar c-pool1" style="--x: 0.4030"><i title="ngx_mbrotli, pool, 1 task: 25.2 ms"></i><b>25.2</b></span><span class="bar c-pool4" style="--x: 0.3155"><i title="ngx_mbrotli, pool, 4 tasks: 12.5 ms"></i><b>12.5</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.5290"><i title="google/ngx_brotli: 69.1 ms"></i><b>69.1</b></span><span class="bar c-loop" style="--x: 0.4889"><i title="ngx_mbrotli, event loop: 50.1 ms"></i><b>50.1</b></span><span class="bar c-pool1" style="--x: 0.5003"><i title="ngx_mbrotli, pool, 1 task: 54.9 ms"></i><b>54.9</b></span><span class="bar c-pool4" style="--x: 0.3975"><i title="ngx_mbrotli, pool, 4 tasks: 24.1 ms"></i><b>24.1</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 11</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.9496"><i title="google/ngx_brotli: 2,004 ms"></i><b>2,004</b></span><span class="bar c-loop" style="--x: 0.8910"><i title="ngx_mbrotli, event loop: 1,253 ms"></i><b>1,253</b></span><span class="bar c-pool1" style="--x: 0.8303"><i title="ngx_mbrotli, pool, 1 task: 771 ms"></i><b>771</b></span><span class="bar c-pool4" style="--x: 0.7389"><i title="ngx_mbrotli, pool, 4 tasks: 371 ms"></i><b>371</b></span></span></div>
  </div>
  <figcaption>wrk, median of three runs. NGINX 1.30.6, one worker on 4 CPUs, a pool of 4 threads; 20 October 2026.</figcaption>
</figure>

<details class="numbers">
  <summary>Numbers as a table</summary>

  <table>
    <thead>
      <tr>
        <th style="text-align: right">Level</th>
        <th style="text-align: right">google/ngx_brotli</th>
        <th style="text-align: right">ngx_mbrotli, event loop</th>
        <th style="text-align: right">Pool, 1 task</th>
        <th style="text-align: right">Pool, 4 tasks</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td style="text-align: right">4</td>
        <td style="text-align: right">14.6</td>
        <td style="text-align: right">12.9</td>
        <td style="text-align: right">13.1</td>
        <td style="text-align: right">6.6</td>
      </tr>
      <tr>
        <td style="text-align: right">6</td>
        <td style="text-align: right">30.6</td>
        <td style="text-align: right">26.6</td>
        <td style="text-align: right">25.2</td>
        <td style="text-align: right">12.5</td>
      </tr>
      <tr>
        <td style="text-align: right">9</td>
        <td style="text-align: right">69.1</td>
        <td style="text-align: right">50.1</td>
        <td style="text-align: right">54.9</td>
        <td style="text-align: right">24.1</td>
      </tr>
      <tr>
        <td style="text-align: right">11</td>
        <td style="text-align: right">2,004</td>
        <td style="text-align: right">1,253</td>
        <td style="text-align: right">771</td>
        <td style="text-align: right">371</td>
      </tr>
    </tbody>
  </table>

</details>

<p>4 tasks served it 2.0–2.3 times as fast as 1 task, and 2.2–5.4 times as fast as
google/ngx_brotli. Responses up to 1 MiB fit in one segment, so the number of tasks
does not change how fast they are served. <code class="language-plaintext highlighter-rouge">react-dom.development.js</code> is a 1 MiB
segment and a 31 KB one, so 4 tasks served it only 4–34% faster than 1.</p>

<h2 id="brotli-level-11-on-the-fly">Brotli level 11 on the fly</h2>

<p>Level 11 is usually kept for pre-compressed static files, because one response can
hold a worker for a second or more. On a thread pool, that time no longer holds up
other requests: next to a stream of level-11 responses, small requests kept a p99 of
1.8–2.0 ms. A 2.2 MB response took 371 ms with 4 tasks, against 2,004 ms with
google/ngx_brotli.</p>

<p>Level 11 still costs CPU. <code class="language-plaintext highlighter-rouge">libraries.json</code> needed 785 ms of CPU time per response on
the pool, and the worker served 5.3 such responses per second on 4 CPUs. It fits
responses that are large, compressible and requested rarely enough, not a busy API.</p>

<h2 id="what-the-thread-pool-costs">What the thread pool costs</h2>

<p><strong>Memory.</strong> In the event loop, a worker compresses one response at a time and needs
one encoder. On a pool, every response being sent holds encoders for its segments
until it is sent. Peak memory then grows with the number of responses in flight and
with the number of tasks per response.</p>

<figure class="chart" id="br-memory" aria-labelledby="br-memory-title">
  <p class="chart-title" id="br-memory-title">Peak memory of the worker</p>
  <p class="chart-sub">VmHWM at 8 connections, MiB, logarithmic scale. The labelled value is the pool with 4 tasks.</p>
  <ul class="chart-legend"><li><span class="key round c-stock"></span>google/ngx_brotli</li><li><span class="key round c-loop"></span>ngx_mbrotli, event loop</li><li><span class="key round c-pool1"></span>ngx_mbrotli, pool, 1 task</li><li><span class="key round c-pool4"></span>ngx_mbrotli, pool, 4 tasks</li></ul>
  <div class="chart-body">
    <div class="chart-grid" aria-hidden="true"><span style="--x: 0.0000"></span><span style="--x: 0.2222"></span><span style="--x: 0.4444"></span><span style="--x: 0.6667"></span><span style="--x: 0.8889"></span></div>
    <div class="chart-axis" aria-hidden="true"><span></span><span class="plot"><span class="tick first" style="--x: 0.0000">4</span><span class="tick" style="--x: 0.2222">16</span><span class="tick" style="--x: 0.4444">64</span><span class="tick" style="--x: 0.6667">256</span><span class="tick" style="--x: 0.8889">1,024</span></span></div>
    <p class="chart-group">react-dom.development.js, 1.03 MB</p>
    <div class="chart-row"><span class="chart-label">Level 4</span><span class="plot dots"><span class="span" style="--a: 0.1300; --b: 0.5676"></span><span class="dot c-stock" style="--x: 0.1300" title="google/ngx_brotli: 9 MiB"></span><span class="dot c-loop" style="--x: 0.1761" title="ngx_mbrotli, event loop: 12 MiB"></span><span class="dot c-pool1" style="--x: 0.5412" title="ngx_mbrotli, pool, 1 task: 117 MiB"></span><span class="dot c-pool4" style="--x: 0.5676" title="ngx_mbrotli, pool, 4 tasks: 138 MiB"></span><b class="dot-value" style="--x: 0.5676">138 MiB</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 6</span><span class="plot dots"><span class="span" style="--a: 0.1622; --b: 0.5744"></span><span class="dot c-stock" style="--x: 0.1622" title="google/ngx_brotli: 11 MiB"></span><span class="dot c-loop" style="--x: 0.2008" title="ngx_mbrotli, event loop: 14 MiB"></span><span class="dot c-pool1" style="--x: 0.5744" title="ngx_mbrotli, pool, 1 task: 144 MiB"></span><span class="dot c-pool4" style="--x: 0.5733" title="ngx_mbrotli, pool, 4 tasks: 143 MiB"></span><b class="dot-value left" style="--x: 0.5733">143 MiB</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot dots"><span class="span" style="--a: 0.3650; --b: 0.7684"></span><span class="dot c-stock" style="--x: 0.3650" title="google/ngx_brotli: 39 MiB"></span><span class="dot c-loop" style="--x: 0.3731" title="ngx_mbrotli, event loop: 41 MiB"></span><span class="dot c-pool1" style="--x: 0.7681" title="ngx_mbrotli, pool, 1 task: 482 MiB"></span><span class="dot c-pool4" style="--x: 0.7684" title="ngx_mbrotli, pool, 4 tasks: 483 MiB"></span><b class="dot-value" style="--x: 0.7684">483 MiB</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 11</span><span class="plot dots"><span class="span" style="--a: 0.2938; --b: 0.6899"></span><span class="dot c-stock" style="--x: 0.2938" title="google/ngx_brotli: 25 MiB"></span><span class="dot c-loop" style="--x: 0.3119" title="ngx_mbrotli, event loop: 28 MiB"></span><span class="dot c-pool1" style="--x: 0.6899" title="ngx_mbrotli, pool, 1 task: 296 MiB"></span><span class="dot c-pool4" style="--x: 0.6746" title="ngx_mbrotli, pool, 4 tasks: 269 MiB"></span><b class="dot-value left" style="--x: 0.6746">269 MiB</b></span></div>
    <p class="chart-group">libraries.json, 2.2 MB</p>
    <div class="chart-row"><span class="chart-label">Level 4</span><span class="plot dots"><span class="span" style="--a: 0.1300; --b: 0.6387"></span><span class="dot c-stock" style="--x: 0.1300" title="google/ngx_brotli: 9 MiB"></span><span class="dot c-loop" style="--x: 0.2008" title="ngx_mbrotli, event loop: 14 MiB"></span><span class="dot c-pool1" style="--x: 0.5733" title="ngx_mbrotli, pool, 1 task: 143 MiB"></span><span class="dot c-pool4" style="--x: 0.6387" title="ngx_mbrotli, pool, 4 tasks: 215 MiB"></span><b class="dot-value" style="--x: 0.6387">215 MiB</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 6</span><span class="plot dots"><span class="span" style="--a: 0.1622; --b: 0.6787"></span><span class="dot c-stock" style="--x: 0.1622" title="google/ngx_brotli: 11 MiB"></span><span class="dot c-loop" style="--x: 0.2008" title="ngx_mbrotli, event loop: 14 MiB"></span><span class="dot c-pool1" style="--x: 0.6349" title="ngx_mbrotli, pool, 1 task: 210 MiB"></span><span class="dot c-pool4" style="--x: 0.6787" title="ngx_mbrotli, pool, 4 tasks: 276 MiB"></span><b class="dot-value" style="--x: 0.6787">276 MiB</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot dots"><span class="span" style="--a: 0.3650; --b: 0.8843"></span><span class="dot c-stock" style="--x: 0.3650" title="google/ngx_brotli: 39 MiB"></span><span class="dot c-loop" style="--x: 0.3807" title="ngx_mbrotli, event loop: 43 MiB"></span><span class="dot c-pool1" style="--x: 0.7800" title="ngx_mbrotli, pool, 1 task: 519 MiB"></span><span class="dot c-pool4" style="--x: 0.8843" title="ngx_mbrotli, pool, 4 tasks: 995 MiB"></span><b class="dot-value" style="--x: 0.8843">995 MiB</b></span></div>
    <div class="chart-row"><span class="chart-label">Level 11</span><span class="plot dots"><span class="span" style="--a: 0.3119; --b: 0.7756"></span><span class="dot c-stock" style="--x: 0.3119" title="google/ngx_brotli: 28 MiB"></span><span class="dot c-loop" style="--x: 0.3230" title="ngx_mbrotli, event loop: 30 MiB"></span><span class="dot c-pool1" style="--x: 0.7341" title="ngx_mbrotli, pool, 1 task: 390 MiB"></span><span class="dot c-pool4" style="--x: 0.7756" title="ngx_mbrotli, pool, 4 tasks: 505 MiB"></span><b class="dot-value" style="--x: 0.7756">505 MiB</b></span></div>
  </div>
  <figcaption>Highest of three runs. NGINX 1.30.6, one worker on 4 CPUs, a pool of 4 threads; 20 October 2026.</figcaption>
</figure>

<details class="numbers">
  <summary>Numbers as a table</summary>

  <table>
    <thead>
      <tr>
        <th>Response</th>
        <th style="text-align: right">Level</th>
        <th style="text-align: right">google/ngx_brotli</th>
        <th style="text-align: right">ngx_mbrotli, event loop</th>
        <th style="text-align: right">Pool, 1 task</th>
        <th style="text-align: right">Pool, 4 tasks</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">4</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">12</td>
        <td style="text-align: right">117</td>
        <td style="text-align: right">138</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">6</td>
        <td style="text-align: right">11</td>
        <td style="text-align: right">14</td>
        <td style="text-align: right">144</td>
        <td style="text-align: right">143</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">39</td>
        <td style="text-align: right">41</td>
        <td style="text-align: right">482</td>
        <td style="text-align: right">483</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">11</td>
        <td style="text-align: right">25</td>
        <td style="text-align: right">28</td>
        <td style="text-align: right">296</td>
        <td style="text-align: right">269</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">4</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">14</td>
        <td style="text-align: right">143</td>
        <td style="text-align: right">215</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">6</td>
        <td style="text-align: right">11</td>
        <td style="text-align: right">14</td>
        <td style="text-align: right">210</td>
        <td style="text-align: right">276</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">39</td>
        <td style="text-align: right">43</td>
        <td style="text-align: right">519</td>
        <td style="text-align: right">995</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">11</td>
        <td style="text-align: right">28</td>
        <td style="text-align: right">30</td>
        <td style="text-align: right">390</td>
        <td style="text-align: right">505</td>
      </tr>
    </tbody>
  </table>

</details>

<p>In the event loop, ngx_mbrotli needs 2–5 MiB more than google/ngx_brotli. On the
pool, it needs 11–26 times as much.</p>

<p><strong>CPU time.</strong> CPU time per request, including the pool’s threads, was within 0.6–1.4
times that of the event loop. At level 11 the pool needed 37–40% less, because 1 MiB
segments compress faster than a whole file. At level 9 it needed 22–40% more.</p>

<figure class="chart" id="br-cpu" aria-labelledby="br-cpu-title">
  <p class="chart-title" id="br-cpu-title">CPU time per request</p>
  <p class="chart-sub">Worker and pool threads together, 8 connections. ms, logarithmic scale.</p>
  <ul class="chart-legend"><li><span class="key  c-stock"></span>google/ngx_brotli</li><li><span class="key  c-loop"></span>ngx_mbrotli, event loop</li><li><span class="key  c-pool1"></span>ngx_mbrotli, pool, 1 task</li><li><span class="key  c-pool4"></span>ngx_mbrotli, pool, 4 tasks</li></ul>
  <div class="chart-body">
    <div class="chart-grid" aria-hidden="true"><span style="--x: 0.0000"></span><span style="--x: 0.2876"></span><span style="--x: 0.5752"></span><span style="--x: 0.8628"></span></div>
    <div class="chart-axis" aria-hidden="true"><span></span><span class="plot"><span class="tick first" style="--x: 0.0000">1</span><span class="tick" style="--x: 0.2876">10</span><span class="tick" style="--x: 0.5752">100</span><span class="tick" style="--x: 0.8628">1,000</span></span></div>
    <p class="chart-group">react-dom.development.js, 1.03 MB</p>
    <div class="chart-row"><span class="chart-label">Level 6</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.3463"><i title="google/ngx_brotli: 16.0 ms"></i><b>16.0</b></span><span class="bar c-loop" style="--x: 0.3241"><i title="ngx_mbrotli, event loop: 13.4 ms"></i><b>13.4</b></span><span class="bar c-pool1" style="--x: 0.3314"><i title="ngx_mbrotli, pool, 1 task: 14.2 ms"></i><b>14.2</b></span><span class="bar c-pool4" style="--x: 0.3340"><i title="ngx_mbrotli, pool, 4 tasks: 14.5 ms"></i><b>14.5</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.4677"><i title="google/ngx_brotli: 42.3 ms"></i><b>42.3</b></span><span class="bar c-loop" style="--x: 0.4050"><i title="ngx_mbrotli, event loop: 25.6 ms"></i><b>25.6</b></span><span class="bar c-pool1" style="--x: 0.4469"><i title="ngx_mbrotli, pool, 1 task: 35.8 ms"></i><b>35.8</b></span><span class="bar c-pool4" style="--x: 0.4297"><i title="ngx_mbrotli, pool, 4 tasks: 31.2 ms"></i><b>31.2</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 11</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.8806"><i title="google/ngx_brotli: 1,153 ms"></i><b>1,153</b></span><span class="bar c-loop" style="--x: 0.8173"><i title="ngx_mbrotli, event loop: 695 ms"></i><b>695</b></span><span class="bar c-pool1" style="--x: 0.7526"><i title="ngx_mbrotli, pool, 1 task: 414 ms"></i><b>414</b></span><span class="bar c-pool4" style="--x: 0.7529"><i title="ngx_mbrotli, pool, 4 tasks: 415 ms"></i><b>415</b></span></span></div>
    <p class="chart-group">libraries.json, 2.2 MB</p>
    <div class="chart-row"><span class="chart-label">Level 9</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.5331"><i title="google/ngx_brotli: 71.4 ms"></i><b>71.4</b></span><span class="bar c-loop" style="--x: 0.4913"><i title="ngx_mbrotli, event loop: 51.1 ms"></i><b>51.1</b></span><span class="bar c-pool1" style="--x: 0.5175"><i title="ngx_mbrotli, pool, 1 task: 63.0 ms"></i><b>63.0</b></span><span class="bar c-pool4" style="--x: 0.5173"><i title="ngx_mbrotli, pool, 4 tasks: 62.9 ms"></i><b>62.9</b></span></span></div>
    <div class="chart-row"><span class="chart-label">Level 11</span><span class="plot bars"><span class="bar c-stock" style="--x: 0.9738"><i title="google/ngx_brotli: 2,432 ms"></i><b>2,432</b></span><span class="bar c-loop" style="--x: 0.8913"><i title="ngx_mbrotli, event loop: 1,256 ms"></i><b>1,256</b></span><span class="bar c-pool1" style="--x: 0.8332"><i title="ngx_mbrotli, pool, 1 task: 789 ms"></i><b>789</b></span><span class="bar c-pool4" style="--x: 0.8325"><i title="ngx_mbrotli, pool, 4 tasks: 785 ms"></i><b>785</b></span></span></div>
  </div>
  <figcaption>Median of three runs. NGINX 1.30.6, one worker on 4 CPUs, a pool of 4 threads; 20 October 2026.</figcaption>
</figure>

<details class="numbers">
  <summary>Numbers as a table</summary>

  <table>
    <thead>
      <tr>
        <th>Response</th>
        <th style="text-align: right">Level</th>
        <th style="text-align: right">google/ngx_brotli</th>
        <th style="text-align: right">ngx_mbrotli, event loop</th>
        <th style="text-align: right">Pool, 1 task</th>
        <th style="text-align: right">Pool, 4 tasks</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">6</td>
        <td style="text-align: right">16.0</td>
        <td style="text-align: right">13.4</td>
        <td style="text-align: right">14.2</td>
        <td style="text-align: right">14.5</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">42.3</td>
        <td style="text-align: right">25.6</td>
        <td style="text-align: right">35.8</td>
        <td style="text-align: right">31.2</td>
      </tr>
      <tr>
        <td>react-dom.development.js, 1.03 MB</td>
        <td style="text-align: right">11</td>
        <td style="text-align: right">1,153</td>
        <td style="text-align: right">695</td>
        <td style="text-align: right">414</td>
        <td style="text-align: right">415</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">9</td>
        <td style="text-align: right">71.4</td>
        <td style="text-align: right">51.1</td>
        <td style="text-align: right">63.0</td>
        <td style="text-align: right">62.9</td>
      </tr>
      <tr>
        <td>libraries.json, 2.2 MB</td>
        <td style="text-align: right">11</td>
        <td style="text-align: right">2,432</td>
        <td style="text-align: right">1,256</td>
        <td style="text-align: right">789</td>
        <td style="text-align: right">785</td>
      </tr>
    </tbody>
  </table>

</details>

<p><strong>Compression ratio.</strong> Responses longer than one segment came out 0.9–5.2% larger,
because segments are compressed independently. Responses up to one segment are
byte-for-byte what the event loop produces.</p>

<h2 id="how-to-configure-it">How to configure it</h2>

<p>The defaults suit most sites: a 1 MiB segment and 4 tasks per response. What to
change and why:</p>

<ul>
  <li><strong>Declare a pool of your own</strong> with <code class="language-plaintext highlighter-rouge">thread_pool brotli threads=N;</code>. The
benchmark used 4 threads on 4 CPUs. Pool sizes were not compared, so start near
the number of CPUs a worker can use.</li>
  <li><strong>Keep <code class="language-plaintext highlighter-rouge">brotli_parallel_segment_size 1m</code>.</strong> With <code class="language-plaintext highlighter-rouge">4m</code>, both files here fit in
one segment, so the output is as small as in the event loop and peak memory is
19–42% lower. At levels 4–9, it served between 9% fewer and 14% more requests per
second than 1m. At level 11 it served 35–37% fewer, because the whole file
compresses slower than 1 MiB segments. With 4m, only responses longer than 4 MiB
are split across threads.</li>
  <li><strong>Lower <code class="language-plaintext highlighter-rouge">brotli_parallel_tasks</code> to save memory</strong> when single responses are rarely
longer than a segment. On the 2.2 MB file, 4 tasks needed up to 1.9 times the
memory of 1. Under load, 1 and 4 tasks served about the same number of requests.</li>
  <li><strong>Bound encoder memory</strong> with <code class="language-plaintext highlighter-rouge">brotli_parallel_compressors</code> (4 by default), which
limits how many compressors a worker keeps encoders for, and with
<code class="language-plaintext highlighter-rouge">brotli_retention_policy bounded &lt;size&gt;</code> or <code class="language-plaintext highlighter-rouge">release_all</code>, which keep less memory
between responses.</li>
  <li><strong>Turn the pool off per location</strong> with <code class="language-plaintext highlighter-rouge">brotli_thread_pool off;</code>.
Compressing a small response in the event loop holds the loop only briefly. In this
run the pool still served 1.5–5.4 times as many 1.9 KB responses per second, so
there is no measured reason to turn it off for small responses. A size threshold
was not measured.</li>
</ul>

<p>The <a href="https://github.com/M-Compressors/ngx_mbrotli#configuration-directives">ngx_mbrotli README</a>
documents every directive.</p>

<h2 id="when-you-do-not-need-it">When you do not need it</h2>

<ul>
  <li><strong>Static files</strong> are better compressed once, at build time, at level 11, and
served with <code class="language-plaintext highlighter-rouge">brotli_static on;</code>. Nothing is compressed per request then.</li>
  <li><strong>A CDN that compresses</strong> for you takes the work off NGINX. Traffic between the CDN
and NGINX may still be worth compressing.</li>
  <li><strong>Small responses only.</strong> If no response takes long to compress, the event loop
is never held long, and the pool mostly adds memory.</li>
</ul>

<h2 id="how-it-was-measured">How it was measured</h2>

<p><strong>Hardware.</strong> Intel Core i7-13700KF, Windows 10.0.26300.9711, WSL 2.7.14.0 with kernel
6.18.33.2-microsoft-standard-WSL2, Ubuntu 22.04.5. WSL2 shows 12 identical cores with
2 threads each and does not tell which are performance cores, so some of the pinned
CPUs may be efficiency cores. Traffic went over loopback without TLS, and files were
served from tmpfs (<code class="language-plaintext highlighter-rouge">/dev/shm</code>).</p>

<p><strong>Build.</strong> NGINX 1.30.6, <code class="language-plaintext highlighter-rouge">./configure --with-compat --with-threads
--without-http_rewrite_module</code>, gcc 11.4.0, both modules dynamic.
google/ngx_brotli at commit <code class="language-plaintext highlighter-rouge">a71f9312</code> with the Brotli 1.1.0 it pins as a
submodule. ngx_mbrotli at commit <code class="language-plaintext highlighter-rouge">8f7508a</code> with mbrotli 0.5.10, built with rustc
1.99.0. This commit predates the 0.4.0 release; the current release is 0.4.5,
and the run has not been repeated on it.</p>

<p><strong>Configuration.</strong> <code class="language-plaintext highlighter-rouge">worker_processes 1</code>, with the worker and its pool pinned to 4
physical cores, and <code class="language-plaintext highlighter-rouge">thread_pool cp threads=4</code>. On the pool, <code class="language-plaintext highlighter-rouge">brotli_thread_pool cp</code>
with <code class="language-plaintext highlighter-rouge">brotli_parallel_tasks 1</code> or the default 4. Everything else was left at its
default, including the 1m segment and encoder reuse.</p>

<p><strong>Load.</strong> Requests per second and CPU time: wrk 4.1.0 on 4 other physical cores, 2
threads and 8 connections, 4 seconds per run (8 seconds for files over 1 MB, 5 times
as long at level 11) after a 1-second warm-up. NGINX was restarted for every run,
and each run started once the worker had been idle for 300 ms. Three runs, in an
order shuffled between runs; tables give the median. CPU time is the worker’s user
and system time from <code class="language-plaintext highlighter-rouge">/proc/&lt;pid&gt;/stat</code>, including the pool’s threads. Latency:
two wrk2 streams (commit <code class="language-plaintext highlighter-rouge">44a94c1</code>) at once, open loop, with coordinated omission
corrected; 8 at once is the median of three 30-second runs, one at a time is a single
30-second run.</p>

<p><strong>Files.</strong> jQuery 3.7.1 (<code class="language-plaintext highlighter-rouge">jquery.min.js</code>, 85 KB) and React DOM 18.3.1
(<code class="language-plaintext highlighter-rouge">react-dom.development.js</code>, 1.03 MB) from cdnjs, the full cdnjs API listing
(<code class="language-plaintext highlighter-rouge">libraries.json</code>, 2.2 MB) and its first 10 entries (<code class="language-plaintext highlighter-rouge">small.json</code>, 1.9 KB), all
fetched on 20 October 2026.</p>

<p><strong>Correctness.</strong> Before the runs, every response in every combination of module,
variant, level and file was decoded with curl’s Brotli decoder and compared with the
original by SHA-256. All matched.</p>

<p><strong>Not measured.</strong> gzip, more than one worker process, pool sizes other than 4
threads, a response-size threshold for the pool, HTTP/2 and HTTP/3, slow clients,
long runs and bare-metal Linux. Runs were 3 per cell and 4–40 seconds long, shorter
than a full benchmark.</p>

<h2 id="install-it">Install it</h2>

<p>ngx_mbrotli has packages for Debian, Ubuntu, RHEL and its derivatives, and Amazon
Linux: <a href="/2026/install-ngx-mbrotli-packages/">install it from packages</a>, or
<a href="/2026/build-ngx-mbrotli-from-source/">build it from source</a>. Existing
google/ngx_brotli configuration keeps working; add <code class="language-plaintext highlighter-rouge">brotli_thread_pool</code> to move
compression off the event loop.</p>

<p>The same measurements for Zstandard, with ngx_mzstd against tokers/zstd-nginx-module,
are in <a href="/2026/ngx-mzstd-thread-pool-benchmarks/">zstd on NGINX thread pools</a>.</p>]]></content><author><name>Mikhail Panfilov</name></author><category term="nginx" /><category term="brotli" /><category term="thread-pool" /><category term="latency" /><category term="benchmarks" /><category term="performance" /><summary type="html"><![CDATA[On-the-fly Brotli blocks the NGINX event loop. ngx_mbrotli on a thread pool against google/ngx_brotli: p99 latency, requests per second, CPU time and memory.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://m-compressors.github.io/assets/img/og/ngx-mbrotli-thread-pool-benchmarks.png" /><media:content medium="image" url="https://m-compressors.github.io/assets/img/og/ngx-mbrotli-thread-pool-benchmarks.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Building the ngx_mzstd Zstandard module for NGINX from source</title><link href="https://m-compressors.github.io/2026/build-ngx-mzstd-from-source/" rel="alternate" type="text/html" title="Building the ngx_mzstd Zstandard module for NGINX from source" /><published>2026-10-22T10:30:00+03:00</published><updated>2026-10-22T10:30:00+03:00</updated><id>https://m-compressors.github.io/2026/build-ngx-mzstd-from-source</id><content type="html" xml:base="https://m-compressors.github.io/2026/build-ngx-mzstd-from-source/"><![CDATA[<p>The <a href="/2026/install-ngx-mzstd-packages/">previous post</a> installed <strong>ngx_mzstd</strong> from
packages. Build it yourself when there is no package for your system, when you
compile nginx yourself, or when you want a module version that has not been
packaged yet.</p>

<p>Every command here was run on fresh containers on 22 October 2026, against nginx.org’s
nginx on Debian 13 and AlmaLinux 9, and against nginx built from source on
Ubuntu 24.04. Each one ended with nginx answering <code class="language-plaintext highlighter-rouge">Content-Encoding: zstd</code>.</p>

<h2 id="install-nginx-from-nginxorg">Install nginx from nginx.org</h2>

<p>nginx loads a dynamic module only if it was built for the same nginx version and the
same configuration. nginx.org’s packages are built with <code class="language-plaintext highlighter-rouge">--with-compat</code>, which makes
a separately built module match them reliably, so on Alpine, Arch Linux, openSUSE and
every other system, install nginx from
<a href="https://nginx.org/en/linux_packages.html">nginx.org’s official repository</a> first.
Check that your nginx has the flag:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nginx <span class="nt">-V</span> 2&gt;&amp;1 | <span class="nb">grep</span> <span class="nt">-o</span> <span class="nt">--</span> <span class="nt">--with-compat</span>
</code></pre></div></div>

<p>The module also needs nginx 1.25.4 or newer. To build nginx yourself instead, see
<a href="#building-nginx-too">the end of this post</a>.</p>

<h2 id="install-the-build-tools">Install the build tools</h2>

<p>You need a C toolchain, git, libclang (the Rust bindings to nginx are generated from
its headers), and the headers nginx is compiled against. The zstd library is not
among them.</p>

<p>Debian and Ubuntu:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>build-essential curl git libclang-dev libpcre2-dev libssl-dev zlib1g-dev
</code></pre></div></div>

<p>RHEL, AlmaLinux, Rocky Linux and Oracle Linux:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install </span>redhat-rpm-config gcc make git <span class="nb">tar </span>clang-devel pcre2-devel openssl-devel zlib-devel
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">redhat-rpm-config</code> provides the compiler specs that nginx.org’s RPM builds pass to
the compiler. Without it, <code class="language-plaintext highlighter-rouge">./configure</code> stops with
<code class="language-plaintext highlighter-rouge">the invalid value in --with-ld-opt="-Wl,-z,relro -Wl,-z,now -pie"</code>.</p>

<p>On other systems, install the same things with the system’s package manager: a C
compiler and make, git, curl, libclang, and the PCRE2, OpenSSL and zlib development
headers.</p>

<h2 id="install-rust">Install Rust</h2>

<p>ngx_mzstd needs Rust 1.89 or newer. Distribution packages are often older: Debian 13
ships 1.85. Install Rust with <a href="https://rustup.rs">rustup</a> as the user who will build:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">--proto</span> <span class="s1">'=https'</span> <span class="nt">--tlsv1</span>.2 <span class="nt">-sSf</span> https://sh.rustup.rs | sh <span class="nt">-s</span> <span class="nt">--</span> <span class="nt">-y</span> <span class="nt">--profile</span> minimal
<span class="nb">.</span> <span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/.cargo/env"</span>
</code></pre></div></div>

<h2 id="configure-nginxs-source-like-your-nginx">Configure nginx’s source like your nginx</h2>

<p>Download the source of exactly the version you run and configure it with the
arguments your binary was built with. Only <code class="language-plaintext highlighter-rouge">./configure</code> is needed: the module reads
its output in <code class="language-plaintext highlighter-rouge">objs/</code>, and nothing is compiled or installed.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">ver</span><span class="o">=</span><span class="si">$(</span>nginx <span class="nt">-v</span> 2&gt;&amp;1 | <span class="nb">sed</span> <span class="s1">'s|.*nginx/||'</span><span class="si">)</span>
<span class="nb">cd</span> ~
curl <span class="nt">-O</span> <span class="s2">"https://nginx.org/download/nginx-</span><span class="nv">$ver</span><span class="s2">.tar.gz"</span>
<span class="nb">tar </span>xzf <span class="s2">"nginx-</span><span class="nv">$ver</span><span class="s2">.tar.gz"</span>
<span class="nb">cd</span> <span class="s2">"nginx-</span><span class="nv">$ver</span><span class="s2">"</span>
<span class="nb">eval</span> <span class="s2">"./configure </span><span class="si">$(</span>nginx <span class="nt">-V</span> 2&gt;&amp;1 | <span class="nb">sed</span> <span class="nt">-n</span> <span class="s1">'s/^configure arguments: //p'</span><span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">eval</code> keeps quoted arguments such as <code class="language-plaintext highlighter-rouge">--with-cc-opt='-O2 -g ...'</code> in one piece. If
<code class="language-plaintext highlighter-rouge">./configure</code> reports a missing library, install its development package and run it
again. If you already configured this tree for
<a href="/2026/build-ngx-mbrotli-from-source/">ngx_mbrotli</a>, reuse it.</p>

<h2 id="build-the-module">Build the module</h2>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~
git clone <span class="nt">--depth</span> 1 <span class="nt">--branch</span> v0.2.3 https://github.com/M-Compressors/ngx_mzstd
<span class="nb">cd </span>ngx_mzstd
<span class="nv">NGINX_BUILD_DIR</span><span class="o">=</span><span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/nginx-</span><span class="nv">$ver</span><span class="s2">/objs"</span> cargo build <span class="nt">--release</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">v0.2.3</code> is the latest release at the time of writing; the
<a href="https://github.com/M-Compressors/ngx_mzstd/releases">release page</a> lists newer
ones. The module is written to <code class="language-plaintext highlighter-rouge">target/release/libngx_mzstd.so</code>.</p>

<h2 id="install-and-load-it">Install and load it</h2>

<p>Copy the module into nginx’s modules directory, which <code class="language-plaintext highlighter-rouge">nginx -V</code> shows as
<code class="language-plaintext highlighter-rouge">--modules-path</code>:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">moddir</span><span class="o">=</span><span class="si">$(</span>nginx <span class="nt">-V</span> 2&gt;&amp;1 | <span class="nb">grep</span> <span class="nt">-o</span> <span class="nt">--</span> <span class="s1">'--modules-path=[^ ]*'</span> | <span class="nb">cut</span> <span class="nt">-d</span><span class="o">=</span> <span class="nt">-f2</span><span class="si">)</span>
<span class="nb">sudo install</span> <span class="nt">-m</span> 644 target/release/libngx_mzstd.so <span class="s2">"</span><span class="nv">$moddir</span><span class="s2">/"</span>
</code></pre></div></div>

<p>Load it at the very top of <code class="language-plaintext highlighter-rouge">/etc/nginx/nginx.conf</code>:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">load_module</span> <span class="nc">modules/libngx</span><span class="s">_mzstd.so</span><span class="p">;</span>
</code></pre></div></div>

<p>Turn compression on as in the
<a href="/2026/install-ngx-mzstd-packages/#turn-compression-on">previous post</a>, then check and
restart:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nginx <span class="nt">-t</span>
<span class="nb">sudo </span>systemctl restart nginx
curl <span class="nt">-sS</span> <span class="nt">-o</span> /dev/null <span class="nt">-D</span> - <span class="nt">-H</span> <span class="s1">'Accept-Encoding: zstd'</span> http://localhost/ | <span class="nb">grep</span> <span class="nt">-i</span> content-encoding
</code></pre></div></div>

<h2 id="building-nginx-too">Building nginx too</h2>

<p>To build nginx yourself instead of installing it from nginx.org, configure it with
<code class="language-plaintext highlighter-rouge">--with-compat</code>. This builds nginx 1.30.6 into <code class="language-plaintext highlighter-rouge">/usr/local/nginx</code> on Ubuntu 24.04,
with the build tools from above installed:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~
curl <span class="nt">-O</span> https://nginx.org/download/nginx-1.30.6.tar.gz
<span class="nb">tar </span>xzf nginx-1.30.6.tar.gz
<span class="nb">cd </span>nginx-1.30.6
./configure <span class="nt">--prefix</span><span class="o">=</span>/usr/local/nginx <span class="nt">--with-compat</span> <span class="nt">--with-threads</span> <span class="se">\</span>
    <span class="nt">--with-http_ssl_module</span> <span class="nt">--with-http_v2_module</span>
make <span class="nt">-j</span><span class="s2">"</span><span class="si">$(</span><span class="nb">nproc</span><span class="si">)</span><span class="s2">"</span>
<span class="nb">sudo </span>make <span class="nb">install</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--with-threads</code> lets the module compress on thread pools. Build ngx_mzstd against
this tree with <code class="language-plaintext highlighter-rouge">NGINX_BUILD_DIR="$HOME/nginx-1.30.6/objs"</code> as above, copy
<code class="language-plaintext highlighter-rouge">libngx_mzstd.so</code> to <code class="language-plaintext highlighter-rouge">/usr/local/nginx/modules/</code>, and load it with
<code class="language-plaintext highlighter-rouge">load_module modules/libngx_mzstd.so;</code>.</p>

<h2 id="when-nginx-is-upgraded">When nginx is upgraded</h2>

<p>A module built for one nginx version does not load into another, so after every
nginx upgrade, download the new version’s source, configure it, build the module
again and restart nginx. A reload is not enough: the master process keeps the old
module loaded.</p>

<h2 id="errors-you-may-see">Errors you may see</h2>

<table>
  <thead>
    <tr>
      <th>Error</th>
      <th>Cause</th>
      <th>Fix</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">module "…" is not binary compatible</code></td>
      <td>The source was configured differently from the running nginx, or that nginx lacks <code class="language-plaintext highlighter-rouge">--with-compat</code>.</td>
      <td>Configure with exactly the arguments from <code class="language-plaintext highlighter-rouge">nginx -V</code>, with nginx from nginx.org or built with <code class="language-plaintext highlighter-rouge">--with-compat</code>.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">module "…" version 1030005 instead of 1030006</code></td>
      <td>The module was built against another nginx version.</td>
      <td>Build against the source of the version you run.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">module "ngx_http_zstd_filter_module" is already loaded</code></td>
      <td>tokers/zstd-nginx-module is still loaded; the modules have the same names.</td>
      <td>Remove its <code class="language-plaintext highlighter-rouge">load_module</code> line or package.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">the invalid value in --with-ld-opt</code> on RHEL</td>
      <td>The compiler specs used by nginx.org’s RPM builds are missing.</td>
      <td>Install <code class="language-plaintext highlighter-rouge">redhat-rpm-config</code>.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Unable to find libclang</code> during <code class="language-plaintext highlighter-rouge">cargo build</code></td>
      <td>libclang is not installed.</td>
      <td>Install <code class="language-plaintext highlighter-rouge">libclang-dev</code> or <code class="language-plaintext highlighter-rouge">clang-devel</code>.</td>
    </tr>
  </tbody>
</table>]]></content><author><name>Mikhail Panfilov</name></author><category term="nginx" /><category term="zstd" /><category term="zstandard" /><category term="linux" /><category term="rust" /><category term="build" /><summary type="html"><![CDATA[How to build ngx_mzstd from source against nginx from nginx.org's official repository, on Debian, Ubuntu, RHEL-compatible and other Linux systems, or against an nginx you compile yourself.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://m-compressors.github.io/assets/img/og/build-ngx-mzstd-from-source.png" /><media:content medium="image" url="https://m-compressors.github.io/assets/img/og/build-ngx-mzstd-from-source.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Building the ngx_mbrotli Brotli module for NGINX from source</title><link href="https://m-compressors.github.io/2026/build-ngx-mbrotli-from-source/" rel="alternate" type="text/html" title="Building the ngx_mbrotli Brotli module for NGINX from source" /><published>2026-10-22T10:00:00+03:00</published><updated>2026-10-22T10:00:00+03:00</updated><id>https://m-compressors.github.io/2026/build-ngx-mbrotli-from-source</id><content type="html" xml:base="https://m-compressors.github.io/2026/build-ngx-mbrotli-from-source/"><![CDATA[<p>The <a href="/2026/install-ngx-mbrotli-packages/">previous post</a> installed <strong>ngx_mbrotli</strong>
from packages. Build it yourself when there is no package for your system, when you
compile nginx yourself, or when you want a module version that has not been
packaged yet.</p>

<p>Every command here was run on fresh containers on 22 October 2026, against nginx.org’s
nginx on Debian 13 and AlmaLinux 9, and against nginx built from source on
Ubuntu 24.04. Each one ended with nginx answering <code class="language-plaintext highlighter-rouge">Content-Encoding: br</code>.</p>

<h2 id="install-nginx-from-nginxorg">Install nginx from nginx.org</h2>

<p>nginx loads a dynamic module only if it was built for the same nginx version and the
same configuration. nginx.org’s packages are built with <code class="language-plaintext highlighter-rouge">--with-compat</code>, which makes
a separately built module match them reliably, so on Alpine, Arch Linux, openSUSE and
every other system, install nginx from
<a href="https://nginx.org/en/linux_packages.html">nginx.org’s official repository</a> first.
Check that your nginx has the flag:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nginx <span class="nt">-V</span> 2&gt;&amp;1 | <span class="nb">grep</span> <span class="nt">-o</span> <span class="nt">--</span> <span class="nt">--with-compat</span>
</code></pre></div></div>

<p>The module also needs nginx 1.25.4 or newer. To build nginx yourself instead, see
<a href="#building-nginx-too">the end of this post</a>.</p>

<h2 id="install-the-build-tools">Install the build tools</h2>

<p>You need a C toolchain, git, libclang (the Rust bindings to nginx are generated from
its headers), and the headers nginx is compiled against.</p>

<p>Debian and Ubuntu:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>build-essential curl git libclang-dev libpcre2-dev libssl-dev zlib1g-dev
</code></pre></div></div>

<p>RHEL, AlmaLinux, Rocky Linux and Oracle Linux:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install </span>redhat-rpm-config gcc make git <span class="nb">tar </span>clang-devel pcre2-devel openssl-devel zlib-devel
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">redhat-rpm-config</code> provides the compiler specs that nginx.org’s RPM builds pass to
the compiler. Without it, <code class="language-plaintext highlighter-rouge">./configure</code> stops with
<code class="language-plaintext highlighter-rouge">the invalid value in --with-ld-opt="-Wl,-z,relro -Wl,-z,now -pie"</code>.</p>

<p>On other systems, install the same things with the system’s package manager: a C
compiler and make, git, curl, libclang, and the PCRE2, OpenSSL and zlib development
headers.</p>

<h2 id="install-rust">Install Rust</h2>

<p>ngx_mbrotli needs Rust 1.89 or newer. Distribution packages are often older: Debian 13
ships 1.85. Install Rust with <a href="https://rustup.rs">rustup</a> as the user who will build:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">--proto</span> <span class="s1">'=https'</span> <span class="nt">--tlsv1</span>.2 <span class="nt">-sSf</span> https://sh.rustup.rs | sh <span class="nt">-s</span> <span class="nt">--</span> <span class="nt">-y</span> <span class="nt">--profile</span> minimal
<span class="nb">.</span> <span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/.cargo/env"</span>
</code></pre></div></div>

<h2 id="configure-nginxs-source-like-your-nginx">Configure nginx’s source like your nginx</h2>

<p>Download the source of exactly the version you run and configure it with the
arguments your binary was built with. Only <code class="language-plaintext highlighter-rouge">./configure</code> is needed: the module reads
its output in <code class="language-plaintext highlighter-rouge">objs/</code>, and nothing is compiled or installed.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">ver</span><span class="o">=</span><span class="si">$(</span>nginx <span class="nt">-v</span> 2&gt;&amp;1 | <span class="nb">sed</span> <span class="s1">'s|.*nginx/||'</span><span class="si">)</span>
<span class="nb">cd</span> ~
curl <span class="nt">-O</span> <span class="s2">"https://nginx.org/download/nginx-</span><span class="nv">$ver</span><span class="s2">.tar.gz"</span>
<span class="nb">tar </span>xzf <span class="s2">"nginx-</span><span class="nv">$ver</span><span class="s2">.tar.gz"</span>
<span class="nb">cd</span> <span class="s2">"nginx-</span><span class="nv">$ver</span><span class="s2">"</span>
<span class="nb">eval</span> <span class="s2">"./configure </span><span class="si">$(</span>nginx <span class="nt">-V</span> 2&gt;&amp;1 | <span class="nb">sed</span> <span class="nt">-n</span> <span class="s1">'s/^configure arguments: //p'</span><span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">eval</code> keeps quoted arguments such as <code class="language-plaintext highlighter-rouge">--with-cc-opt='-O2 -g ...'</code> in one piece. If
<code class="language-plaintext highlighter-rouge">./configure</code> reports a missing library, install its development package and run it
again.</p>

<h2 id="build-the-module">Build the module</h2>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~
git clone <span class="nt">--depth</span> 1 <span class="nt">--branch</span> v0.4.5 https://github.com/M-Compressors/ngx_mbrotli
<span class="nb">cd </span>ngx_mbrotli
<span class="nv">NGINX_BUILD_DIR</span><span class="o">=</span><span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/nginx-</span><span class="nv">$ver</span><span class="s2">/objs"</span> cargo build <span class="nt">--release</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">v0.4.5</code> is the latest release at the time of writing; the
<a href="https://github.com/M-Compressors/ngx_mbrotli/releases">release page</a> lists newer
ones. The module is written to <code class="language-plaintext highlighter-rouge">target/release/libngx_mbrotli.so</code>. It contains the
mbrotli encoder, so no brotli C library is needed.</p>

<h2 id="install-and-load-it">Install and load it</h2>

<p>Copy the module into nginx’s modules directory, which <code class="language-plaintext highlighter-rouge">nginx -V</code> shows as
<code class="language-plaintext highlighter-rouge">--modules-path</code>:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">moddir</span><span class="o">=</span><span class="si">$(</span>nginx <span class="nt">-V</span> 2&gt;&amp;1 | <span class="nb">grep</span> <span class="nt">-o</span> <span class="nt">--</span> <span class="s1">'--modules-path=[^ ]*'</span> | <span class="nb">cut</span> <span class="nt">-d</span><span class="o">=</span> <span class="nt">-f2</span><span class="si">)</span>
<span class="nb">sudo install</span> <span class="nt">-m</span> 644 target/release/libngx_mbrotli.so <span class="s2">"</span><span class="nv">$moddir</span><span class="s2">/"</span>
</code></pre></div></div>

<p>Load it at the very top of <code class="language-plaintext highlighter-rouge">/etc/nginx/nginx.conf</code>:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">load_module</span> <span class="nc">modules/libngx</span><span class="s">_mbrotli.so</span><span class="p">;</span>
</code></pre></div></div>

<p>Turn compression on as in the
<a href="/2026/install-ngx-mbrotli-packages/#turn-compression-on">previous post</a>, then check
and restart:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nginx <span class="nt">-t</span>
<span class="nb">sudo </span>systemctl restart nginx
curl <span class="nt">-sS</span> <span class="nt">-o</span> /dev/null <span class="nt">-D</span> - <span class="nt">-H</span> <span class="s1">'Accept-Encoding: br'</span> http://localhost/ | <span class="nb">grep</span> <span class="nt">-i</span> content-encoding
</code></pre></div></div>

<h2 id="building-nginx-too">Building nginx too</h2>

<p>To build nginx yourself instead of installing it from nginx.org, configure it with
<code class="language-plaintext highlighter-rouge">--with-compat</code>. This builds nginx 1.30.6 into <code class="language-plaintext highlighter-rouge">/usr/local/nginx</code> on Ubuntu 24.04,
with the build tools from above installed:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> ~
curl <span class="nt">-O</span> https://nginx.org/download/nginx-1.30.6.tar.gz
<span class="nb">tar </span>xzf nginx-1.30.6.tar.gz
<span class="nb">cd </span>nginx-1.30.6
./configure <span class="nt">--prefix</span><span class="o">=</span>/usr/local/nginx <span class="nt">--with-compat</span> <span class="nt">--with-threads</span> <span class="se">\</span>
    <span class="nt">--with-http_ssl_module</span> <span class="nt">--with-http_v2_module</span>
make <span class="nt">-j</span><span class="s2">"</span><span class="si">$(</span><span class="nb">nproc</span><span class="si">)</span><span class="s2">"</span>
<span class="nb">sudo </span>make <span class="nb">install</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--with-threads</code> lets the module compress on thread pools. Build ngx_mbrotli against
this tree with <code class="language-plaintext highlighter-rouge">NGINX_BUILD_DIR="$HOME/nginx-1.30.6/objs"</code> as above, copy
<code class="language-plaintext highlighter-rouge">libngx_mbrotli.so</code> to <code class="language-plaintext highlighter-rouge">/usr/local/nginx/modules/</code>, and load it with
<code class="language-plaintext highlighter-rouge">load_module modules/libngx_mbrotli.so;</code>.</p>

<h2 id="when-nginx-is-upgraded">When nginx is upgraded</h2>

<p>A module built for one nginx version does not load into another, so after every
nginx upgrade, download the new version’s source, configure it, build the module
again and restart nginx. A reload is not enough: the master process keeps the old
module loaded.</p>

<h2 id="errors-you-may-see">Errors you may see</h2>

<table>
  <thead>
    <tr>
      <th>Error</th>
      <th>Cause</th>
      <th>Fix</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">module "…" is not binary compatible</code></td>
      <td>The source was configured differently from the running nginx, or that nginx lacks <code class="language-plaintext highlighter-rouge">--with-compat</code>.</td>
      <td>Configure with exactly the arguments from <code class="language-plaintext highlighter-rouge">nginx -V</code>, with nginx from nginx.org or built with <code class="language-plaintext highlighter-rouge">--with-compat</code>.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">module "…" version 1030005 instead of 1030006</code></td>
      <td>The module was built against another nginx version.</td>
      <td>Build against the source of the version you run.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">module "ngx_http_brotli_filter_module" is already loaded</code></td>
      <td>google/ngx_brotli is still loaded; the modules have the same names.</td>
      <td>Remove its <code class="language-plaintext highlighter-rouge">load_module</code> line or package.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">the invalid value in --with-ld-opt</code> on RHEL</td>
      <td>The compiler specs used by nginx.org’s RPM builds are missing.</td>
      <td>Install <code class="language-plaintext highlighter-rouge">redhat-rpm-config</code>.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Unable to find libclang</code> during <code class="language-plaintext highlighter-rouge">cargo build</code></td>
      <td>libclang is not installed.</td>
      <td>Install <code class="language-plaintext highlighter-rouge">libclang-dev</code> or <code class="language-plaintext highlighter-rouge">clang-devel</code>.</td>
    </tr>
  </tbody>
</table>

<p>To add Zstandard as well, <a href="/2026/build-ngx-mzstd-from-source/">build ngx_mzstd</a> the
same way against the same nginx source.</p>]]></content><author><name>Mikhail Panfilov</name></author><category term="nginx" /><category term="brotli" /><category term="linux" /><category term="rust" /><category term="build" /><summary type="html"><![CDATA[How to build ngx_mbrotli from source against nginx from nginx.org's official repository, on Debian, Ubuntu, RHEL-compatible and other Linux systems, or against an nginx you compile yourself.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://m-compressors.github.io/assets/img/og/build-ngx-mbrotli-from-source.png" /><media:content medium="image" url="https://m-compressors.github.io/assets/img/og/build-ngx-mbrotli-from-source.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Installing Zstandard for NGINX from packages: ngx_mzstd</title><link href="https://m-compressors.github.io/2026/install-ngx-mzstd-packages/" rel="alternate" type="text/html" title="Installing Zstandard for NGINX from packages: ngx_mzstd" /><published>2026-10-22T09:30:00+03:00</published><updated>2026-10-22T09:30:00+03:00</updated><id>https://m-compressors.github.io/2026/install-ngx-mzstd-packages</id><content type="html" xml:base="https://m-compressors.github.io/2026/install-ngx-mzstd-packages/"><![CDATA[<p><strong>ngx_mzstd</strong> adds Zstandard compression to NGINX. It is a drop-in replacement for
tokers/zstd-nginx-module, written in Rust on top of the
<a href="https://github.com/M-Compressors/mzstd">mzstd</a> encoder, so it needs no zstd C
library, and it can compress on NGINX thread pools. This post installs it from
packages. If there is no package for your system,
<a href="/2026/build-ngx-mzstd-from-source/">build it from source</a>.</p>

<p>Every command below was run on 22 October 2026 on fresh containers of Debian 13 and
AlmaLinux 10 with their own nginx, and of Ubuntu 24.04, Rocky Linux 9 and Amazon
Linux 2023 with nginx.org’s nginx. Each one ended with nginx answering <code class="language-plaintext highlighter-rouge">Content-Encoding: zstd</code>.</p>

<h2 id="find-out-which-nginx-you-run">Find out which nginx you run</h2>

<p>Each module package is built against one exact nginx package, so the package to
install depends on where your nginx came from:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nginx <span class="nt">-v</span>
apt-cache policy nginx    <span class="c"># Debian, Ubuntu</span>
rpm <span class="nt">-q</span> nginx              <span class="c"># RHEL and derivatives, Amazon Linux</span>
</code></pre></div></div>

<p>nginx.org’s packages have versions like <code class="language-plaintext highlighter-rouge">1.30.6-1~noble</code> on Debian and Ubuntu and
end in <code class="language-plaintext highlighter-rouge">.ngx</code> on RPM systems, as in <code class="language-plaintext highlighter-rouge">nginx-1.30.6-1.el9.ngx</code>. Anything else is the
distribution’s own nginx.</p>

<table>
  <thead>
    <tr>
      <th>System</th>
      <th>nginx from</th>
      <th>Package</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Debian 12, 13; Ubuntu 22.04, 24.04, 26.04</td>
      <td>nginx.org</td>
      <td><code class="language-plaintext highlighter-rouge">nginx-module-mzstd</code></td>
    </tr>
    <tr>
      <td>Debian 13, Ubuntu 26.04</td>
      <td>the distribution</td>
      <td><code class="language-plaintext highlighter-rouge">libnginx-mod-http-mzstd</code></td>
    </tr>
    <tr>
      <td>RHEL, AlmaLinux, Rocky Linux, Oracle Linux 8–10; Amazon Linux 2023</td>
      <td>nginx.org</td>
      <td><code class="language-plaintext highlighter-rouge">nginx-module-mzstd</code></td>
    </tr>
    <tr>
      <td>RHEL, AlmaLinux, Rocky Linux 10</td>
      <td>the distribution</td>
      <td><code class="language-plaintext highlighter-rouge">nginx-mod-mzstd</code></td>
    </tr>
  </tbody>
</table>

<p>All packages are built for amd64 and arm64. The module needs nginx 1.25.4 or newer,
which rules out the nginx that Debian 12, Ubuntu 22.04 and 24.04, and RHEL 8 and 9
ship: on those systems, install nginx from nginx.org as shown below.</p>

<h2 id="debian-and-ubuntu-with-nginxorgs-nginx">Debian and Ubuntu with nginx.org’s nginx</h2>

<p>If nginx is not from nginx.org yet, add its repository first. These are
<a href="https://nginx.org/en/linux_packages.html">nginx.org’s instructions</a> for Ubuntu; on
Debian, replace <code class="language-plaintext highlighter-rouge">ubuntu-keyring</code> with <code class="language-plaintext highlighter-rouge">debian-archive-keyring</code> and <code class="language-plaintext highlighter-rouge">ubuntu</code> in the
URL with <code class="language-plaintext highlighter-rouge">debian</code>.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>curl gnupg2 ca-certificates lsb-release ubuntu-keyring
curl https://nginx.org/keys/nginx_signing.key | gpg <span class="nt">--dearmor</span> <span class="se">\</span>
    | <span class="nb">sudo tee</span> /usr/share/keyrings/nginx-archive-keyring.gpg <span class="o">&gt;</span>/dev/null
<span class="nb">echo</span> <span class="s2">"deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] </span><span class="se">\</span><span class="s2">
https://nginx.org/packages/ubuntu </span><span class="sb">`</span>lsb_release <span class="nt">-cs</span><span class="sb">`</span><span class="s2"> nginx"</span> <span class="se">\</span>
    | <span class="nb">sudo tee</span> /etc/apt/sources.list.d/nginx.list
<span class="nb">printf</span> <span class="s2">"Package: *</span><span class="se">\n</span><span class="s2">Pin: origin nginx.org</span><span class="se">\n</span><span class="s2">Pin: release o=nginx</span><span class="se">\n</span><span class="s2">Pin-Priority: 900</span><span class="se">\n</span><span class="s2">"</span> <span class="se">\</span>
    | <span class="nb">sudo tee</span> /etc/apt/preferences.d/99nginx
<span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install </span>nginx
</code></pre></div></div>

<p>Then add the module repository and install the module:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-1sLf</span> <span class="s1">'https://dl.cloudsmith.io/public/m-compressors/ngx_mzstd/setup.deb.sh'</span> | <span class="nb">sudo</span> <span class="nt">-E</span> bash
<span class="nb">sudo </span>apt-get <span class="nb">install </span>nginx-module-mzstd
</code></pre></div></div>

<p>The setup script comes from <a href="https://cloudsmith.io/~m-compressors/repos/">Cloudsmith</a>,
which hosts the repository. It adds the repository’s signing key and an apt source;
download it and read it first if you prefer.</p>

<p>Like nginx.org’s own <code class="language-plaintext highlighter-rouge">nginx-module-*</code> packages, this one installs the module without
enabling it. Add this line to the very top of <code class="language-plaintext highlighter-rouge">/etc/nginx/nginx.conf</code>:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">load_module</span> <span class="nc">modules/ngx</span><span class="s">_mzstd.so</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="debian-13-and-ubuntu-2604-with-their-own-nginx">Debian 13 and Ubuntu 26.04 with their own nginx</h2>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>nginx
curl <span class="nt">-1sLf</span> <span class="s1">'https://dl.cloudsmith.io/public/m-compressors/ngx_mzstd/setup.deb.sh'</span> | <span class="nb">sudo</span> <span class="nt">-E</span> bash
<span class="nb">sudo </span>apt-get <span class="nb">install </span>libnginx-mod-http-mzstd
</code></pre></div></div>

<p>As with Debian’s own module packages, the module is enabled on installation through
<code class="language-plaintext highlighter-rouge">/etc/nginx/modules-enabled/</code>, so there is no <code class="language-plaintext highlighter-rouge">load_module</code> line to add.</p>

<h2 id="rhel-almalinux-rocky-linux-and-amazon-linux-with-nginxorgs-nginx">RHEL, AlmaLinux, Rocky Linux and Amazon Linux with nginx.org’s nginx</h2>

<p>If nginx is not from nginx.org yet, create <code class="language-plaintext highlighter-rouge">/etc/yum.repos.d/nginx.repo</code>:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[nginx-stable]</span>
<span class="py">name</span><span class="p">=</span><span class="s">nginx stable repo</span>
<span class="py">baseurl</span><span class="p">=</span><span class="s">https://nginx.org/packages/centos/$releasever/$basearch/</span>
<span class="py">gpgcheck</span><span class="p">=</span><span class="s">1</span>
<span class="py">enabled</span><span class="p">=</span><span class="s">1</span>
<span class="py">gpgkey</span><span class="p">=</span><span class="s">https://nginx.org/keys/nginx_signing.key</span>
<span class="py">module_hotfixes</span><span class="p">=</span><span class="s">true</span>
</code></pre></div></div>

<p>On Amazon Linux 2023 the <code class="language-plaintext highlighter-rouge">baseurl</code> is
<code class="language-plaintext highlighter-rouge">https://nginx.org/packages/amzn/2023/$basearch/</code>, and nginx.org also adds
<code class="language-plaintext highlighter-rouge">priority=9</code>. Then install nginx and the module:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install </span>nginx
curl <span class="nt">-1sLf</span> <span class="s1">'https://dl.cloudsmith.io/public/m-compressors/ngx_mzstd/setup.rpm.sh'</span> | <span class="nb">sudo</span> <span class="nt">-E</span> bash
<span class="nb">sudo </span>dnf <span class="nb">install </span>nginx-module-mzstd
</code></pre></div></div>

<p>dnf asks you to accept the repository’s signing key. Add the <code class="language-plaintext highlighter-rouge">load_module</code> line to
the top of <code class="language-plaintext highlighter-rouge">/etc/nginx/nginx.conf</code>:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">load_module</span> <span class="nc">modules/ngx</span><span class="s">_mzstd.so</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="rhel-10-with-its-own-nginx">RHEL 10 with its own nginx</h2>

<p>This covers the default AppStream nginx on RHEL 10, AlmaLinux 10 and Rocky Linux 10:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install </span>nginx
curl <span class="nt">-1sLf</span> <span class="s1">'https://dl.cloudsmith.io/public/m-compressors/ngx_mzstd/setup.rpm.sh'</span> | <span class="nb">sudo</span> <span class="nt">-E</span> bash
<span class="nb">sudo </span>dnf <span class="nb">install </span>nginx-mod-mzstd
</code></pre></div></div>

<p>The package enables the module through <code class="language-plaintext highlighter-rouge">/usr/share/nginx/modules/mod-mzstd.conf</code>,
which the stock <code class="language-plaintext highlighter-rouge">nginx.conf</code> includes.</p>

<h2 id="nginxorg-mainline">nginx.org mainline</h2>

<p>For nginx.org’s mainline nginx, use the <code class="language-plaintext highlighter-rouge">ngx_mzstd-mainline</code> repository. The
package name stays the same:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-1sLf</span> <span class="s1">'https://dl.cloudsmith.io/public/m-compressors/ngx_mzstd-mainline/setup.deb.sh'</span> | <span class="nb">sudo</span> <span class="nt">-E</span> bash
</code></pre></div></div>

<p>Use <code class="language-plaintext highlighter-rouge">setup.rpm.sh</code> instead of <code class="language-plaintext highlighter-rouge">setup.deb.sh</code> on RPM systems.</p>

<h2 id="turn-compression-on">Turn compression on</h2>

<p>The module does nothing until compression is switched on. Put this in
<code class="language-plaintext highlighter-rouge">/etc/nginx/conf.d/zstd.conf</code>, which every nginx above includes inside <code class="language-plaintext highlighter-rouge">http</code>:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">zstd</span> <span class="no">on</span><span class="p">;</span>
<span class="k">zstd_comp_level</span> <span class="mi">3</span><span class="p">;</span>
<span class="k">zstd_types</span> <span class="nc">text/plain</span> <span class="nc">text/css</span> <span class="nc">application/javascript</span> <span class="nc">application/json</span> <span class="nc">image/svg</span><span class="s">+xml</span><span class="p">;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">text/html</code> is compressed by default and does not need to be listed. Check the
configuration, restart nginx and ask for Zstandard:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nginx <span class="nt">-t</span>
<span class="nb">sudo </span>systemctl restart nginx
curl <span class="nt">-sS</span> <span class="nt">-o</span> /dev/null <span class="nt">-D</span> - <span class="nt">-H</span> <span class="s1">'Accept-Encoding: zstd'</span> http://localhost/ | <span class="nb">grep</span> <span class="nt">-i</span> content-encoding
</code></pre></div></div>

<p>The answer is <code class="language-plaintext highlighter-rouge">Content-Encoding: zstd</code>. If you also run
<a href="/2026/install-ngx-mbrotli-packages/">ngx_mbrotli</a>, a browser that accepts both gets
Zstandard, in either <code class="language-plaintext highlighter-rouge">load_module</code> order.</p>

<p>To keep large responses from blocking other requests, the module can compress on a
thread pool with <code class="language-plaintext highlighter-rouge">zstd_thread_pool default;</code>. This needs nginx built with
<code class="language-plaintext highlighter-rouge">--with-threads</code>, which <code class="language-plaintext highlighter-rouge">nginx -V</code> shows. The
<a href="https://github.com/M-Compressors/ngx_mzstd#benchmarks">ngx_mzstd README</a> has the
numbers and the trade-offs, including the extra memory a pool needs.</p>

<h2 id="upgrades">Upgrades</h2>

<p>The module package requires the exact nginx package it was built for, so nginx and
the module are upgraded together. The version shows the pairing: nginx.org packages
put nginx first, as in <code class="language-plaintext highlighter-rouge">1.30.6+0.2.3</code>, and the distribution packages put the module
first, as in <code class="language-plaintext highlighter-rouge">0.2.3+1.26.3</code>.</p>

<p>After an upgrade, <strong>restart</strong> nginx rather than reloading it. A reload keeps the
master process, and the master keeps the old module loaded.</p>

<h2 id="moving-from-tokerszstd-nginx-module">Moving from tokers/zstd-nginx-module</h2>

<p>Existing <code class="language-plaintext highlighter-rouge">zstd*</code> directives keep working unchanged. The packages conflict with the
old module’s, such as <code class="language-plaintext highlighter-rouge">nginx-module-zstd</code>, so remove those first. Also
delete any <code class="language-plaintext highlighter-rouge">load_module</code> lines that load the old <code class="language-plaintext highlighter-rouge">.so</code> files: the old and new
modules have the same names, and nginx refuses to load a module name twice.</p>]]></content><author><name>Mikhail Panfilov</name></author><category term="nginx" /><category term="zstd" /><category term="zstandard" /><category term="linux" /><category term="debian" /><category term="ubuntu" /><category term="rhel" /><summary type="html"><![CDATA[How to install the ngx_mzstd Zstandard module with apt or dnf on Debian, Ubuntu, RHEL, AlmaLinux, Rocky Linux and Amazon Linux, for nginx.org's nginx and for the distribution's own.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://m-compressors.github.io/assets/img/og/install-ngx-mzstd-packages.png" /><media:content medium="image" url="https://m-compressors.github.io/assets/img/og/install-ngx-mzstd-packages.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Installing Brotli for NGINX from packages: ngx_mbrotli</title><link href="https://m-compressors.github.io/2026/install-ngx-mbrotli-packages/" rel="alternate" type="text/html" title="Installing Brotli for NGINX from packages: ngx_mbrotli" /><published>2026-10-22T09:00:00+03:00</published><updated>2026-10-22T09:00:00+03:00</updated><id>https://m-compressors.github.io/2026/install-ngx-mbrotli-packages</id><content type="html" xml:base="https://m-compressors.github.io/2026/install-ngx-mbrotli-packages/"><![CDATA[<p><strong>ngx_mbrotli</strong> adds Brotli compression to NGINX. It is a drop-in replacement for
google/ngx_brotli, written in Rust on top of the
<a href="https://github.com/M-Compressors/mbrotli">mbrotli</a> encoder, and it can compress on
NGINX thread pools. This post installs it from packages. If there is no package for
your system, <a href="/2026/build-ngx-mbrotli-from-source/">build it from source</a>.</p>

<p>Every command below was run on 22 October 2026 on fresh containers of Debian 13 and
AlmaLinux 10 with their own nginx, and of Ubuntu 24.04, Rocky Linux 9 and Amazon
Linux 2023 with nginx.org’s nginx. Each one ended with nginx answering <code class="language-plaintext highlighter-rouge">Content-Encoding: br</code>.</p>

<h2 id="find-out-which-nginx-you-run">Find out which nginx you run</h2>

<p>Each module package is built against one exact nginx package, so the package to
install depends on where your nginx came from:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nginx <span class="nt">-v</span>
apt-cache policy nginx    <span class="c"># Debian, Ubuntu</span>
rpm <span class="nt">-q</span> nginx              <span class="c"># RHEL and derivatives, Amazon Linux</span>
</code></pre></div></div>

<p>nginx.org’s packages have versions like <code class="language-plaintext highlighter-rouge">1.30.6-1~noble</code> on Debian and Ubuntu and
end in <code class="language-plaintext highlighter-rouge">.ngx</code> on RPM systems, as in <code class="language-plaintext highlighter-rouge">nginx-1.30.6-1.el9.ngx</code>. Anything else is the
distribution’s own nginx.</p>

<table>
  <thead>
    <tr>
      <th>System</th>
      <th>nginx from</th>
      <th>Package</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Debian 12, 13; Ubuntu 22.04, 24.04, 26.04</td>
      <td>nginx.org</td>
      <td><code class="language-plaintext highlighter-rouge">nginx-module-mbrotli</code></td>
    </tr>
    <tr>
      <td>Debian 13, Ubuntu 26.04</td>
      <td>the distribution</td>
      <td><code class="language-plaintext highlighter-rouge">libnginx-mod-http-mbrotli</code></td>
    </tr>
    <tr>
      <td>RHEL, AlmaLinux, Rocky Linux, Oracle Linux 8–10; Amazon Linux 2023</td>
      <td>nginx.org</td>
      <td><code class="language-plaintext highlighter-rouge">nginx-module-mbrotli</code></td>
    </tr>
    <tr>
      <td>RHEL, AlmaLinux, Rocky Linux 10</td>
      <td>the distribution</td>
      <td><code class="language-plaintext highlighter-rouge">nginx-mod-mbrotli</code></td>
    </tr>
  </tbody>
</table>

<p>All packages are built for amd64 and arm64. The module needs nginx 1.25.4 or newer,
which rules out the nginx that Debian 12, Ubuntu 22.04 and 24.04, and RHEL 8 and 9
ship: on those systems, install nginx from nginx.org as shown below.</p>

<h2 id="debian-and-ubuntu-with-nginxorgs-nginx">Debian and Ubuntu with nginx.org’s nginx</h2>

<p>If nginx is not from nginx.org yet, add its repository first. These are
<a href="https://nginx.org/en/linux_packages.html">nginx.org’s instructions</a> for Ubuntu; on
Debian, replace <code class="language-plaintext highlighter-rouge">ubuntu-keyring</code> with <code class="language-plaintext highlighter-rouge">debian-archive-keyring</code> and <code class="language-plaintext highlighter-rouge">ubuntu</code> in the
URL with <code class="language-plaintext highlighter-rouge">debian</code>.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>curl gnupg2 ca-certificates lsb-release ubuntu-keyring
curl https://nginx.org/keys/nginx_signing.key | gpg <span class="nt">--dearmor</span> <span class="se">\</span>
    | <span class="nb">sudo tee</span> /usr/share/keyrings/nginx-archive-keyring.gpg <span class="o">&gt;</span>/dev/null
<span class="nb">echo</span> <span class="s2">"deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] </span><span class="se">\</span><span class="s2">
https://nginx.org/packages/ubuntu </span><span class="sb">`</span>lsb_release <span class="nt">-cs</span><span class="sb">`</span><span class="s2"> nginx"</span> <span class="se">\</span>
    | <span class="nb">sudo tee</span> /etc/apt/sources.list.d/nginx.list
<span class="nb">printf</span> <span class="s2">"Package: *</span><span class="se">\n</span><span class="s2">Pin: origin nginx.org</span><span class="se">\n</span><span class="s2">Pin: release o=nginx</span><span class="se">\n</span><span class="s2">Pin-Priority: 900</span><span class="se">\n</span><span class="s2">"</span> <span class="se">\</span>
    | <span class="nb">sudo tee</span> /etc/apt/preferences.d/99nginx
<span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install </span>nginx
</code></pre></div></div>

<p>Then add the module repository and install the module:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-1sLf</span> <span class="s1">'https://dl.cloudsmith.io/public/m-compressors/ngx_mbrotli/setup.deb.sh'</span> | <span class="nb">sudo</span> <span class="nt">-E</span> bash
<span class="nb">sudo </span>apt-get <span class="nb">install </span>nginx-module-mbrotli
</code></pre></div></div>

<p>The setup script comes from <a href="https://cloudsmith.io/~m-compressors/repos/">Cloudsmith</a>,
which hosts the repository. It adds the repository’s signing key and an apt source;
download it and read it first if you prefer.</p>

<p>Like nginx.org’s own <code class="language-plaintext highlighter-rouge">nginx-module-*</code> packages, this one installs the module without
enabling it. Add this line to the very top of <code class="language-plaintext highlighter-rouge">/etc/nginx/nginx.conf</code>:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">load_module</span> <span class="nc">modules/ngx</span><span class="s">_mbrotli.so</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="debian-13-and-ubuntu-2604-with-their-own-nginx">Debian 13 and Ubuntu 26.04 with their own nginx</h2>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>nginx
curl <span class="nt">-1sLf</span> <span class="s1">'https://dl.cloudsmith.io/public/m-compressors/ngx_mbrotli/setup.deb.sh'</span> | <span class="nb">sudo</span> <span class="nt">-E</span> bash
<span class="nb">sudo </span>apt-get <span class="nb">install </span>libnginx-mod-http-mbrotli
</code></pre></div></div>

<p>As with Debian’s own module packages, the module is enabled on installation through
<code class="language-plaintext highlighter-rouge">/etc/nginx/modules-enabled/</code>, so there is no <code class="language-plaintext highlighter-rouge">load_module</code> line to add.</p>

<h2 id="rhel-almalinux-rocky-linux-and-amazon-linux-with-nginxorgs-nginx">RHEL, AlmaLinux, Rocky Linux and Amazon Linux with nginx.org’s nginx</h2>

<p>If nginx is not from nginx.org yet, create <code class="language-plaintext highlighter-rouge">/etc/yum.repos.d/nginx.repo</code>:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[nginx-stable]</span>
<span class="py">name</span><span class="p">=</span><span class="s">nginx stable repo</span>
<span class="py">baseurl</span><span class="p">=</span><span class="s">https://nginx.org/packages/centos/$releasever/$basearch/</span>
<span class="py">gpgcheck</span><span class="p">=</span><span class="s">1</span>
<span class="py">enabled</span><span class="p">=</span><span class="s">1</span>
<span class="py">gpgkey</span><span class="p">=</span><span class="s">https://nginx.org/keys/nginx_signing.key</span>
<span class="py">module_hotfixes</span><span class="p">=</span><span class="s">true</span>
</code></pre></div></div>

<p>On Amazon Linux 2023 the <code class="language-plaintext highlighter-rouge">baseurl</code> is
<code class="language-plaintext highlighter-rouge">https://nginx.org/packages/amzn/2023/$basearch/</code>, and nginx.org also adds
<code class="language-plaintext highlighter-rouge">priority=9</code>. Then install nginx and the module:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install </span>nginx
curl <span class="nt">-1sLf</span> <span class="s1">'https://dl.cloudsmith.io/public/m-compressors/ngx_mbrotli/setup.rpm.sh'</span> | <span class="nb">sudo</span> <span class="nt">-E</span> bash
<span class="nb">sudo </span>dnf <span class="nb">install </span>nginx-module-mbrotli
</code></pre></div></div>

<p>dnf asks you to accept the repository’s signing key. Add the <code class="language-plaintext highlighter-rouge">load_module</code> line to
the top of <code class="language-plaintext highlighter-rouge">/etc/nginx/nginx.conf</code>:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">load_module</span> <span class="nc">modules/ngx</span><span class="s">_mbrotli.so</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="rhel-10-with-its-own-nginx">RHEL 10 with its own nginx</h2>

<p>This covers the default AppStream nginx on RHEL 10, AlmaLinux 10 and Rocky Linux 10:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install </span>nginx
curl <span class="nt">-1sLf</span> <span class="s1">'https://dl.cloudsmith.io/public/m-compressors/ngx_mbrotli/setup.rpm.sh'</span> | <span class="nb">sudo</span> <span class="nt">-E</span> bash
<span class="nb">sudo </span>dnf <span class="nb">install </span>nginx-mod-mbrotli
</code></pre></div></div>

<p>The package enables the module through <code class="language-plaintext highlighter-rouge">/usr/share/nginx/modules/mod-mbrotli.conf</code>,
which the stock <code class="language-plaintext highlighter-rouge">nginx.conf</code> includes.</p>

<h2 id="nginxorg-mainline">nginx.org mainline</h2>

<p>For nginx.org’s mainline nginx, use the <code class="language-plaintext highlighter-rouge">ngx_mbrotli-mainline</code> repository. The
package name stays the same:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-1sLf</span> <span class="s1">'https://dl.cloudsmith.io/public/m-compressors/ngx_mbrotli-mainline/setup.deb.sh'</span> | <span class="nb">sudo</span> <span class="nt">-E</span> bash
</code></pre></div></div>

<p>Use <code class="language-plaintext highlighter-rouge">setup.rpm.sh</code> instead of <code class="language-plaintext highlighter-rouge">setup.deb.sh</code> on RPM systems.</p>

<h2 id="turn-compression-on">Turn compression on</h2>

<p>The module does nothing until compression is switched on. Put this in
<code class="language-plaintext highlighter-rouge">/etc/nginx/conf.d/brotli.conf</code>, which every nginx above includes inside <code class="language-plaintext highlighter-rouge">http</code>:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">brotli</span> <span class="no">on</span><span class="p">;</span>
<span class="k">brotli_comp_level</span> <span class="mi">5</span><span class="p">;</span>
<span class="k">brotli_types</span> <span class="nc">text/plain</span> <span class="nc">text/css</span> <span class="nc">application/javascript</span> <span class="nc">application/json</span> <span class="nc">image/svg</span><span class="s">+xml</span><span class="p">;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">text/html</code> is compressed by default and does not need to be listed. Check the
configuration, restart nginx and ask for Brotli:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nginx <span class="nt">-t</span>
<span class="nb">sudo </span>systemctl restart nginx
curl <span class="nt">-sS</span> <span class="nt">-o</span> /dev/null <span class="nt">-D</span> - <span class="nt">-H</span> <span class="s1">'Accept-Encoding: br'</span> http://localhost/ | <span class="nb">grep</span> <span class="nt">-i</span> content-encoding
</code></pre></div></div>

<p>The answer is <code class="language-plaintext highlighter-rouge">Content-Encoding: br</code>.</p>

<p>To keep large responses from blocking other requests, the module can compress on a
thread pool with <code class="language-plaintext highlighter-rouge">brotli_thread_pool default;</code>. This needs nginx built with
<code class="language-plaintext highlighter-rouge">--with-threads</code>, which <code class="language-plaintext highlighter-rouge">nginx -V</code> shows. The
<a href="https://github.com/M-Compressors/ngx_mbrotli#benchmarks">ngx_mbrotli README</a> has
the numbers and the trade-offs, including the extra memory a pool needs.</p>

<h2 id="upgrades">Upgrades</h2>

<p>The module package requires the exact nginx package it was built for, so nginx and
the module are upgraded together. The version shows the pairing: nginx.org packages
put nginx first, as in <code class="language-plaintext highlighter-rouge">1.30.6+0.4.5</code>, and the distribution packages put the module
first, as in <code class="language-plaintext highlighter-rouge">0.4.5+1.26.3</code>.</p>

<p>After an upgrade, <strong>restart</strong> nginx rather than reloading it. A reload keeps the
master process, and the master keeps the old module loaded.</p>

<h2 id="moving-from-googlengx_brotli">Moving from google/ngx_brotli</h2>

<p>Existing <code class="language-plaintext highlighter-rouge">brotli*</code> directives keep working unchanged. The packages conflict with
google/ngx_brotli’s, such as <code class="language-plaintext highlighter-rouge">nginx-module-brotli</code> or Debian’s
<code class="language-plaintext highlighter-rouge">libnginx-mod-http-brotli-filter</code> and <code class="language-plaintext highlighter-rouge">libnginx-mod-http-brotli-static</code>, so remove
those first. Also delete any <code class="language-plaintext highlighter-rouge">load_module</code> lines that load the old <code class="language-plaintext highlighter-rouge">.so</code> files: the
old and new modules have the same names, and nginx refuses to load a module name
twice.</p>

<h2 id="zstandard-too">Zstandard too</h2>

<p>The sibling module ngx_mzstd adds Zstandard the same way:
<a href="/2026/install-ngx-mzstd-packages/">install it from packages</a>.</p>]]></content><author><name>Mikhail Panfilov</name></author><category term="nginx" /><category term="brotli" /><category term="linux" /><category term="debian" /><category term="ubuntu" /><category term="rhel" /><summary type="html"><![CDATA[How to install the ngx_mbrotli Brotli module with apt or dnf on Debian, Ubuntu, RHEL, AlmaLinux, Rocky Linux and Amazon Linux, for nginx.org's nginx and for the distribution's own.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://m-compressors.github.io/assets/img/og/install-ngx-mbrotli-packages.png" /><media:content medium="image" url="https://m-compressors.github.io/assets/img/og/install-ngx-mbrotli-packages.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>