Skip to main content

How HTTPS works and difference from HTTP

HTTPS is HTTP with a TLS layer on top. It encrypts every byte between browser and server and verifies the server's identity before any data flows.

Theory

TL;DR

  • HTTP is a postcard (anyone on the network reads it). HTTPS is a sealed envelope with an ID check
  • The main difference: TLS handshake negotiates encryption keys and validates the server certificate before data moves
  • HTTP uses port 80, HTTPS uses port 443
  • TLS 1.3 cut the handshake to one round trip, so the performance gap is now negligible
  • If it touches users or data, use HTTPS. Only localhost is an exception

Quick example

javascript
// Node.js: HTTP vs HTTPS server const express = require('express'); const https = require('https'); const http = require('http'); const fs = require('fs'); const app = express(); app.get('/data', (req, res) => res.json({ token: 'secret-abc123' })); // Port 8080: token visible in Wireshark as plaintext http.createServer(app).listen(8080); // Port 8443: token encrypted, Wireshark shows gibberish https.createServer({ key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') }, app).listen(8443);

On port 8080, every request is readable on the wire. Wireshark shows the token in plain JSON. On port 8443 with TLS, the same response is unreadable without the session keys.

Key difference

HTTP sends everything as plaintext. A proxy, a coffee shop router, or anyone between client and server can read passwords, session tokens, and API keys. HTTPS wraps all of that in TLS, which uses ECDHE to agree on a shared secret and then encrypts data with AES-256-GCM. The server also proves its identity via an X.509 certificate signed by a trusted CA, so the client knows it is talking to the real server and not an impostor.

When to use

  • Login forms, payment flows, or any API that handles tokens: HTTPS, no exceptions
  • Public pages with no user data: HTTPS still, because Google ranks HTTP pages lower and HSTS prevents going back once set
  • Static assets (images, CSS, JS): HTTPS, because mixed content on an HTTPS page gets blocked by the browser
  • Internal tools: HTTPS with self-signed certs (browser shows a warning, but the habit of encrypted connections matters)
  • Local development on localhost: HTTP is fine; browsers treat localhost as a secure context

Comparison table

AspectHTTPHTTPS
Port80443
EncryptionNone (plaintext)TLS 1.3, AES-256-GCM
Server authenticationNoneX.509 cert from a CA (e.g., Let's Encrypt)
Attack surfaceEavesdropping, injection, MITMMitigated (cert pinning adds another layer)
PerformanceNo handshake overhead+10-50ms handshake; negligible with TLS 1.3
When to useLocal dev onlyAll production traffic

How the TLS handshake works

The browser opens TCP to port 443 and sends a ClientHello with supported ciphers and a key share. The server replies with a ServerHello, its certificate chain, and a signed key share. The browser checks the certificate against its CA trust store (Chrome uses BoringSSL, Node.js uses OpenSSL) and computes a shared secret via ECDHE. From that point, all HTTP data travels encrypted with symmetric keys.

TLS 1.3 does this in one round trip. TLS 1.2 needed two. That difference adds up on high-latency connections, which is why upgrading matters.

I have seen legacy internal services skip TLS entirely because the network was "closed." Three weeks later, a compromised developer laptop was the inside network. HTTPS within the perimeter is not paranoia; it is cheap insurance.

Common mistakes

Serving production APIs on HTTP. Tokens and session cookies travel as plaintext. Wireshark on the same Wi-Fi captures them in under a second. The fix:

javascript
// Express: redirect HTTP to HTTPS app.set('trust proxy', 1); app.use((req, res, next) => { if (!req.secure) { return res.redirect(301, `https://${req.headers.host}${req.url}`); } next(); });

Ignoring certificate expiry. Let's Encrypt certificates expire every 90 days. Browsers show a hard block, not a warning. Automate renewal:

bash
certbot renew --dry-run # Add to cron: 0 3 * * * certbot renew --quiet

Using self-signed certs in production. Browsers show NET::ERR_CERT_AUTHORITY_INVALID and most users leave immediately. Self-signed certs are fine for internal dev. Production needs a cert from a real CA. Let's Encrypt is free.

No HSTS header. Without it, a user who types example.com hits HTTP first, giving an attacker a one-request window for a downgrade attack. The fix:

javascript
res.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');

HTTP/2 without TLS. Browsers require TLS for HTTP/2 per RFC 7540. Enabling HTTP/2 in Nginx without a valid certificate causes browsers to fall back to HTTP/1.1 silently.

Real-world usage

  • Express: https.createServer({ key, cert }, app) for any API handling user data
  • Next.js: HSTS via next.config.js headers array with max-age=63072000
  • Nginx: ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; with Certbot for auto-renewal
  • Stripe: registers only HTTPS URLs for webhooks; HTTP endpoints are rejected at registration
  • AWS ALB: enforce HTTPS via a 301 redirect listener rule on port 80

Follow-up questions

Q: What changed between TLS 1.2 and TLS 1.3?
A: TLS 1.3 cut the handshake from 2 round trips to 1, removed RSA key exchange (which had no forward secrecy), and made AEAD ciphers like AES-GCM mandatory. Fewer negotiation options also means a smaller attack surface.

Q: What is forward secrecy and why does it matter?
A: ECDHE generates a fresh key pair per session. If an attacker records encrypted traffic today and later steals the server's private key, they still cannot decrypt old sessions. TLS 1.2 with RSA key exchange does not have this property.

Q: How does certificate validation actually work?
A: The browser checks the certificate's signature against the issuing CA's public key, walks up the chain to a root CA in its trust store, checks the expiry date, and queries OCSP or a CRL to confirm the cert was not revoked.

Q: What is HSTS preloading?
A: Browsers ship with a hardcoded list of domains from hstspreload.org that are forced to HTTPS before any request is made. Google requires max-age of at least one year plus includeSubDomains before accepting a submission. Removal from the list takes months and browser release cycles. Once preloaded, there is no fast rollback.

Q: What is a mixed content error?
A: An HTTPS page loading any resource over HTTP triggers mixed content. Scripts and iframes get blocked. Images show a warning. The fix is to use relative URLs or ensure every subresource is on HTTPS.

Examples

Basic: HTTP vs HTTPS server in Node.js

javascript
const express = require('express'); const https = require('https'); const http = require('http'); const fs = require('fs'); const app = express(); app.get('/profile', (req, res) => { res.json({ user: 'alice', token: 'secret-abc123' }); }); // HTTP: token visible in Wireshark as plaintext JSON http.createServer(app).listen(8080, () => { console.log('HTTP on 8080 - traffic is sniffable'); }); // HTTPS: token encrypted, Wireshark shows raw bytes https.createServer({ key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') }, app).listen(8443, () => { console.log('HTTPS on 8443 - encrypted'); });

curl http://localhost:8080/profile returns the token in readable JSON. On a shared network, anyone can capture it with Wireshark. The HTTPS version returns the same data but encrypted end-to-end.

Intermediate: HTTPS redirect and Stripe webhook

javascript
const express = require('express'); const https = require('https'); const http = require('http'); const fs = require('fs'); const stripe = require('stripe')('sk_test_...'); const app = express(); app.use(express.raw({ type: 'application/json' })); // Redirect all HTTP traffic to HTTPS http.createServer((req, res) => { res.writeHead(301, { Location: `https://${req.headers.host}${req.url}` }); res.end(); }).listen(80); // Stripe requires HTTPS for webhook endpoints app.post('/webhook', (req, res) => { const sig = req.headers['stripe-signature']; try { const event = stripe.webhooks.constructEvent(req.body, sig, 'whsec_...'); // handle event res.json({ received: true }); } catch (err) { res.status(400).send(`Webhook error: ${err.message}`); } }); https.createServer({ key: fs.readFileSync('privkey.pem'), cert: fs.readFileSync('fullchain.pem') }, app).listen(443);

Stripe validates stripe-signature on every webhook. Over HTTP, that signature can be stripped and replaced. HTTPS prevents that. Stripe also rejects HTTP URLs at registration time, so this is not optional.

Advanced: HSTS and mixed content in the browser

javascript
// Express: set HSTS header on every response app.use((req, res, next) => { res.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); next(); }); // React: fetch works because HSTS blocks HTTP for preloaded domains async function fetchUserData(userId) { const response = await fetch(`https://api.example.com/users/${userId}`, { credentials: 'include', // cookies only sent over TLS referrerPolicy: 'strict-origin-when-cross-origin' }); if (!response.ok) throw new Error('Fetch failed'); return response.json(); } // Mixed content: blocked by Chrome on HTTPS pages const img = new Image(); img.src = 'http://insecure.com/photo.jpg'; // Chrome blocks this console.log('Blocked: mixed content policy');

The HSTS header tells the browser to refuse HTTP for this origin for up to a year. The preload flag submits the domain to the list baked into Chrome. The blocked image appears in DevTools Network tab with status (blocked:mixed-content).

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?