Skip to main content

HTTP Compression in .htaccess

Every kilobyte you send over the network costs time. On a 3G mobile connection, an uncompressed 200KB JavaScript file takes noticeably longer to download than a compressed 50KB version of the same file. HTTP compression reduces the size of text-based responses before they leave your server, often achieving 60-80% size reduction with negligible CPU overhead. It is one of the highest-impact performance optimizations you can make, and it requires no changes to your application code.

This guide covers why compression matters, how to fix broken encoding headers from misbehaving proxies, how to configure both gzip (mod_deflate) and Brotli (mod_brotli) compression, which file types benefit from compression, which should be left alone, and how to verify that compression is working correctly.

Why Compression Matters

When a browser requests a resource from your server, it sends an Accept-Encoding header indicating which compression algorithms it supports:

Accept-Encoding: gzip, deflate, br

If the server supports one of these algorithms and the response is compressible, it compresses the response body and sends it with a Content-Encoding header:

Content-Encoding: gzip

The browser decompresses the content transparently. The user sees no difference except that the page loads faster.

Real-World Impact

Resource TypeUncompressedGzip CompressedBrotli CompressedSavings
HTML page100 KB25 KB22 KB75-78%
CSS file150 KB30 KB25 KB80-83%
JavaScript bundle500 KB120 KB100 KB76-80%
JSON API response80 KB15 KB12 KB81-85%
SVG image60 KB12 KB10 KB80-83%

These savings translate directly to:

  • Faster page loads: Less data to transfer means pages render sooner, especially on slow connections.
  • Lower bandwidth costs: If you pay for bandwidth (CDN, cloud hosting), compression reduces your bill proportionally.
  • Better Core Web Vitals: Google's Largest Contentful Paint (LCP) and other performance metrics improve with smaller transfer sizes.
  • Improved mobile experience: Mobile connections are often bandwidth-constrained. Compression makes a dramatic difference on 3G/4G networks.
  • Higher search rankings: Page speed is a Google ranking factor. Compression is explicitly recommended in Google's PageSpeed Insights.

Fixing Broken Accept-Encoding Headers

Before configuring compression, there is a practical problem to address. Some network proxies, corporate firewalls, antivirus software, and security appliances mangle or strip the Accept-Encoding header from HTTP requests. They do this to scan the response body for malware or to comply with content inspection policies, but the result is that your server never sees a valid Accept-Encoding header and therefore never compresses the response.

These broken headers look like:

Accept-EncodXng: gzip, deflate
X-cept-Encoding: gzip, deflate
~~~~~~~~~~~~~~~: gzip, deflate
---------------: gzip, deflate
XXXXXXXXXXXXXXX: gzip, deflate

The software modifies the header name just enough to make it unrecognizable to the server while keeping the value intact (hoping to restore it later). The fix detects these mangled headers and restores a proper Accept-Encoding header:

<IfModule mod_deflate.c>
<IfModule mod_setenvif.c>
<IfModule mod_headers.c>
SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
</IfModule>
</IfModule>
</IfModule>

How this works:

  • SetEnvIfNoCase checks if any incoming header matches the known mangled patterns (Accept-EncodXng, X-cept-Encoding, or a string of 15 repeated characters like X, ~, or -). It also checks that the value looks like a valid encoding list (gzip, deflate, or a string of the characters used for mangling).
  • If a match is found, the HAVE_Accept-Encoding environment variable is set.
  • RequestHeader append adds a proper Accept-Encoding: gzip,deflate header to the request, but only if the environment variable is set (env=HAVE_Accept-Encoding).

This fix ensures that users behind problematic proxies still receive compressed responses. Place it before your compression configuration.

note

This fix addresses a relatively uncommon scenario. Most modern networks pass the Accept-Encoding header through unchanged. However, corporate environments and some mobile carriers still use intercepting proxies, and including this fix costs nothing while improving compression coverage for affected users.

mod_deflate Configuration

Apache's mod_deflate module provides gzip compression (despite the somewhat confusing name, mod_deflate actually uses gzip, not the raw deflate algorithm). It is the most widely supported and most commonly used compression module.

AddOutputFilterByType

The AddOutputFilterByType directive tells Apache to compress responses based on their media type (MIME type). This is the recommended approach because it targets specific content types rather than file extensions.

Comprehensive mod_deflate configuration
<IfModule mod_deflate.c>
<IfModule mod_filter.c>
AddOutputFilterByType DEFLATE "application/atom+xml" \
"application/javascript" \
"application/json" \
"application/ld+json" \
"application/manifest+json" \
"application/rdf+xml" \
"application/rss+xml" \
"application/schema+json" \
"application/geo+json" \
"application/vnd.ms-fontobject" \
"application/wasm" \
"application/x-font-ttf" \
"application/x-javascript" \
"application/x-web-app-manifest+json" \
"application/xhtml+xml" \
"application/xml" \
"font/eot" \
"font/opentype" \
"font/otf" \
"font/ttf" \
"image/bmp" \
"image/svg+xml" \
"image/vnd.microsoft.icon" \
"text/cache-manifest" \
"text/calendar" \
"text/css" \
"text/html" \
"text/javascript" \
"text/plain" \
"text/markdown" \
"text/vcard" \
"text/vnd.rim.location.xloc" \
"text/vtt" \
"text/x-component" \
"text/x-cross-domain-policy" \
"text/xml"
</IfModule>
</IfModule>

The <IfModule mod_filter.c> wrapper is required because AddOutputFilterByType relies on mod_filter in Apache 2.4+.

By Extension vs By Media Type

You might wonder why we use media types instead of file extensions. Both approaches work, but media types are more reliable:

By media type (recommended):

AddOutputFilterByType DEFLATE "text/html" "text/css" "text/javascript"

This compresses the response based on the actual Content-Type header, regardless of the file extension. A PHP script that outputs JSON with Content-Type: application/json is correctly compressed even though its file extension is .php.

By extension (less reliable):

<FilesMatch "\.(html|css|js)$">
SetOutputFilter DEFLATE
</FilesMatch>

This approach only looks at the file extension. Dynamic content generated by PHP, Python, or other languages would not be compressed unless you also match their extensions, and even then, the compression applies regardless of what content type the script actually outputs.

tip

Always use AddOutputFilterByType for compression. It correctly handles dynamic content, works with any file extension, and ensures that only appropriate content types are compressed. The extension-based approach is a legacy pattern that should be avoided.

Setting Compression Level

You can control how aggressively mod_deflate compresses content:

<IfModule mod_deflate.c>
DeflateCompressionLevel 6
</IfModule>

The level ranges from 1 (fastest, least compression) to 9 (slowest, most compression). The default is typically 6, which provides a good balance. Going beyond 6 yields diminishing returns in file size reduction while significantly increasing CPU usage:

LevelCompression RatioCPU UsageRecommendation
1LowVery lowHigh-traffic sites with CPU constraints
4-6GoodModerateRecommended for most sites
9Slightly betterHighRarely worth the CPU cost

Brotli via mod_brotli

Brotli is a newer compression algorithm developed by Google that achieves 15-25% better compression than gzip for most web content. It is supported by all modern browsers (Chrome, Firefox, Safari, Edge) and is increasingly the preferred compression method.

Brotli support requires mod_brotli, which is available in Apache 2.4.26 and later.

Brotli compression configuration
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS "application/atom+xml" \
"application/javascript" \
"application/json" \
"application/ld+json" \
"application/manifest+json" \
"application/rdf+xml" \
"application/rss+xml" \
"application/schema+json" \
"application/geo+json" \
"application/vnd.ms-fontobject" \
"application/wasm" \
"application/xhtml+xml" \
"application/xml" \
"font/eot" \
"font/opentype" \
"font/otf" \
"font/ttf" \
"image/bmp" \
"image/svg+xml" \
"image/vnd.microsoft.icon" \
"text/cache-manifest" \
"text/calendar" \
"text/css" \
"text/html" \
"text/javascript" \
"text/plain" \
"text/markdown" \
"text/vcard" \
"text/vtt" \
"text/x-component" \
"text/xml"
</IfModule>

Using Both Brotli and Gzip

The best practice is to configure both algorithms. Apache automatically selects the best one based on what the browser supports. Browsers that support Brotli receive Brotli-compressed responses. Older browsers that only support gzip receive gzip-compressed responses.

Both Brotli and gzip with fallback
# Brotli compression (preferred, better ratio)
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS "text/html" \
"text/css" \
"text/javascript" \
"application/javascript" \
"application/json" \
"application/xml" \
"image/svg+xml" \
"font/ttf" \
"font/otf" \
"font/woff" \
"application/vnd.ms-fontobject"
</IfModule>

# Gzip compression (fallback for older browsers)
<IfModule mod_deflate.c>
<IfModule mod_filter.c>
AddOutputFilterByType DEFLATE "text/html" \
"text/css" \
"text/javascript" \
"application/javascript" \
"application/json" \
"application/xml" \
"image/svg+xml" \
"font/ttf" \
"font/otf" \
"font/woff" \
"application/vnd.ms-fontobject"
</IfModule>
</IfModule>

When both modules are active, Apache checks the Accept-Encoding header:

  • If the browser sends Accept-Encoding: br, gzip, Apache uses Brotli.
  • If the browser sends Accept-Encoding: gzip, Apache uses gzip.
  • If the browser sends no encoding support, Apache sends the response uncompressed.
note

mod_brotli is not available on all hosting providers. Many shared hosting environments only offer mod_deflate. If Brotli is not available, gzip alone is still a major improvement. You can check with apachectl -M | grep brotli or by examining phpinfo() output.

What to Compress

Not all content benefits equally from compression. The key distinction is between text-based formats (which compress extremely well) and binary formats (which are usually already compressed).

Text-Based Assets

These are your primary compression targets. Text-based files are highly repetitive and compress dramatically:

Content TypeFile TypesTypical Compression Ratio
text/html.html, .htm70-80%
text/css.css75-85%
text/javascript.js, .mjs70-80%
text/plain.txt, .log60-75%
text/markdown.md60-75%
text/vtt.vtt (subtitles)70-80%

These are the highest-impact types to compress because they are both large and highly compressible.

SVG and XML

SVG images and XML documents are text-based and compress exceptionally well:

image/svg+xml          → SVG images (often 80%+ compression)
application/xml → XML documents
application/atom+xml → Atom feeds
application/rss+xml → RSS feeds
application/xhtml+xml → XHTML pages
application/rdf+xml → RDF data

A complex SVG illustration might be 100KB uncompressed and 15KB after gzip. Since SVGs are increasingly used for icons, logos, and illustrations, compressing them provides significant savings.

JSON and API Responses

API responses are often the largest single transfers on modern web applications. JSON compresses extremely well due to its repetitive key structure:

application/json            → Standard JSON
application/ld+json → JSON-LD (structured data)
application/manifest+json → Web app manifests
application/geo+json → Geographic data
application/schema+json → JSON Schema

A JSON API response with 1000 records might be 500KB uncompressed but only 50KB after compression. For data-heavy applications, compressing JSON responses is one of the most impactful optimizations you can make.

Fonts

Some font formats are uncompressed and benefit from gzip:

Font FormatCompress?Reason
TTF/OTFYesUncompressed by default. Significant savings.
EOTYesOlder format, typically uncompressed.
WOFFMinimalAlready uses its own compression. Minor benefit.
WOFF2NoAlready Brotli-compressed internally.

What Not to Compress

Attempting to compress already-compressed content wastes CPU cycles, adds latency, and can occasionally make the output larger than the input. These formats should be excluded from compression.

Images (JPEG, PNG, WebP)

JPEG, PNG, WebP, AVIF, and GIF images use their own compression algorithms. Applying gzip on top provides negligible size reduction (often less than 1%) while consuming CPU:

image/jpeg     → Already JPEG-compressed
image/png → Already uses DEFLATE internally
image/webp → Already compressed
image/avif → Already compressed
image/gif → Already uses LZW compression
warning

Do not add image/jpeg, image/png, image/webp, image/avif, or image/gif to your AddOutputFilterByType list. Compressing these formats wastes server resources and provides no benefit. The only image format that should be compressed is SVG (image/svg+xml) because it is text-based XML.

Video and Audio

Media files are heavily compressed by their codecs. Additional compression is pointless:

video/mp4      → H.264/H.265 compressed
video/webm → VP8/VP9/AV1 compressed
audio/mpeg → MP3 compressed
audio/ogg → Vorbis/Opus compressed
audio/mp4 → AAC compressed

Already-Compressed Formats

Several other formats are internally compressed and should not be double-compressed:

FormatMedia TypeWhy Not Compress
ZIP archivesapplication/zipAlready compressed with DEFLATE
GZIP archivesapplication/gzipAlready gzip-compressed
PDF filesapplication/pdfUsually internally compressed (streams)
WOFF2 fontsfont/woff2Already Brotli-compressed
WOFF fontsfont/woffAlready compressed (minimal benefit from gzip)

Common Mistake: Compressing Everything

Wrong approach:

# This compresses ALL responses, including images and video
SetOutputFilter DEFLATE

This applies compression globally to every response regardless of content type. Binary files like images and videos waste CPU cycles with zero benefit, and the output can occasionally be larger than the input.

Correct approach:

# Only compress content types that benefit from compression
<IfModule mod_deflate.c>
<IfModule mod_filter.c>
AddOutputFilterByType DEFLATE "text/html" "text/css" "text/javascript" "application/json" "image/svg+xml"
</IfModule>
</IfModule>

Explicitly list each content type that benefits from compression. If a type is not in the list, it passes through uncompressed.

Verifying Compression Is Working

After configuring compression, you need to verify that it is actually active. There are several ways to check.

Using curl

The most reliable method is to request a resource with curl and examine the response headers:

curl -I -H "Accept-Encoding: gzip, deflate, br" https://example.com/

Look for the Content-Encoding header in the response:

HTTP/2 200
content-type: text/html; charset=utf-8
content-encoding: gzip
vary: Accept-Encoding

If Content-Encoding: gzip (or br for Brotli) is present, compression is working.

To see the actual size difference, compare compressed and uncompressed responses:

# Compressed size
curl -so /dev/null -w '%{size_download}' -H "Accept-Encoding: gzip" https://example.com/

# Uncompressed size
curl -so /dev/null -w '%{size_download}' --compressed https://example.com/

Using Browser Developer Tools

  1. Open Developer Tools (F12 or Ctrl+Shift+I).
  2. Go to the Network tab.
  3. Reload the page.
  4. Click on any resource.
  5. In the Response Headers section, look for Content-Encoding: gzip or Content-Encoding: br.

Most browser Network tabs also show two size columns:

  • Size: The compressed (transferred) size.
  • Actual Size (or Content): The uncompressed (original) size.

If these two numbers differ significantly, compression is working.

Using Online Tools

Several online tools test compression:

Checking the Vary Header

When compression is active, Apache should include a Vary: Accept-Encoding header in the response:

Vary: Accept-Encoding

This header tells caches (CDNs, proxies, browsers) that the response content varies based on the Accept-Encoding request header. Without it, a cache might serve a gzip-compressed response to a client that does not support gzip, or an uncompressed response to a client that does support it.

If your Vary header is missing, add it explicitly:

<IfModule mod_headers.c>
<FilesMatch "\.(html|css|js|json|xml|svg|txt|vtt|md)$">
Header append Vary "Accept-Encoding"
</FilesMatch>
</IfModule>

Complete Production Configuration

Here is a comprehensive, production-ready compression configuration:

Complete compression configuration
# === Fix Broken Accept-Encoding Headers ===
<IfModule mod_deflate.c>
<IfModule mod_setenvif.c>
<IfModule mod_headers.c>
SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
</IfModule>
</IfModule>
</IfModule>

# === Brotli Compression (preferred) ===
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS "application/atom+xml" \
"application/javascript" \
"application/json" \
"application/ld+json" \
"application/manifest+json" \
"application/rdf+xml" \
"application/rss+xml" \
"application/schema+json" \
"application/geo+json" \
"application/vnd.ms-fontobject" \
"application/wasm" \
"application/xhtml+xml" \
"application/xml" \
"font/eot" \
"font/opentype" \
"font/otf" \
"font/ttf" \
"image/bmp" \
"image/svg+xml" \
"image/vnd.microsoft.icon" \
"text/cache-manifest" \
"text/calendar" \
"text/css" \
"text/html" \
"text/javascript" \
"text/plain" \
"text/markdown" \
"text/vcard" \
"text/vtt" \
"text/x-component" \
"text/xml"
</IfModule>

# === Gzip Compression (fallback) ===
<IfModule mod_deflate.c>
<IfModule mod_filter.c>
AddOutputFilterByType DEFLATE "application/atom+xml" \
"application/javascript" \
"application/json" \
"application/ld+json" \
"application/manifest+json" \
"application/rdf+xml" \
"application/rss+xml" \
"application/schema+json" \
"application/geo+json" \
"application/vnd.ms-fontobject" \
"application/wasm" \
"application/x-font-ttf" \
"application/x-javascript" \
"application/x-web-app-manifest+json" \
"application/xhtml+xml" \
"application/xml" \
"font/eot" \
"font/opentype" \
"font/otf" \
"font/ttf" \
"image/bmp" \
"image/svg+xml" \
"image/vnd.microsoft.icon" \
"text/cache-manifest" \
"text/calendar" \
"text/css" \
"text/html" \
"text/javascript" \
"text/plain" \
"text/markdown" \
"text/vcard" \
"text/vnd.rim.location.xloc" \
"text/vtt" \
"text/x-component" \
"text/x-cross-domain-policy" \
"text/xml"
</IfModule>
</IfModule>

HTTP compression is one of those rare optimizations that is all upside. It reduces transfer sizes by 60-80% for text-based content, speeds up page loads on every connection type, lowers bandwidth costs, and improves search engine rankings. Configure mod_deflate as a baseline, add mod_brotli if your server supports it, compress text-based formats only, and verify with curl or browser developer tools. The few minutes spent setting this up pay dividends on every single page load your server handles.