You moved your site to HTTPS. The padlock shows up in the address bar. Job done.
Then a colleague opens the console and finds a wall of yellow warnings, one image never loads on mobile, and the padlock quietly disappears on exactly one page. Welcome to mixed content.
What HTTPS actually protects
HTTP sends everything in readable text. Anyone sitting between your visitor and your server — a coffee shop router, an ISP, a compromised access point — can read what is sent and change it on the way through. That is a man-in-the-middle attack, and it is not theoretical: injecting ads into unencrypted pages was a real business model for a while.
HTTPS wraps that same traffic in encryption. Someone in the middle can still see that you are talking to example.com, but not what is being said, and they cannot alter it without breaking the connection.
The important detail: that protection applies per request, not per page.
What mixed content is
A single page is rarely one request. It is a document, plus stylesheets, scripts, images, fonts, and API calls — often dozens of them.
Mixed content is when an HTTPS page pulls in some of those over plain HTTP.
<!-- The page itself: https://example.com/dashboard ✅ encrypted -->
<img src="http://cdn.example.com/logo.png"> <!-- ❌ not encrypted -->
<script src="http://cdn.example.com/analytics.js"> <!-- ❌ not encrypted, and dangerous -->
The lock icon says the page is secure. Some of what is on the page is not. And the weakest request sets the real security of the page.
Why one HTTP script is worse than one HTTP image
Browsers split mixed content into two categories, and the difference matters.
Passive mixed content — images, video, audio. An attacker who tampers with these can swap your logo for something unpleasant or learn which pages a user visits. Bad, but they cannot take over the page.
Active mixed content — scripts, stylesheets, iframes, fetch/XHR calls. These can read and rewrite everything on the page. If an attacker can modify that one HTTP script in transit, they can read your users' session cookies, rewrite the form they are typing into, and redirect the submit to their own server. One insecure <script> tag hands over the entire page, no matter how well the other ninety-nine requests were encrypted.
This is why the browser's response differs so sharply between the two.
What browsers do about it
Modern browsers have steadily escalated their handling:
- Active mixed content is blocked outright. The request never goes out. You get a console error and the feature silently does not work — which is why "it works locally but the button does nothing in production" is so often this.
- Passive mixed content is auto-upgraded. The browser retries the request over HTTPS. If the server supports it, the image loads and you never notice. If it does not, the request is blocked and the image is simply missing.
- The padlock is downgraded on pages with mixed content, which is the part users actually see.
The net effect: mixed content usually shows up as a bug, not as a security warning. Something is missing or broken, and the cause is two lines deep in the console.
Finding it on your own site
Open DevTools and look in two places:
- Console. Mixed content messages are explicit:
Mixed Content: The page at 'https://...' was loaded over HTTPS, but requested an insecure resource 'http://...'. This request has been blocked.
- Network tab. Sort by protocol, or just search the request list for
http://.
For a whole-site check, crawling tools and the HTTPS section of most SEO auditors will list every offending URL. The commonest sources, in my experience:
- Hardcoded
http:// URLs in old templates, seed data, or a CMS field someone pasted years ago
- A third-party widget or analytics snippet that never got updated
- A CDN or asset domain where HTTPS was never configured
- Absolute URLs baked into database content (user-uploaded posts are a classic)
Fixing it
Use protocol-relative or absolute HTTPS URLs. The straightforward fix is to change http:// to https:// and confirm the resource actually serves over TLS. Do not assume it does — check.
Let the browser upgrade what it can. Adding this header tells the browser to try HTTPS for every subresource before giving up:
Content-Security-Policy: upgrade-insecure-requests
This is a good safety net, especially for legacy content in a database you cannot easily rewrite. It is not a substitute for fixing the URLs, because it only helps where the target server supports HTTPS.
Catch it before it ships. A stricter directive turns mixed content into a reported violation rather than a silent failure:
Content-Security-Policy-Report-Only: default-src https:; report-uri /csp-report
Then lock the door. Once everything is clean, HSTS tells browsers to never attempt HTTP for your domain again:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Be deliberate with that last one. A long max-age is cached by the browser and is genuinely hard to walk back if you have a subdomain that is not ready for HTTPS.
The one-line version
HTTPS secures requests, not pages. A page is only as secure as the least secure thing it loads — and the browser will tell you which one, if you open the console.