gzip compression in nginx: where speed and traffic are lost
Compression is enabled, the directive is set in the Nginx configuration, the restart is done — but the browser still receives a heavy response. The page response weight does not change, slow first load remains, and this is noticeable not in logs but in client-side metrics.
A typical cause: gzip compression is configured but does not apply to all responses — some bypass the rule from an internal cache, through a proxy, or as static content for which the condition did not match.
The second class of problems is coverage. The default content type list is narrow: styles, scripts, vector graphics, and JSON are not included.
Some responses are compressed on the fly on every request, others are served compressed again — and the client decompresses what would have been better received as is.
This can only be checked by headers: whether the server looks at formats supported by the client and what actually lies in the response body.
The third is the delivery bottleneck. Compression is performed when a request arrives, and resources are spent on every hit instead of serving a pre-prepared file.
At peaks this creates a queue: response time grows not because of channel load, but because of the same recurring work. The cost is measurable — several times more bytes per client, reduced conversion on slow first load, and traffic that grows with the audience.
Below we break down how this is resolved: where compression is lost, what rules catch it, and how to serve prepared archives instead of doing work on every request.
What proper compression delivers: response time and volume
Transfer volume
Text resources are compressed: markup, styles, scripts, API responses in JSON. On a typical frontend this is 60–80% of page weight, and after configuration they go out in a stream 3–5 times smaller.
Images, video, and fonts are already compressed by format — they are excluded from the policy so as not to waste CPU.
Response time
Fewer bytes on the wire — faster first painted screen, especially on mobile networks and in distant regions. Brotli on static content delivers a noticeably smaller volume than gzip at the same level: the size difference reaches 15–25% on styles and scripts.
For dynamic content, on-the-fly compression with a moderate level is enabled — the time gain outweighs the cost.
CPU load
Compression costs cycles, so it is separated by content type and level. Static content is prepared in advance and cached, dynamic content is compressed at a low level, small responses are left untouched — for them headers weigh more than the body. This way the CPU is busy only where it actually reduces transfer.
Resilience under load
Saving channel capacity is headroom at peaks. When traffic grows several times over, the bottleneck shifts from bandwidth to the services themselves, and the site handles the influx without expanding the channel.
Static delivery is also accelerated by caching at the network edge: the compressed response is served from it without reaching the application.
What we check after configuration
We measure the share of compressed responses, the actual ratio by file type, and CPU load during peak hours. We make changes based on numbers, not by eye: you can see which formats to keep in the policy and which to exclude.
Where to enable compression: options and limitations
The enablement point is chosen based on who owns the response and where it is appropriate to spend CPU. Compression is a link in the delivery chain: the application forms the body, the reverse proxy routes requests, the web server serves static content, the edge handles caching.
You cannot compress in two places in a row: reprocessing an already compressed body breaks the response, so one point is chosen.
Selection criteria: control over the type list and minimum response length, presence of Vary: Accept-Encoding for correct cache operation, the cost of CPU time per request, and the ability to store prepared archives.
| Option | When it fits | Limitations |
|---|---|---|
| Compression on the web server | Mixed traffic: static content and application responses go through one server, flexible type-based rules are needed | The type list and length threshold are set manually; an error in the rules results either in an uncompressed response or in unnecessary load |
| Compression on the reverse proxy | Multiple services behind one entry point, unified rules and a cache of compressed responses are needed | Compresses based on headers, does not see the content; when caching, variants must be separated by Accept-Encoding |
| Pre-preparation of files | Static content that rarely changes: the build immediately places .gz and .br alongside it |
Only files on disk; does not cover dynamic content, archives must be rebuilt on every update |
| Compression at the delivery edge | Geographically distributed audience and a large volume of static content | Settings depend on the platform; some responses bypass it if compression has already been performed at the origin |
| Compression in the application | Responses are generated on the fly and do not pass through a shared server | CPU time is spent on every request, caching and debugging are more difficult |
Workflow: from delivery audit to production verification
Compression is not a single directive in a config, but a chain of decisions: what to compress, at which node to place compression, and how not to break already compressed formats such as images and video.
Below is the order in which we carry out implementation: the effect is predictable, and rollback takes minutes.
-
Delivery audit. We capture real response sizes by content type: markup, styles, scripts, fonts, API responses. We look at what already arrives compressed from the edge. The output is a list of types where compression provides a gain and those where it only wastes resources.
-
Request processing scheme. We record at which node compression is applied and what the edge compresses. Double compression of one response is load without a size gain.
-
Compression configuration. We enable gzip as baseline coverage and Brotli for clients that support it: on text types it gives a smaller size.
We set the compression level, minimum response size, type list, and exclusions for already compressed formats; the compressed and uncompressed variants must be correctly separated in the cache.
-
Pre-release verification. We compare size and delivery time before and after on the staging environment, separately on cold and warm cache, and run through key pages and API responses.
-
Rollback plan. The configuration is versioned, changes ship as a single edit that can be removed without rebuilding the application. We define in advance the metrics by which we consider the rollout unsuccessful.
-
Rollout and handover. We first roll out to part of the traffic, monitor the share of compressed responses and delivery time, then enable it fully. We hand over configs, the scheme, and a short post-release verification instruction to the team.
Output configurations, schemes, and procedures
We hand over not only a working server but also a set of artifacts by which compression can be further maintained in-house. Each document is tied to a specific place in the configuration, so the logic of the rules does not have to be reconstructed from edit history.
- Ready-to-use server configurations: compression levels for static and dynamic content, minimum response size threshold, handling of pre-compressed files — with an explanation of why these particular values were chosen.
- Request processing scheme: the path from client to delivery through the proxy, cache, and application, the place of compression in this chain, and the points where a response may bypass it.
- Exclusion policies: a list of file types that are pointless to compress — images, video, archives, already packed formats.
- Change procedure: how edits to lists and levels are verified before rollout, what is recorded in the history, and under what condition a rollback occurs.
- Delivery documentation: which headers the server must return, how to verify that the response arrived compressed, and where to look when there is a complaint about slow loading.
- A set of checks and metrics: share of responses with compression, volume of transferred data before and after, delivery time, and frequency of compression errors.
- Support instructions: where to change exclusions and levels, how to add a new file type, and what this will affect.
Case study: accelerated catalog delivery without replacing servers
Catalog of a large supplier: tens of thousands of product cards, heavy templates, a lot of text and markup in every response.
Measurements showed that the bottleneck was neither the server nor the database — CPU and disk were half-loaded, while the page was slow specifically in transfer.
Not a single text response went out compressed: the setting had been disabled during the initial installation and no one had revisited it since.
Measurements before implementation
The average catalog page weighed about 1.2 MB, of which markup, styles, and scripts accounted for more than 80%. Time to first render on an average connection was around 3.4 s, and the share of compressed responses was zero.
Bounce rates on mobile were noticeably higher than on desktop: the difference was explained by channel bandwidth, not by markup quality.
What we changed and what the result was
We enabled gzip for text types and added brotli with pre-compression of static content on disk — files were prepared once, not on every request.
The Nginx configuration was supplemented with an explicit type list, filtering out already compressed formats, and caching variants on the proxy.
The share of compressed responses grew to 96%, the catalog page weight dropped to 210 KB, and response time fell to 1.1 s — on the same hardware and without rewriting server logic.
The result was confirmed using access logs and measurements from different regions to exclude cache influence. The configuration is stored in a separate file, so returning to the previous behavior is one command.
How does pre-preparation differ from on-the-fly processing?
The difference is in where and when CPU time is spent. With pre-preparation, compression is performed once — during build or content publication — and the result lies next to the original as a separate file.
In this case, the web server only selects the appropriate variant based on the Accept-Encoding header and serves ready bytes without loading the CPU on every request.
On-the-fly processing compresses the response body every time the server forms it.
The cost of CPU time grows linearly with traffic: the higher the compression level, the more significant the contribution to core load, and on a stream of small responses the overhead outweighs the gain in transfer volume.
Dynamic content is exactly this category: the body depends on the request, session, and data, and a ready file cannot be assembled for it.
Two techniques work here: an edge cache that holds an already compressed representation and serves it again without recompression, and a cutoff by size and type — there is no point compressing responses below the threshold; they only waste CPU.
In practice, the modes do not compete but divide areas of responsibility: static content and templates — in advance, dynamic content — on the fly with a moderate level and edge caching enabled. This arrangement saves traffic with predictable CPU load.
Common technical questions about delivery configuration
Doubts before enabling compression are usually the same: how to make sure it works, whether content will start being compressed twice, and what to exclude from processing. Short answers to each are below.
How to verify that compression has actually been enabled?
A request with the Accept-Encoding header and a response with Content-Encoding: the same content arrives in the body, but with a noticeably smaller Content-Length.
The easiest way is to compare the response size with and without the header, and additionally check the compression ratio in the logs — this shows that the rule worked not only on markup but also on styles, scripts, and fonts.
Where does double compression come from?
If there is a proxy or external cache in front of the server that compresses responses itself, the content goes out encoded twice. The working option is to leave one level: either at the edge or on the server itself.
We record this before work so that later we do not have to look for the cause of an increased response size for part of users.
Which file types are excluded?
By default, only markup is compressed; the rest — styles, scripts, json, svg, fonts — are listed manually. Binary formats and pre-compressed copies are served as is: there is no size gain, while CPU load grows on every request.
What about caching and headers?
Without the Vary: Accept-Encoding header, an intermediate cache may serve the compressed variant to a client that does not support it. The modification date is preserved, and the static content lifetime is set long — then repeat visits do not touch the server at all.
Is this compatible with proxies and load balancers?
Yes. The only thing worth agreeing on in advance is at which level compression is enabled: if the connection terminates at the load balancer, it makes more sense to do it there. This is usually how it becomes clear why responses arrive uncompressed for part of the audience.
Let's discuss the task: where we start and what we need from you
The first conversation is needed to see your request processing scheme as a whole: where the secure connection terminates, what sits in front of the application, which responses already go out compressed and which do not.
From this picture it becomes clear where enabling gzip and Brotli will have an immediate effect and where it will run into CPU or an intermediate cache.
Then we need input data — without it, estimating the work turns into guesswork. We usually ask for:
- web server and reverse proxy configuration: where compression is enabled, which modules are built;
- a list of content types and the current share of compressed responses: markup, JSON, styles, fonts, static content;
- caching scheme: intermediate proxy, content delivery network, file versioning;
- CPU headroom on the nodes that compress traffic, and the peak load profile;
- environments and release procedure: how a change reaches production and whether there is a test environment;
- rollback requirements and windows when the configuration can be switched without risk;
- metrics by which you judge the result: response time, traffic volume, CPU load, cache hit rate.
As a result, you get a work plan: what we enable, in what order, how we verify the effect, and how we roll back in one step. Describe the task — we will come back with an estimate and plan, and after approval we will schedule the project start.







