I’ve seen the same Render setup more than once: HTTPS is enabled, the app works, redirects are in place, and everyone assumes transport security is “done.” Then someone runs a header scan and realizes the site still skips one of the most useful protections you can ship in five minutes: HSTS.
This is a real-world style case study based on a common Render deployment pattern. Same stack, same hosting model, same mistake.
The setup
A small SaaS marketing site and API were deployed on Render:
- Frontend: static site on Render
- Backend: Node/Express web service on Render
- Custom domain attached
- Render-managed TLS enabled
- HTTP redirected to HTTPS
From a product perspective, everything looked fine. The team had the padlock. No browser warnings. No obvious issues.
From a security perspective, the site was still missing a guardrail.
The problem
The application redirected HTTP to HTTPS, but it did not send the Strict-Transport-Security header.
That matters because redirects alone are not enough. On a user’s first visit, a browser may still attempt plain HTTP before it learns to use HTTPS. That leaves room for downgrade and interception attacks on hostile networks.
HSTS tells the browser:
- only use HTTPS for this domain
- keep doing that for a defined period
- optionally apply the rule to subdomains too
If you serve real traffic over a custom domain, I think HSTS should be your default unless you have a very specific reason not to use it.
Before: what the Render deployment looked like
The team had this Express app behind Render:
import express from "express";
const app = express();
app.use((req, res, next) => {
if (req.header("x-forwarded-proto") !== "https") {
return res.redirect(301, `https://${req.hostname}${req.originalUrl}`);
}
next();
});
app.get("/", (req, res) => {
res.send("Hello from Render");
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Listening on ${port}`);
});
Looks reasonable. Render terminates TLS at the edge and forwards requests to the app. The redirect uses x-forwarded-proto, which is the right idea on a proxy-based platform.
But the response headers looked like this:
HTTP/2 200
content-type: text/html; charset=utf-8
x-powered-by: Express
No HSTS.
A quick scan with Headertest flagged it immediately.
Why this happens on Render
Render gives you HTTPS termination, certificates, and redirect support, but HSTS is still your responsibility at the application or edge configuration layer.
That catches people off guard. They assume “managed TLS” means “managed transport policy.” It doesn’t.
TLS answers: can we encrypt this connection?
HSTS answers: will the browser refuse to ever use HTTP for this site after learning the policy?
Different jobs.
The fix
For the Express service, the cleanest fix was to set the header on all HTTPS responses.
The team switched to this:
import express from "express";
const app = express();
app.set("trust proxy", true);
app.use((req, res, next) => {
if (!req.secure) {
return res.redirect(301, `https://${req.hostname}${req.originalUrl}`);
}
res.setHeader(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains"
);
next();
});
app.get("/", (req, res) => {
res.send("Hello from Render");
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Listening on ${port}`);
});
Two changes matter here:
-
app.set("trust proxy", true)
Without this,req.securemay be wrong behind Render’s proxy. -
The HSTS header is only sent on HTTPS responses
That’s how it should be. Browsers ignore HSTS over plain HTTP anyway.
After deployment, the headers looked like this:
HTTP/2 200
content-type: text/html; charset=utf-8
strict-transport-security: max-age=31536000; includeSubDomains
That’s already a big improvement.
Before and after for a static site on Render
The static-site version of this problem is even more common.
Before
A frontend app had a _redirects file:
http://example.com/* https://example.com/:splat 301!
Traffic got pushed to HTTPS, but the final HTTPS response didn’t include HSTS.
After
The team added a _headers file to the static site:
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains
That’s it.
If you’re serving a static site on Render, _headers is usually the easiest place to manage HSTS and other security headers.
The rollout mistake I see most often
People jump straight to this:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
That can be fine later. I would not start there.
HSTS is sticky. Once a browser caches it, you can’t easily undo the effect for that user until max-age expires. If you include subdomains and one of them still serves mixed or broken TLS, you just created your own outage.
The safer rollout is staged.
Stage 1: short max-age
Start with something small:
Strict-Transport-Security: max-age=300
Five minutes is enough to verify behavior without committing users for months.
Stage 2: increase after validation
If everything looks good:
Strict-Transport-Security: max-age=86400
Then move to:
Strict-Transport-Security: max-age=31536000
Stage 3: add subdomains only when you mean it
Only add this when every subdomain is actually ready:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Stage 4: consider preload carefully
If you want preload, you need to meet browser preload requirements and be very sure every subdomain is permanently HTTPS-only.
Example:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Preload is not something I enable casually. It’s great when you’re ready. It’s painful when you’re not.
For preload requirements and browser behavior, check the official docs at MDN’s HSTS reference.
The actual outcome
After adding HSTS and rolling it out in stages, the team ended up with:
- HTTPS enforced by redirect
- browser-side HTTPS pinning via HSTS
- cleaner scanner results
- less exposure to downgrade attacks on first-return visits
- fewer questions during customer security reviews
That last one matters more than people admit. Security questionnaires love asking whether HSTS is enabled. It’s low effort and high signal.
A couple of Render-specific gotchas
1. Don’t forget trust proxy for Express
If you’re checking req.secure on Render and haven’t enabled proxy trust, your app may think every request is HTTP.
Use:
app.set("trust proxy", true);
I’ve debugged this one enough times that I now add it almost by muscle memory on hosted Node platforms.
2. Don’t set HSTS on preview or temporary domains unless you understand the impact
If you use lots of test subdomains or temporary environments, be careful with includeSubDomains. One aggressive HSTS policy at the parent domain can affect all of them.
3. Don’t ship HSTS if some subdomains still need HTTP
This sounds obvious, but internal tools, legacy callbacks, and old asset hosts are where HSTS rollouts go to die.
Inventory the domain space first.
A practical baseline I’d use
For a typical production Render deployment on a custom domain, this is a sane starting header:
Strict-Transport-Security: max-age=31536000
Then, once you verify all subdomains are clean:
Strict-Transport-Security: max-age=31536000; includeSubDomains
And only after that, maybe preload.
How to verify it
You can verify HSTS in a few ways.
Browser devtools
Check the response headers on the HTTPS response.
curl
curl -I https://yourdomain.com
You want to see:
Strict-Transport-Security: max-age=31536000
Header scanner
Use Headertest for a quick pass, especially if you want to confirm the header is present across redirects and final responses.
Official docs
For Render deployment behavior and config options, check the official docs at Render Documentation.
The final state
Here’s the “after” Express version I’d actually keep in production:
import express from "express";
const app = express();
app.set("trust proxy", true);
app.use((req, res, next) => {
if (!req.secure) {
return res.redirect(301, `https://${req.hostname}${req.originalUrl}`);
}
res.setHeader("Strict-Transport-Security", "max-age=31536000");
next();
});
app.get("/", (req, res) => {
res.send("Secure on Render");
});
const port = process.env.PORT || 3000;
app.listen(port);
And for a Render static site:
_headers
/*
Strict-Transport-Security: max-age=31536000
That’s the whole fix.
If your Render deployment already has HTTPS, adding HSTS is one of those rare security tasks that is both easy and genuinely useful. I’d call it baseline hardening, not “extra credit.”