HSTS report-only mode sounds like something browsers should support.
They don’t.
That’s the first thing worth clearing up, because a lot of developers go looking for a Strict-Transport-Security-Report-Only header and assume they just haven’t found the right syntax yet. There is no standardized HSTS report-only header implemented by browsers in the way Content-Security-Policy-Report-Only works.
So if you’re trying to safely “test” HSTS before enforcing it, the real answer is a mix of:
- understanding what HSTS actually does
- using short
max-agevalues during rollout - using reporting mechanisms around HTTPS failures where available
- validating your current headers and redirects before increasing enforcement
If you want a quick sanity check on your current headers, run your site through Headertest.
The short version
There is no browser-supported header like this:
Strict-Transport-Security-Report-Only: max-age=31536000
And this also won’t do anything in browsers:
Strict-Transport-Security: max-age=31536000; report-to="default"
Browsers ignore reporting directives for HSTS because HSTS doesn’t define a report-only mode or reporting integration like CSP does.
If somebody tells you to “deploy HSTS in report-only first,” what they usually mean is:
- make sure HTTP redirects cleanly to HTTPS
- fix mixed content and insecure subresource references
- start with a very small HSTS
max-age - observe production behavior
- gradually increase
max-age - only add
includeSubDomainsand preload when you’re absolutely sure
That’s the operationally safe path.
What HSTS actually enforces
The real header is:
Strict-Transport-Security: max-age=31536000; includeSubDomains
This tells the browser:
- for the next
max-ageseconds - only connect to this site over HTTPS
- and optionally apply that to all subdomains too
Once a browser has seen that header over a valid HTTPS connection, it rewrites future HTTP requests to HTTPS before the request leaves the browser.
That means HSTS is not just a redirect optimization. It’s a client-side policy cache.
That’s also why a fake report-only mode would be tricky: HSTS changes browser connection behavior, not just document policy evaluation.
Why teams ask for report-only mode
Usually because they’re worried about these rollout risks:
- legacy subdomains that still serve plain HTTP
- broken certificates on less-used hosts
- old internal links pointing to
http:// - third-party integrations hitting HTTP endpoints
- accidentally bricking subdomains with
includeSubDomains - getting stuck with a long
max-ageafter a bad deployment
Those are valid concerns. I’ve seen teams break staging, uploads, old admin portals, and random forgotten support tools by turning on includeSubDomains too early.
The fix is not “HSTS report-only.” The fix is a staged rollout.
Safe rollout pattern that replaces report-only mode
Phase 1: Verify HTTPS everywhere
Before setting HSTS, make sure:
- the apex domain serves HTTPS correctly
wwwserves HTTPS correctly- any user-facing subdomains have valid certificates
- HTTP redirects to HTTPS with a clean 301 or 308
- no redirect loops
- no certificate name mismatches
A minimal redirect setup in Nginx:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
And HTTPS with HSTS disabled initially:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/ssl/example/fullchain.pem;
ssl_certificate_key /etc/ssl/example/privkey.pem;
root /var/www/html;
index index.html;
}
Phase 2: Start with a tiny max-age
Use a short duration first. I usually start with 5 minutes or 1 hour, not a year.
Strict-Transport-Security: max-age=300
Or in Nginx:
add_header Strict-Transport-Security "max-age=300" always;
Apache:
Header always set Strict-Transport-Security "max-age=300"
Express.js with Helmet:
import express from "express";
import helmet from "helmet";
const app = express();
app.use(
helmet.hsts({
maxAge: 300,
includeSubDomains: false,
preload: false,
})
);
app.listen(3000);
This gives you real enforcement, but with a very short recovery window if something goes wrong.
Phase 3: Increase gradually
If the short rollout is clean, increase in steps:
Strict-Transport-Security: max-age=86400
Then:
Strict-Transport-Security: max-age=604800
Then:
Strict-Transport-Security: max-age=31536000
That progression is much safer than jumping straight to one year.
Phase 4: Add includeSubDomains only after inventory
This is where people hurt themselves.
Strict-Transport-Security: max-age=31536000; includeSubDomains
Do not add includeSubDomains until you’ve accounted for every relevant subdomain you care about. That includes weird old hosts nobody remembers until they stop working.
If you have a mixed environment like this:
www.example.com— public site, HTTPS readyapi.example.com— HTTPS readyoldcrm.example.com— still broken over HTTPSdev.example.com— self-signed certstatus.example.com— third-party managed
Then includeSubDomains is not ready yet.
Phase 5: Consider preload last
Preload is not “extra secure HSTS.” It’s a commitment.
Typical preload-ready header:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Before preload, make sure you actually meet the preload requirements and want every browser to hardcode your domain as HTTPS-only. If you later discover a broken subdomain, preload removal is slow and annoying.
Official docs for preload behavior and HSTS are covered by browser vendors and the HTTP specification, including MDN’s HSTS reference and the spec itself:
Can you get reports for HSTS-related problems?
Not directly from an HSTS report-only mode, because again, that mode does not exist.
What you can do is use surrounding signals.
1. Monitor failed HTTP requests at the edge
If you still receive traffic on port 80 for URLs that should already be HTTPS-only, log it. That gives you a practical picture of who still relies on HTTP entry points.
Nginx example:
log_format hsts_http '$remote_addr - $host "$request" $status "$http_user_agent"';
server {
listen 80;
server_name example.com www.example.com;
access_log /var/log/nginx/hsts-http.log hsts_http;
return 301 https://$host$request_uri;
}
This won’t tell you “HSTS would have blocked this,” but it tells you how much HTTP traffic still exists.
2. Use Reporting API for other policy issues
The Reporting API is useful for CSP, COOP, network errors in some contexts, and browser-generated reports depending on support. It is not a substitute for HSTS report-only, but it’s still worth knowing.
Basic reporting endpoint declaration:
Reporting-Endpoints: default="https://reports.example.com/reports"
A tiny Express receiver:
import express from "express";
const app = express();
app.use(express.json({ type: ["application/json", "application/reports+json"] }));
app.post("/reports", (req, res) => {
console.log(JSON.stringify(req.body, null, 2));
res.sendStatus(204);
});
app.listen(8080);
You can combine this with CSP report-only while cleaning up insecure references:
Reporting-Endpoints: default="https://reports.example.com/reports"
Content-Security-Policy-Report-Only: default-src 'self'; upgrade-insecure-requests; report-to default
That’s useful because teams often confuse mixed content cleanup with HSTS rollout. They’re related, but not the same mechanism.
3. Track certificate and TLS failures separately
If your real concern is “will HTTPS break for some clients or hosts,” then certificate monitoring and TLS handshake monitoring matter more than imaginary HSTS reports.
Common mistakes
Sending HSTS over HTTP
Browsers ignore HSTS received over plain HTTP.
This does nothing useful:
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000
The header must be delivered over valid HTTPS.
Setting a huge max-age on day one
This is the classic self-own:
Strict-Transport-Security: max-age=31536000; includeSubDomains
If one subdomain is broken, you’ve just created a hard failure for users whose browsers cached the policy.
Assuming redirects are equivalent to HSTS
A redirect helps after the first insecure request. HSTS prevents that first insecure hop on subsequent visits.
You want both:
- HTTP to HTTPS redirect on the server
- HSTS on the HTTPS response
Forgetting that localhost and fresh browsers behave differently
HSTS only applies after the browser has seen the header for that host. Testing in a clean browser profile often reveals first-visit behavior you won’t notice in your own cached environment.
Copy-paste production examples
Nginx, cautious rollout
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/ssl/example/fullchain.pem;
ssl_certificate_key /etc/ssl/example/privkey.pem;
add_header Strict-Transport-Security "max-age=300" always;
root /var/www/html;
index index.html;
}
Later:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Apache
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
Redirect permanent / https://example.com/
</VirtualHost>
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
SSLEngine on
SSLCertificateFile /etc/ssl/example/fullchain.pem
SSLCertificateKeyFile /etc/ssl/example/privkey.pem
Header always set Strict-Transport-Security "max-age=300"
</VirtualHost>
Express with Helmet
import express from "express";
import helmet from "helmet";
const app = express();
app.enable("trust proxy");
app.use((req, res, next) => {
if (!req.secure) {
return res.redirect(301, `https://${req.headers.host}${req.url}`);
}
next();
});
app.use(
helmet.hsts({
maxAge: 300,
includeSubDomains: false,
preload: false,
})
);
app.get("/", (req, res) => {
res.send("ok");
});
app.listen(3000);
The practical takeaway
HSTS report-only mode is basically a myth in browser deployment.
If you need a safe way to adopt HSTS, do this instead:
- verify HTTPS coverage
- keep HTTP redirects in place
- start with
max-age=300 - increase gradually
- hold off on
includeSubDomains - treat preload as a one-way door
That’s the real-world playbook. It’s boring, but boring is what you want from transport security.