Every HTTP response starts with a status line and a stack of headers before a single byte of your page arrives. Those headers decide whether the browser upgrades to HTTPS, blocks an injected script, serves a cached copy, or refuses to hand your JavaScript a cross-origin response. Getting them right is one of the highest-leverage things you can do for a site's security and speed.
What HTTP headers are
An HTTP message is split into a start line, a block of headers, a blank line, and an optional body. Headers are simple Name: value pairs of metadata that travel alongside the payload. They come in two directions:
- Request headers are sent by the client (your browser or a tool like
curl). They describe who is asking and what they can accept — for exampleUser-Agent,Accept,Accept-Encoding,Cookie, andAuthorization. - Response headers are sent back by the server. They describe the resource and instruct the browser how to treat it —
Content-Type,Cache-Control,Set-Cookie, and the security headers below.
This guide focuses on response headers, because those are the ones you configure on your server or CDN and the ones that make or break security and caching behaviour.
Security headers that matter
Modern browsers enforce a set of opt-in protections that are only active if the server explicitly asks for them. Missing them is the most common finding in any web security scan. Here are the ones worth adding to nearly every site.
Strict-Transport-Security (HSTS)
Tells the browser to only ever connect over HTTPS for the given duration, defeating protocol-downgrade and SSL-stripping attacks. Once seen, the browser refuses plain http:// to your domain even if a user types it. A safe value is Strict-Transport-Security: max-age=31536000; includeSubDomains. Only add preload once you are certain every subdomain supports HTTPS, because it is hard to undo.
Content-Security-Policy (CSP)
The most powerful and most complex header. It whitelists exactly which sources scripts, styles, images, and frames may load from, which is your strongest defence against cross-site scripting (XSS). A conservative starting point is Content-Security-Policy: default-src 'self', then loosen it per resource type as needed. Prefer frame-ancestors 'none' here over the older X-Frame-Options when you can.
X-Content-Type-Options
The single value nosniff stops browsers from second-guessing the declared Content-Type and executing, say, an uploaded image as JavaScript. It is a one-line, no-downside win: X-Content-Type-Options: nosniff.
X-Frame-Options / frame-ancestors
Prevents clickjacking by controlling whether your pages can be embedded in an <iframe>. Use X-Frame-Options: DENY (or SAMEORIGIN if you legitimately frame your own pages). The modern equivalent is the CSP frame-ancestors directive, which browsers honour over the older header.
Referrer-Policy & Permissions-Policy
Referrer-Policy: strict-origin-when-cross-origin limits how much of your URLs leak to third parties in the Referer header — you send the full path to your own origin but only the bare origin off-site. Permissions-Policy switches off browser features you don't use, e.g. Permissions-Policy: geolocation=(), camera=(), microphone=(), shrinking the attack surface.
Security header reference
| Header | Purpose | Safe example value |
|---|---|---|
Strict-Transport-Security | Force HTTPS, block downgrade attacks | max-age=31536000; includeSubDomains |
Content-Security-Policy | Whitelist resource sources, stop XSS | default-src 'self' |
X-Content-Type-Options | Disable MIME-type sniffing | nosniff |
X-Frame-Options | Prevent clickjacking via framing | DENY |
Referrer-Policy | Limit referrer data sent off-site | strict-origin-when-cross-origin |
Permissions-Policy | Disable unused browser features | geolocation=(), camera=() |
Content-Security-Policy-Report-Only first so violations are logged, not enforced, until you're confident the policy is clean.Caching headers: Cache-Control, ETag & Last-Modified
Caching headers decide whether the browser and any CDN in between keep a copy of your response and for how long. Done well, they eliminate redundant downloads; done badly, they either serve stale pages or make every visit re-fetch everything.
Cache-Control
This is the primary caching header, and it takes a list of directives:
max-age=SECONDS— how long the response stays fresh before it must be revalidated.public/private— whether shared caches (CDNs, proxies) may store it, or only the user's own browser.no-cache— may be stored, but must revalidate with the server before every reuse.no-store— must not be stored anywhere; every request is a fresh download.immutable— promises the file never changes, so the browser skips revalidation entirely (ideal for fingerprinted assets likeapp.a1b2c3.js).
A typical setup: HTML pages get Cache-Control: no-cache so they always revalidate, while hashed static assets get Cache-Control: public, max-age=31536000, immutable.
ETag and Last-Modified (revalidation)
These two headers make revalidation cheap. An ETag is a fingerprint of the response body; Last-Modified is a timestamp. When a cached copy expires, the browser sends a conditional request with If-None-Match (the ETag) or If-Modified-Since (the date). If nothing changed, the server replies 304 Not Modified with an empty body — the browser reuses its cached copy and you save the full download. This is exactly what no-cache relies on to stay fast.
CORS headers and why requests get blocked
By default, browsers enforce the same-origin policy: JavaScript on https://app.example.com cannot read a response from https://api.other.com. Cross-Origin Resource Sharing (CORS) is the mechanism a server uses to opt in and grant that access via response headers.
Access-Control-Allow-Origin— the origin allowed to read the response, e.g.https://app.example.com, or*for any origin.Access-Control-Allow-Methods— which HTTP methods are permitted, e.g.GET, POST, PUT, DELETE.Access-Control-Allow-Headers— which custom request headers the client may send, e.g.Content-Type, Authorization.Access-Control-Allow-Credentials—trueif cookies or auth headers may be included.
The classic "CORS is blocking me" confusion: the request usually reaches the server and succeeds there. The browser simply refuses to expose the response to your script because the required Access-Control-Allow-Origin header was missing or didn't match your page's origin. That means the fix lives on the server, not in your fetch call. For anything beyond a simple GET, the browser first sends a preflight OPTIONS request — if that preflight doesn't return the right allow headers, the real request never fires.
Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. Browsers reject that pairing because it would let any site make authenticated requests on a user's behalf. When credentials are involved, you must echo back a specific, validated origin instead of *.Content headers: type & encoding
Two response headers describe the body itself:
Content-Typetells the browser how to interpret the bytes, including the character set:Content-Type: text/html; charset=utf-8orapplication/json. A wrong or missing type is a frequent cause of pages rendering as plain text or JSON downloading instead of parsing.Content-Encodingnames the compression applied to the body, such asgziporbr(Brotli). The browser transparently decompresses it. This is negotiated: the client advertises what it supports in theAccept-Encodingrequest header, and the server responds with what it actually used.
Inspecting and debugging headers
You cannot fix headers you cannot see. There are three practical ways to inspect them:
- Browser DevTools. Open the Network tab, reload, click the request, and read the Response Headers panel. Best for the site you're currently on.
- Command line.
curl -I https://example.comprints just the response headers; add-H 'Accept-Encoding: gzip'to see compression negotiation in action. - An online header checker. It fetches the URL server-side and lists every header at once — handy for a site you're not logged into or to compare responses without your local browser cache in the way.
Common misconfigurations to look for while debugging:
- Missing HSTS — the site loads over HTTPS but never sends
Strict-Transport-Security, leaving the first request downgradeable. - Wildcard CORS with credentials —
Access-Control-Allow-Origin: *alongsideAllow-Credentials: true, which browsers silently reject. - no-cache vs no-store confusion — using
no-storewhereno-cachewas meant, forcing full re-downloads of content that could have been cheaply revalidated with a304. - Duplicate or conflicting headers — both the app and the CDN setting
Cache-Controlor a security header, producing unexpected combined behaviour.
Inspect any site's HTTP headers
See every response header, spot missing security headers, and debug caching or CORS in seconds — free, no signup.
Open the Header Checker →Frequently asked questions
What are the most important security headers?
What does Cache-Control do?
Why am I getting a CORS error?
What is the difference between no-cache and no-store?
How do I see a site's response headers?
curl -I https://example.com to print just the headers. An online header checker fetches the URL server-side and lists every response header at once, which is handy for testing a site you are not currently on.