Every byte that travels between a server and a browser costs time. On a slow mobile connection — the everyday reality for millions of users in Saudi Arabia and globally — that cost is amplified. Gzip compression is one of the highest-leverage optimisations available to any developer, system administrator, or DevOps engineer: a configuration change that typically reduces the size of HTML, CSS, JavaScript, and API responses by 60–80%, with no loss of content and minimal CPU overhead.
Despite being one of the most impactful and widely supported web optimisations available — it has been part of the HTTP standard since 1999 — gzip remains misconfigured or completely disabled on a surprising number of production systems. This guide covers everything: what gzip is, how it works, and exactly how to enable and verify it across Linux, every major web server, every major cloud provider, and common development stacks.
1. What Is Gzip?
Gzip (GNU zip) is a lossless data compression format and tool, standardised as RFC 1952. “Lossless” means the original data is fully recovered on decompression — nothing is discarded. Gzip is based on the DEFLATE algorithm, which combines LZ77 (a sliding window algorithm that replaces repeated byte sequences with references to earlier occurrences) and Huffman coding (a variable-length encoding that assigns shorter codes to more frequent patterns).
In the context of the web, gzip compression is negotiated via HTTP headers. The browser sends an Accept-Encoding: gzip, deflate, br header with every request, signalling which compression formats it supports. The server compresses the response body using gzip (or another supported format) and adds a Content-Encoding: gzip header so the browser knows to decompress it before rendering.
Gzip does not help — and should not be applied — to already-compressed formats: JPEG, PNG, WebP, AVIF images, MP4/WebM videos, ZIP files, PDF documents. Compressing already-compressed data produces negligible savings and wastes CPU time.
2. How Gzip Compression Works
Gzip uses the DEFLATE algorithm in two stages:
Stage 1: LZ77 — Remove Repeated Sequences
LZ77 scans the input data looking for repeated byte sequences. When it finds a sequence it has seen before (within the last 32KB — the “sliding window”), instead of writing the bytes again, it writes a (distance, length) pointer: “go back N bytes and copy L bytes from there.” Text, code, and markup files have enormous amounts of repetition — HTML elements, CSS property names, JavaScript keywords, JSON keys — making them highly compressible.
Stage 2: Huffman Coding — Compress the Symbol Frequency
After LZ77, the output consists of literal bytes and (distance, length) pairs. Huffman coding then assigns shorter bit sequences to more frequent symbols and longer sequences to rarer ones. The letter “e” in English text might be encoded in 3 bits; a rare symbol might take 12 bits. The mapping itself (the Huffman tree) is transmitted as part of the compressed file so the decompressor can reverse the process.
Compression Levels
Gzip supports nine compression levels (1–9). Level 1 is fastest with least compression; level 9 provides maximum compression at the cost of CPU time. Level 6 is the widely accepted default — it captures roughly 98% of the compression benefit of level 9 at about half the CPU cost. For real-time web serving, level 6 is the right balance.
# Compression level comparison (HTML file, 120KB)
Level 1: 31 KB — 0.3ms compression time
Level 6: 28 KB — 1.1ms compression time ← sweet spot
Level 9: 27 KB — 4.2ms compression time
# Level 9 saves only 1KB more than level 6 but takes 4x longer
3. Gzip on Linux — Command Line
The gzip command is part of every Linux distribution and macOS. It is the foundational tool you use for compressing log files, backups, data pipelines, and build artefacts.
Basic Usage
# Compress a file (replaces original with .gz version)
gzip filename.txt
# → Creates filename.txt.gz, removes filename.txt
# Compress and KEEP the original file
gzip -k filename.txt
# → Creates filename.txt.gz, keeps filename.txt
# Decompress
gzip -d filename.txt.gz
# OR
gunzip filename.txt.gz
# Decompress and keep the .gz file
gzip -dk filename.txt.gz
# Test integrity of a compressed file
gzip -t filename.txt.gz
# → No output if valid; non-zero exit code if corrupted
Compression Level and Verbose Output
# Use maximum compression (level 9)
gzip -9 largefile.log
# Use fastest compression (level 1) for quick processing
gzip -1 quickbackup.tar
# Verbose — show compression ratio
gzip -v filename.html
# → filename.html: 77.3% -- replaced with filename.html.gz
# Compress multiple files
gzip -k *.html *.css *.js
Working With tar + gzip
The most common Linux pattern: tar archives combined with gzip compression, creating the familiar .tar.gz or .tgz files.
# Create a compressed archive
tar -czf archive.tar.gz /path/to/directory/
# -c: create -z: gzip -f: filename
# Extract a compressed archive
tar -xzf archive.tar.gz
# -x: extract -z: gzip -f: filename
# Extract to a specific directory
tar -xzf archive.tar.gz -C /target/directory/
# List contents without extracting
tar -tzf archive.tar.gz
# Compress with best ratio + verbose progress
tar -czvf archive.tar.gz --use-compress-program="gzip -9" /path/to/dir/
Compressing Log Files (Production Pattern)
# Compress all logs older than 7 days
find /var/log/nginx/ -name "*.log" -mtime +7 -exec gzip -9 {} \;
# Real-time gzip — pipe command output directly to gzip
# Useful for large dumps that shouldn't sit on disk uncompressed
mysqldump mydb | gzip -6 > backup_$(date +%Y%m%d).sql.gz
# Stream a gzip file without decompressing to disk
zcat compressed.log.gz | grep "ERROR" | head -50
# Concatenate and search multiple gzip logs
zcat /var/log/nginx/*.gz | grep "POST /api" | wc -l
pigz — Parallel Gzip for Large Files
Standard gzip is single-threaded. For large files on multi-core servers, pigz (parallel implementation of gzip) uses all available CPU cores and can be 3–8x faster on modern servers.
# Install pigz
sudo apt install pigz # Ubuntu/Debian
sudo dnf install pigz # RHEL/CentOS/Rocky
sudo pacman -S pigz # Arch Linux
# Use pigz exactly like gzip
pigz -6 largefile.sql # Single file
tar -czf archive.tar.gz --use-compress-program=pigz /path/
# Use 8 cores explicitly
pigz -p 8 -9 database_dump.sql
4. Gzip in Nginx
Nginx has built-in gzip support via the ngx_http_gzip_module, compiled in by default. This is the most common and highest-impact place to enable gzip for web applications served behind Nginx.
Complete Nginx Gzip Configuration
# In /etc/nginx/nginx.conf (http block) or site-specific server block
http {
# Enable gzip compression
gzip on;
# Minimum file size to compress (don't compress tiny files)
# Files smaller than 1KB compress poorly and waste CPU
gzip_min_length 1024;
# Compression level: 1 (fast) - 9 (best). 6 is optimal
gzip_comp_level 6;
# Compress these MIME types (text/html is always compressed)
gzip_types
text/plain
text/css
text/javascript
text/xml
text/x-component
application/javascript
application/json
application/xml
application/xml+rss
application/vnd.api+json
application/atom+xml
application/x-font-ttf
application/x-font-opentype
application/x-font-woff
application/x-web-app-manifest+json
font/opentype
font/woff
font/woff2
image/svg+xml
image/x-icon;
# Add Vary: Accept-Encoding header so CDNs cache both
# compressed and uncompressed versions separately
gzip_vary on;
# Compress responses for all proxied requests
# regardless of the response code
gzip_proxied any;
# Enable gzip for HTTP/1.0 clients (rare but safe to include)
gzip_http_version 1.0;
# Disable gzip for IE6 (ancient, ignore if not needed)
# gzip_disable "msie6";
# Buffer size for compression (default 32 4k or 16 8k)
gzip_buffers 16 8k;
}
Nginx Static Gzip (Pre-compressed Files)
For maximum performance, pre-compress your static assets at build time and serve the .gz version directly. Nginx’s gzip_static module eliminates the per-request CPU cost of real-time compression entirely.
# Enable static gzip serving in Nginx
location /static/ {
gzip_static on; # Serve .gz version if it exists
# Nginx will serve /static/app.js.gz when /static/app.js is requested
# (if Accept-Encoding: gzip is present in the request)
}
# Pre-compress your files at deployment time:
# find /var/www/static/ -name "*.css" -o -name "*.js" | xargs gzip -9 -k
# This creates .gz versions alongside originals
Verify Nginx Gzip
# Test Nginx config before reloading
nginx -t
# Reload Nginx
sudo systemctl reload nginx
# Verify gzip is working
curl -H "Accept-Encoding: gzip" -I https://yoursite.com/
# Look for: Content-Encoding: gzip
5. Gzip in Apache
Apache uses the mod_deflate module to serve gzip-compressed responses. Despite the module’s name (deflate), it actually serves gzip-encoded content — the naming is a historical quirk.
Enable mod_deflate
# Enable the module (Ubuntu/Debian)
sudo a2enmod deflate
sudo systemctl restart apache2
# On RHEL/CentOS — already enabled by default in most installations
# Check: httpd -M | grep deflate
Complete Apache mod_deflate Configuration
Add to your Apache virtual host config or .htaccess:
<IfModule mod_deflate.c>
# Compress HTML, CSS, JavaScript, Text, XML and fonts
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/json
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/vnd.api+json
AddOutputFilterByType DEFLATE application/x-font
AddOutputFilterByType DEFLATE application/x-font-opentype
AddOutputFilterByType DEFLATE application/x-font-otf
AddOutputFilterByType DEFLATE application/x-font-truetype
AddOutputFilterByType DEFLATE application/x-font-ttf
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE font/opentype
AddOutputFilterByType DEFLATE font/otf
AddOutputFilterByType DEFLATE font/ttf
AddOutputFilterByType DEFLATE font/woff
AddOutputFilterByType DEFLATE font/woff2
AddOutputFilterByType DEFLATE image/svg+xml
AddOutputFilterByType DEFLATE image/x-icon
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/javascript
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/xml
# Set compression level (1-9, default is 6)
DeflateCompressionLevel 6
# Add Vary header for CDN cache differentiation
<IfModule mod_headers.c>
Header append Vary Accept-Encoding
</IfModule>
# Disable for buggy browsers (legacy — safe to remove)
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
</IfModule>
WordPress-specific .htaccess Gzip
# Add ABOVE the # BEGIN WordPress section in .htaccess
<IfModule mod_deflate.c>
<FilesMatch "\.(css|js|html|htm|xml|json|svg|ttf|otf|woff|woff2)$">
SetOutputFilter DEFLATE
</FilesMatch>
</IfModule>
# Enable browser caching for compressed assets
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
</IfModule>
6. Gzip in Development Stacks
Node.js / Express
# Install compression middleware
npm install compression
# Basic setup — compresses all responses
const express = require('express');
const compression = require('compression');
const app = express();
// Add compression middleware BEFORE routes
app.use(compression({
level: 6, // Compression level (1-9)
threshold: 1024, // Only compress responses > 1KB
filter: (req, res) => {
// Don't compress if client explicitly doesn't want it
if (req.headers['x-no-compression']) return false;
// Default filter — compress text-based responses
return compression.filter(req, res);
}
}));
app.get('/api/data', (req, res) => {
res.json({ data: 'Large JSON payload here...' });
// compression middleware automatically gzips this
});
app.listen(3000);
Python — Flask
pip install flask flask-compress
from flask import Flask
from flask_compress import Compress
app = Flask(__name__)
Compress(app)
# Configuration
app.config['COMPRESS_LEVEL'] = 6 # Gzip level
app.config['COMPRESS_MIN_SIZE'] = 500 # Min bytes to compress
@app.route('/api/data')
def get_data():
return {'results': [...]}, 200 # Auto-compressed
Python — FastAPI
pip install fastapi uvicorn
from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware
app = FastAPI()
# Add GZip middleware
app.add_middleware(
GZipMiddleware,
minimum_size=1000, # Only compress responses > 1000 bytes
compresslevel=6
)
@app.get("/api/data")
async def get_data():
return {"results": [...]} # Auto-compressed
Python — Manual Gzip in Scripts
import gzip
import json
# Compress a file
with open('data.json', 'rb') as f_in:
with gzip.open('data.json.gz', 'wb', compresslevel=6) as f_out:
f_out.write(f_in.read())
# Compress data in memory (for API responses, S3 uploads)
data = json.dumps({'key': 'value', 'records': [...]}).encode('utf-8')
compressed = gzip.compress(data, compresslevel=6)
# Decompress
decompressed = gzip.decompress(compressed)
# Read a gzip file line by line (efficient for large files)
with gzip.open('large_log.gz', 'rt', encoding='utf-8') as f:
for line in f:
process(line)
PHP
<?php
// Enable output buffering with gzip (PHP built-in)
ob_start('ob_gzhandler');
// Or check client support and compress manually
if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) &&
strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false) {
$data = json_encode(['results' => $results]);
$compressed = gzencode($data, 6);
header('Content-Encoding: gzip');
header('Content-Length: ' . strlen($compressed));
header('Vary: Accept-Encoding');
echo $compressed;
exit;
}
Go
package main
import (
"compress/gzip"
"net/http"
"strings"
)
func gzipMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Vary", "Accept-Encoding")
gz, _ := gzip.NewWriterLevel(w, gzip.BestSpeed) // level 1
defer gz.Close()
gzResponseWriter := &gzipResponseWriter{ResponseWriter: w, Writer: gz}
next(gzResponseWriter, r)
}
}
7. Gzip in AWS
S3 — Uploading Pre-compressed Files
S3 itself doesn’t compress files — you compress them before uploading and set the correct metadata so browsers decompress them correctly.
# Compress a file and upload with correct Content-Encoding
gzip -9 -k app.js # Creates app.js.gz, keeps app.js
aws s3 cp app.js.gz s3://my-bucket/static/app.js \
--content-encoding gzip \
--content-type "application/javascript" \
--cache-control "public, max-age=31536000"
# Script to compress and upload all static assets
for file in dist/*.js dist/*.css; do
gzip -9 -k "$file"
aws s3 cp "${file}.gz" "s3://my-bucket/${file}" \
--content-encoding gzip \
--content-type "$(file -b --mime-type $file)"
rm "${file}.gz"
done
CloudFront — Enable Automatic Compression
CloudFront can compress files automatically in its edge cache — the best option for most use cases.
# Via AWS CLI — update distribution to enable compression
aws cloudfront update-distribution \
--id E1234567890 \
--distribution-config '{
"DefaultCacheBehavior": {
"Compress": true
}
}'
# Via Terraform
resource "aws_cloudfront_distribution" "site" {
default_cache_behavior {
compress = true # CloudFront compresses eligible responses
# Eligible: text/*, application/javascript, application/json,
# application/xml, image/svg+xml (1-10MB, 1KB minimum)
}
}
API Gateway — Enable Gzip
# REST API — set minimum compression size (bytes)
# 0 = compress everything; -1 = disable compression
aws apigateway update-stage \
--rest-api-id abc123 \
--stage-name prod \
--patch-operations op=replace,path=/minimumCompressionSize,value=1024
# HTTP API (via console or Terraform)
resource "aws_apigatewayv2_api" "api" {
name = "my-api"
protocol_type = "HTTP"
# HTTP API compresses automatically when client sends Accept-Encoding: gzip
}
Lambda — Returning Gzip from Function
import gzip
import json
import base64
def handler(event, context):
data = json.dumps({'results': [...]})
compressed = gzip.compress(data.encode('utf-8'), compresslevel=6)
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json',
'Content-Encoding': 'gzip',
'Vary': 'Accept-Encoding'
},
'body': base64.b64encode(compressed).decode('utf-8'),
'isBase64Encoded': True # Required for binary responses via API GW
}
Elastic Load Balancer (ALB)
ALB does not perform gzip compression itself — compression should be handled by your application servers (Nginx, Node.js, etc.) behind the ALB, or by CloudFront in front of the ALB.
Elastic Beanstalk — Nginx Platform
# .ebextensions/nginx-gzip.config
files:
/etc/nginx/conf.d/gzip.conf:
mode: "000644"
owner: root
group: root
content: |
gzip on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_vary on;
gzip_proxied any;
gzip_types
text/plain text/css application/json
application/javascript text/xml application/xml
application/xml+rss text/javascript image/svg+xml;
8. Gzip in Docker and Containers
Nginx in Docker
# nginx.conf with gzip enabled
# Mount as volume or COPY into image
# Dockerfile
FROM nginx:alpine
# Copy custom nginx config with gzip
COPY nginx.conf /etc/nginx/nginx.conf
# Pre-compress static assets at build time
COPY --from=builder /app/dist /usr/share/nginx/html
RUN find /usr/share/nginx/html -name "*.js" -o -name "*.css" \
| xargs gzip -9 -k
Multi-stage Build with Pre-compression
# Dockerfile — React app with pre-compressed assets
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Compress build output
RUN find /app/build -type f \( -name "*.js" -o -name "*.css" -o \
-name "*.html" -o -name "*.json" \) \
-exec gzip -9 -k {} \;
FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
# Copy both original and .gz versions
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
Reduce Docker Image Size with Gzip
# Compress large configuration or data files within image
RUN gzip -9 /app/data/large-dataset.json \
&& mv /app/data/large-dataset.json.gz /app/data/dataset.json.gz
# Stream tar.gz archives into containers efficiently
docker run myimage sh -c "tar -czf - /data" > backup.tar.gz
# Export Docker image as compressed tar
docker save myimage | gzip -9 > myimage.tar.gz
docker load < myimage.tar.gz
9. Gzip in CI/CD Pipelines
GitHub Actions
# .github/workflows/deploy.yml
name: Build and Deploy
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build application
run: npm run build
# Pre-compress static assets for S3/CDN deployment
- name: Gzip static assets
run: |
find dist/ -type f \( -name "*.js" -o -name "*.css" \
-o -name "*.html" -o -name "*.svg" -o -name "*.json" \) \
| while read file; do
gzip -9 -k "$file"
echo "Compressed: $file ($(du -h "$file.gz" | cut -f1))"
done
# Upload to S3 with correct headers
- name: Deploy to S3
run: |
# Upload .gz files with Content-Encoding header
aws s3 sync dist/ s3://my-bucket/ \
--exclude "*" --include "*.gz" \
--content-encoding gzip \
--cache-control "public, max-age=31536000, immutable"
# Upload non-compressed files normally
aws s3 sync dist/ s3://my-bucket/ \
--exclude "*.gz"
GitLab CI
# .gitlab-ci.yml
compress-assets:
stage: build
script:
- npm run build
- find public/ -type f -name "*.js" -exec gzip -9 -k {} \;
- find public/ -type f -name "*.css" -exec gzip -9 -k {} \;
artifacts:
paths:
- public/
expire_in: 1 hour
10. Brotli vs Gzip — Which Should You Use?
Brotli (encoding: br) is Google's compression algorithm, introduced in 2015 and now supported by all major browsers. It consistently outperforms gzip by 15–25% on web content.
Recommended approach: Serve Brotli for pre-compressed static assets (JS, CSS, fonts) and gzip as the fallback. For real-time API responses, gzip at level 6 is the better choice due to its lower CPU cost per request.
# Nginx — serve both Brotli and Gzip
# Requires ngx_brotli module (not in default Nginx builds)
# Install: sudo apt install nginx-extras OR compile with --add-module=ngx_brotli
brotli on;
brotli_comp_level 6;
brotli_static on; # Serve .br files if they exist
brotli_types text/plain text/css application/json application/javascript;
gzip on;
gzip_static on; # Fallback for clients that don't support Brotli
gzip_comp_level 6;
# Nginx serves: .br version for Brotli clients, .gz for gzip clients
# Pre-compress assets: gzip -9 -k file.js && brotli -9 -k file.js
11. Testing and Verifying Gzip
# Method 1: curl — check response headers
curl -H "Accept-Encoding: gzip" -I https://yoursite.com/app.js
# Look for: Content-Encoding: gzip
# Method 2: curl — check actual compression
curl -H "Accept-Encoding: gzip" --compressed -o /dev/null \
-s -w "Size: %{size_download} bytes\n" https://yoursite.com/
# Method 3: Compare compressed vs uncompressed size
# Uncompressed
curl -o /dev/null -s -w "%{size_download}" https://yoursite.com/app.js
# Compressed
curl -H "Accept-Encoding: gzip" --compressed -o /dev/null \
-s -w "%{size_download}" https://yoursite.com/app.js
# Method 4: Check Vary header (important for CDNs)
curl -I https://yoursite.com/ | grep -i "vary"
# Should include: Vary: Accept-Encoding
# Method 5: gzip integrity test on local files
gzip -t yourfile.html.gz && echo "Valid" || echo "Corrupt"
# Online tools:
# https://www.giftofspeed.com/gzip-test/
# https://checkgzipcompression.com/
# Google PageSpeed Insights — checks for gzip
12. Frequently Asked Questions
No. JPEG, PNG, WebP, AVIF, and GIF images are already compressed formats. Applying gzip to them produces negligible size reduction (often 0–2%) while consuming server CPU. The correct approach for image optimisation is to use modern formats (WebP, AVIF), resize images to display dimensions, and use a CDN. Only SVG images (which are XML text) benefit meaningfully from gzip compression.
For real-time gzip, there is a small CPU cost (typically 1–5ms at level 6 for most web responses). This is almost always outweighed by the reduction in transfer time, especially on slower connections. Pre-compressed static gzip (serving .gz files directly) has zero TTFB impact since compression happened at build time.
Content-Encoding describes how the message body is encoded (compressed). Transfer-Encoding describes how the data is transferred over the network (chunked = sent in pieces). They solve different problems and can be used simultaneously — a response can be both Content-Encoding: gzip (the body is compressed) and Transfer-Encoding: chunked (the compressed body is sent in chunks as it's generated).
Incorrectly configured gzip can cause issues. Common problems: compressing already-compressed responses (double compression), missing the Vary: Accept-Encoding header (causing CDNs to serve the compressed version to clients that can't handle it), and wrong Content-Type headers preventing gzip from triggering. Always verify gzip is working after configuration changes using curl or an online tool.
Gzip directly improves Largest Contentful Paint (LCP) and First Contentful Paint (FCP) by reducing the bytes that need to be transferred, which reduces download time. LCP is a direct Core Web Vitals metric that Google uses as a ranking signal. Enabling gzip on a site that doesn't have it can improve LCP scores significantly, particularly on mobile connections. Google PageSpeed Insights explicitly flags missing gzip as an "Enable text compression" opportunity.
Not necessary for local development — network transfer between localhost and your browser is essentially instantaneous regardless of response size. Enabling gzip locally does make sense when: profiling real payload sizes, testing CDN behaviour, or ensuring your app handles Content-Encoding: gzip correctly end-to-end. Keep development config simple and test compression in staging environments that mirror production.
Web Server Management · Saudi Arabia
Need Help Optimising Your Server Performance?
Visit To Me configures and manages Nginx, Apache, and cloud infrastructure for businesses in Saudi Arabia. We handle gzip, caching, CDN integration, and Core Web Vitals optimisation — all with a written SLA and 24-hour response guarantee.
Leave a Reply