Static files — images, CSS, JavaScript, fonts, downloadable PDFs — usually make up the majority of a website’s total requests, even on sites that are technically “dynamic.” How efficiently a server handles these files has an outsized effect on page load speed, bandwidth costs, and overall user experience. Apache, despite its reputation as a “heavier” server compared to something like Nginx, can serve static content extremely efficiently once it’s configured properly.
This guide covers the specific modules, directives, and tuning steps that make Apache genuinely fast at static file delivery.
Why Static File Performance Matters
Every unnecessary byte sent or unnecessary round trip made adds latency. A few compounding effects:
- Page speed directly affects SEO rankings — Core Web Vitals metrics like Largest Contentful Paint are heavily influenced by static asset delivery time.
- Bandwidth costs money — uncompressed assets and missing cache headers mean repeated, avoidable downloads.
- User experience and conversion — studies consistently show bounce rates climb sharply as load time increases.
- Server resource usage — inefficient static file handling ties up worker processes that could be serving dynamic requests instead.
Prerequisites
- A working Apache installation (Ubuntu/Debian examples used below)
- Root or sudo access
- Basic familiarity with Apache’s directory and virtual host configuration
- An existing site with static assets (images, CSS, JS) to optimize
Step 1: Enable the Core Performance Modules
A handful of modules do most of the heavy lifting for static content performance:
sudo a2enmod expires
sudo a2enmod headers
sudo a2enmod deflate
sudo a2enmod brotli
sudo a2enmod cache
sudo a2enmod cache_disk
sudo systemctl restart apache2
mod_expires— setsExpiresandCache-Controlheaders so browsers cache assets instead of re-requesting them.mod_headers— lets you fine-tune arbitrary response headers.mod_deflate— gzip compression for text-based assets.mod_brotli— Brotli compression, typically 15-20% smaller than gzip for the same content (available in Apache 2.4.26+).mod_cache/mod_cache_disk— server-side caching layer, useful when static files are generated dynamically but rarely change.
Step 2: Configure Browser Caching with mod_expires
Add this inside your virtual host or in a global config file:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType text/html "access plus 1 hour"
</IfModule>
Long cache lifetimes for images and fonts make sense because they rarely change; shorter lifetimes for HTML avoid serving stale content after a deploy. If you version your CSS/JS filenames (e.g., app.a1b2c3.js), you can safely cache those for a year too, since a new deploy means a new filename.
Step 3: Add Cache-Control Headers Explicitly
mod_expires sets Expires, but modern browsers prioritize Cache-Control. Combine both:
<IfModule mod_headers.c>
<FilesMatch "\.(jpg|jpeg|png|gif|webp|svg|woff2?|ttf|eot)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "\.(css|js)$">
Header set Cache-Control "public, max-age=2592000"
</FilesMatch>
</IfModule>
The immutable flag tells supporting browsers not to even revalidate the file before its expiry — a meaningful speedup for repeat visitors.
Step 4: Enable Compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
AddOutputFilterByType DEFLATE application/javascript application/json
AddOutputFilterByType DEFLATE image/svg+xml
</IfModule>
If mod_brotli is available, prefer it over gzip for supporting clients — Apache will automatically negotiate based on the client’s Accept-Encoding header:
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript
</IfModule>
Don’t bother compressing already-compressed formats like JPEG, PNG, WebP, or MP4 — it wastes CPU for negligible or negative size gains.
Step 5: Serve Files with sendfile and Efficient MPM
Apache’s EnableSendfile directive lets the OS kernel handle file transfer directly, bypassing extra userspace copies:
EnableSendfile On
For static-heavy workloads, the event MPM generally outperforms the older prefork MPM because it handles keep-alive connections more efficiently:
sudo a2dismod mpm_prefork
sudo a2enmod mpm_event
sudo systemctl restart apache2
Note: if you’re running mod_php (not PHP-FPM), you’ll need to stick with mpm_prefork, since mod_php isn’t thread-safe. This is another good reason to migrate to PHP-FPM if PHP is in the mix at all.
Step 6: Set Up a Dedicated Static Assets Virtual Host (Optional)
For high-traffic sites, separating static content onto its own subdomain or path can simplify caching and CDN integration:
<VirtualHost *:80>
ServerName static.yourdomain.com
DocumentRoot /var/www/static
<Directory /var/www/static>
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
</Directory>
<IfModule mod_headers.c>
Header set Cache-Control "public, max-age=31536000, immutable"
Header unset ETag
</IfModule>
FileETag None
</VirtualHost>
Disabling ETag here is intentional — when serving from multiple servers or after a restart, inode-based ETags can mismatch and cause unnecessary revalidation; a long Cache-Control max-age already covers the caching need.
Step 7: Offload to a CDN (Recommended for Production)
Even a perfectly tuned Apache server benefits from a CDN in front of it for static assets — reduced latency via edge locations, reduced origin load, and built-in DDoS mitigation. Popular options include Cloudflare, AWS CloudFront, and Bunny CDN. Point the CDN at your static virtual host or asset path, and let it handle edge caching using the Cache-Control headers you’ve already configured.
Real-World Use Cases
- E-commerce product images — thousands of product photos benefit enormously from long-lived caching and compression.
- SaaS dashboards — CSS/JS bundles served with immutable caching dramatically speed up repeat logins.
- Documentation sites — often almost entirely static, making this exact tuning the single biggest performance lever available.
- Media/download sites — large file downloads benefit from
sendfileand properContent-Dispositionheaders.
Troubleshooting Common Issues
Assets aren’t being cached despite the config Check the actual response headers:
curl -I https://yourdomain.com/style.css
Look for Cache-Control and Expires in the output. If missing, confirm the modules are enabled:
apache2ctl -M | grep -E "expires|headers|deflate"
Compression isn’t happening Verify with:
curl -H "Accept-Encoding: gzip" -I https://yourdomain.com/script.js
Look for Content-Encoding: gzip in the response. If absent, check that the MIME type in your AddOutputFilterByType matches what’s actually being served — a mismatched Content-Type header will silently skip compression.
Old cached versions serving after deploy This is a caching success, not a failure — but it means you need cache-busting. Use versioned filenames or query strings (style.css?v=2) so browsers fetch the new file.
High CPU from mod_deflate Compression has a CPU cost. If your server is CPU-bound rather than bandwidth-bound, consider lowering the compression level:
DeflateCompressionLevel 6
(Default is 9; 6 is a common speed/ratio compromise.)
Security Best Practices
- Disable directory listing everywhere static files are served:
Options -Indexes - Prevent execution of scripts in upload/static directories:
<Directory /var/www/static/uploads> php_admin_flag engine off RemoveHandler .php .phtml</Directory> - Set
X-Content-Type-Options: nosniffto prevent MIME-sniffing attacks:Header set X-Content-Type-Options "nosniff" - Restrict access to sensitive static files (backups, config files,
.gitdirectories):<FilesMatch "^\.(git|env|htaccess)"> Require all denied</FilesMatch> - Keep Apache and its modules patched — static file serving is a common target for path traversal attempts.
Performance Optimization Checklist
- Enable
mod_expiresandmod_headersfor long-lived cache headers - Enable
mod_deflateand/ormod_brotlifor text-based assets - Use
EnableSendfile Onand the event MPM where possible - Version or hash static filenames for safe long-term caching
- Serve images in modern formats (WebP/AVIF) alongside fallbacks
- Put a CDN in front of static assets for production sites
- Disable ETags in multi-server setups to avoid revalidation mismatches
- Regularly audit uncompressed or uncached assets with browser dev tools or Lighthouse
Frequently Asked Questions
Does Apache’s static file performance really compete with Nginx? For most real-world traffic levels, yes — especially with mod_event, sendfile, and proper caching enabled. Nginx’s edge tends to show up mainly under extremely high concurrency (tens of thousands of simultaneous connections).
Should I compress images with mod_deflate? No. JPEG, PNG, and WebP are already compressed formats; running them through gzip/Brotli wastes CPU for little to no size reduction. Compression should target text-based assets.
What’s the difference between Expires and Cache-Control? Expires is the older HTTP/1.0-era header specifying an absolute date; Cache-Control (HTTP/1.1) is more flexible, using relative max-age values and additional directives like immutable and no-cache. Modern browsers prioritize Cache-Control when both are present.
Is a CDN necessary if Apache is already tuned well? Not strictly necessary, but highly recommended for production sites with geographically distributed visitors — a CDN reduces latency in ways origin-server tuning alone can’t.
Summary and Key Takeaways
Serving static files efficiently with Apache comes down to a short list of well-understood levers: enable long-lived browser caching with mod_expires and mod_headers, compress text assets with mod_deflate/mod_brotli, let the kernel handle file transfer with sendfile, and choose the event MPM when your stack allows it. Layer a CDN on top for production traffic, and you’ve got a static file delivery setup that competes with anything else on the market.
None of this requires exotic configuration — it’s a handful of directives applied consistently, then verified with curl -I and real browser testing. Get these fundamentals in place once, and static asset delivery stops being something you have to think about.