Every time your server responds to a request, it sends a set of HTTP headers before the actual content. These headers are instructions — they tell browsers how to cache the response, what encoding was used, whether the connection should stay alive, and much more. Misconfigured headers silently degrade performance, break caching, and expose security vulnerabilities. This article walks through every header that matters, explains what each one does, and shows you how to configure them correctly.
Before optimizing, you need to see what you're working with. Use the GF.dev HTTP Headers Test to inspect the full response headers for any URL. You can also use curl from the command line:
curl -I https://example.com
The -I flag sends a HEAD request and displays only the headers. For a more detailed view including timing information:
curl -w "TTFB: %{time_starttransfer}s\n" -o /dev/null -s https://example.com
The most impactful header for performance. Cache-Control dictates how browsers and intermediary caches store and reuse responses. A well-configured Cache-Control header can eliminate the majority of requests to your origin server.
Key directives:
max-age=31536000 — Cache for one year. Use for versioned static assets (e.g., app.a1b2c3.js) where the filename changes on every build.no-cache — Cache the response but revalidate with the server before using it. The browser sends a conditional request with If-None-Match or If-Modified-Since. If the content hasn't changed, the server responds with 304 Not Modified (no body), saving bandwidth.no-store — Do not cache at all. Use for sensitive data like banking pages or authenticated API responses.public — Allow shared caches (CDNs, proxies) to store the response. Required if you want CDN caching for responses that might otherwise be considered private.private — Only the end user's browser may cache the response. Use for user-specific content.s-maxage=3600 — Like max-age but applies only to shared caches (CDNs). This lets you set a shorter browser cache and a longer CDN cache.stale-while-revalidate=60 — Serve stale content for up to 60 seconds while fetching a fresh version in the background. Dramatically improves perceived performance.Example for a static CSS file:
Cache-Control: public, max-age=31536000, immutable
Example for a dynamic HTML page:
Cache-Control: public, max-age=0, s-maxage=3600, stale-while-revalidate=60
This header confirms that the server compressed the response body. Without compression, text-based assets (HTML, CSS, JavaScript, JSON, SVG) transfer at their full size, wasting bandwidth and slowing page loads.
Modern options:
Content-Encoding: gzip — Universally supported. Reduces text-based response sizes by 60–80%.Content-Encoding: br — Brotli compression, 15–20% more efficient than gzip. Supported by all modern browsers over HTTPS.In Nginx, enable both:
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
# For Brotli (requires ngx_brotli module)
brotli on;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
If the HTTP Headers Test doesn't show a Content-Encoding header for your HTML pages, compression isn't active and you're leaving easy performance gains on the table.
These headers enable conditional requests — one of the most elegant caching mechanisms in HTTP. When a browser has a cached copy with an ETag, subsequent requests include If-None-Match: "etag-value". If the content hasn't changed, the server returns 304 Not Modified with no body, consuming minimal bandwidth.
ETag: "a1b2c3d4" — A content hash or version identifier. Strong ETags guarantee byte-for-byte identity.Last-Modified: Wed, 01 Mar 2026 00:00:00 GMT — A timestamp-based validator. Less precise than ETags but simpler to implement.Most web servers generate these automatically for static files. For dynamic content, you'll need to generate them in your application code.
HTTP/1.1 uses Connection: keep-alive by default, meaning the TCP connection persists across multiple requests. This avoids the overhead of a new three-way handshake for every asset. If you see Connection: close, something is misconfigured — likely a proxy or load balancer terminating connections prematurely. In HTTP/2 and HTTP/3, connection management is handled at the protocol level, and this header is obsolete.
While HSTS is a security header (covered in our SSL/TLS guide), it has a direct performance benefit: once a browser has seen the HSTS header, it will never attempt an insecure HTTP connection to your domain. This eliminates the HTTP → HTTPS redirect that otherwise adds 100–300 ms to the first request. Configure it as:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
An overly permissive CSP doesn't hurt performance, but a well-crafted one can prevent unauthorized scripts from loading — scripts that might otherwise consume bandwidth and CPU cycles. CSP won't speed up your server, but it can prevent performance degradation from injected resources.
The Server header exposes your web server software and version. While it doesn't affect performance, it's a security concern we've dedicated a full article to: Server Signature Exposure: Why You Should Hide Your Server Version. Check yours with the Server Signature Test.
Several default headers provide no benefit and should be stripped:
X-Powered-By — Exposes your application framework (e.g., X-Powered-By: PHP/8.1 or Express). Remove it. In PHP, add expose_php = Off to php.ini. In Express: app.disable('x-powered-by').X-AspNet-Version — .NET version disclosure. Remove via web.config.Via — Added by proxies and CDNs. Usually harmless but can reveal internal infrastructure details.Here's a practical Nginx configuration block that implements the recommendations above:
# Performance headers
add_header Cache-Control "public, max-age=3600" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Remove information leakage
server_tokens off;
proxy_hide_header X-Powered-By;
# Static assets with long cache
location ~* \.(css|js|woff2|png|jpg|webp|svg|ico)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
HTTP headers are the control plane of web performance. Misconfigured caching headers alone can multiply your server's traffic by 10x, as browsers re-request resources they should have cached locally. Regularly audit your headers using the HTTP Headers Test and cross-reference with the TTFB Test to see the real-world impact of your changes.
For a complete performance troubleshooting methodology that encompasses headers alongside TTFB optimization, network diagnostics, and caching strategies, see our Web Server Performance Troubleshooting pillar guide.