wss vs ws: Secure WebSocket vs Unencrypted Explained
ws:// vs wss:// at a Glance
Section titled “ws:// vs wss:// at a Glance”ws:// | wss:// | |
|---|---|---|
| Encryption | None | TLS (same as HTTPS) |
| Default port | 80 | 443 |
| Works on HTTPS pages | No (mixed content) | Yes |
| Passes through proxies | Often stripped or blocked | Yes (encrypted tunnel) |
| Production use | Local development only | Required |
Why wss:// Is Not Optional
Section titled “Why wss:// Is Not Optional”Encryption is reason enough. But two practical issues make
ws:// unusable in production even if security is not your
primary concern.
Mixed content blocking
Section titled “Mixed content blocking”If your page is served over HTTPS — and every production page
should be — browsers refuse to open ws:// connections. This
is the same mixed content policy that blocks HTTP images on
HTTPS pages. The connection fails silently. No error dialog.
No user prompt. Just a rejected WebSocket in the console.
Mixed Content: The page was loaded over HTTPS, but attemptedto connect to the insecure WebSocket endpoint 'ws://...'.This request has been blocked.There is no workaround. No header, no flag, no user override.
If your page is HTTPS, your WebSocket must be wss://.
Proxy and firewall interference
Section titled “Proxy and firewall interference”Corporate proxies, transparent proxies, and ISP-level
middleboxes inspect unencrypted traffic. They often don’t
understand the WebSocket upgrade handshake and either drop the
connection or strip the Upgrade header. The result: your
WebSocket works on your home network and fails silently for
users behind corporate firewalls.
wss:// solves this because the TLS tunnel is opaque to
intermediaries. They see an HTTPS connection to port 443 and
pass it through. This is the same reason services like
Ably, Pusher, and PubNub exclusively use
wss:// for client connections — ws:// is simply
unreliable across real-world networks.
TLS Performance
Section titled “TLS Performance”The performance argument against TLS died years ago.
The TLS 1.3 handshake adds one round trip — roughly 1-2ms on modern hardware. After the handshake, per-frame encryption uses AES-GCM with hardware acceleration (AES-NI). The overhead is microseconds per frame, not milliseconds.
For context: a WebSocket frame header is 2-14 bytes. TLS adds roughly 29 bytes of overhead per record (21 bytes for TLS 1.2, fewer for TLS 1.3). On a 100-byte message, that is a 29% size increase. On a 1KB message, it is under 3%. The CPU cost of encrypting either is unmeasurable in a flame graph.
WebSocket connections are long-lived. You pay the handshake cost once, then send thousands of frames over the same connection. The amortized TLS cost per message is effectively zero.
Browser Behavior Differences
Section titled “Browser Behavior Differences”Browsers handle ws:// and wss:// differently in ways that
matter for debugging:
Certificate errors are silent. When an HTTPS page loads an
image from a server with a bad certificate, the browser shows a
warning. When a wss:// connection fails due to a certificate
error, the browser fires a generic onerror event with no
details. The CloseEvent.code is typically 1006 (abnormal
closure) with an empty reason string. You get no indication
that the certificate was the problem.
No certificate override. HTTPS pages with invalid certificates show a “proceed anyway” button. WebSocket connections do not. A bad certificate means no connection, period.
DevTools visibility. Chrome and Firefox show WebSocket
frames in the Network tab, but only if you select the
connection before messages start flowing. There is no
retroactive capture. This applies to both ws:// and
wss://, but with wss:// you also cannot use Wireshark to
inspect traffic as a fallback.
TLS Termination in Production
Section titled “TLS Termination in Production”Most production deployments do not terminate TLS in the application. The standard pattern:
Client (wss://) --> Load Balancer (TLS termination) --> Backend (ws://)Your load balancer or reverse proxy handles the certificate and
encryption. Your application server receives plain ws://
connections on an internal network. This is simpler, performs
better, and centralizes certificate management.
Nginx configuration
Section titled “Nginx configuration”Nginx terminates TLS and proxies ws:// to the backend. The key
lines: listen 443 ssl for the TLS listener, proxy_http_version 1.1 (HTTP/1.0 doesn’t support upgrades), and the Upgrade /
Connection header forwarding. Set proxy_read_timeout to at
least 24 hours — Nginx defaults to 60 seconds and will drop idle
WebSocket connections. See the
Nginx WebSocket configuration guide
for a full production config with SSL, health checks, and
upstream tuning.
Other termination points
Section titled “Other termination points”- AWS ALB — terminates TLS with ACM certificates, forwards WebSocket connections to target groups on port 80
- Cloudflare — terminates TLS at the edge, proxies to your
origin over
ws://orwss:// - HAProxy — TLS termination with WebSocket-aware connection handling and health checks
Certificate Setup
Section titled “Certificate Setup”Production: Let’s Encrypt
Section titled “Production: Let’s Encrypt”Use Let’s Encrypt with Certbot for free, automated TLS certificates. Certificates renew every 90 days. Certbot handles renewal automatically.
# Install certbot and get a certificatesudo certbot --nginx -d example.com
# Verify auto-renewal workssudo certbot renew --dry-runIf you terminate TLS at a cloud load balancer (AWS ALB, Cloudflare), use their built-in certificate management instead. AWS Certificate Manager is free for ALB-attached certificates.
Development: mkcert
Section titled “Development: mkcert”Browsers silently reject WebSocket connections to servers with self-signed certificates. Unlike HTTPS pages, there is no “click to proceed anyway” dialog. The connection fails with a generic error and no explanation.
Use mkcert to create locally-trusted development certificates:
# Install mkcert (macOS)brew install mkcertmkcert -install
# Create certificates for localhostmkcert localhost 127.0.0.1 ::1# Creates localhost+2.pem and localhost+2-key.pemThen use these certificates in your development server. Node.js example:
const https = require('https');const fs = require('fs');const { WebSocketServer } = require('ws');
const server = https.createServer({ cert: fs.readFileSync('localhost+2.pem'), key: fs.readFileSync('localhost+2-key.pem'),});
const wss = new WebSocketServer({ server });wss.on('connection', (ws) => { ws.on('message', (data) => ws.send(data));});
server.listen(8443);Common Mistakes
Section titled “Common Mistakes”Hardcoded ws:// in client code
Section titled “Hardcoded ws:// in client code”This is the most common WebSocket bug. A developer uses
ws:// during development, it works on localhost, and the
hardcoded URL ships to production. It fails on every HTTPS
page.
Match the page protocol instead:
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';const ws = new WebSocket(`${protocol}//${location.host}/ws`);Certificate hostname mismatch
Section titled “Certificate hostname mismatch”Your TLS certificate must match the hostname in the wss://
URL. If your certificate is for example.com but your client
connects to wss://api.example.com, the connection fails
silently. The browser fires onerror with no useful details.
Check with OpenSSL:
openssl s_client -connect api.example.com:443 \ -servername api.example.com < /dev/null 2>&1 \ | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"If the hostname is not in the SAN list, the certificate will not work for WebSocket connections to that hostname.
Missing proxy_http_version in Nginx
Section titled “Missing proxy_http_version in Nginx”Nginx defaults to HTTP/1.0 for upstream connections. HTTP/1.0
does not support the Upgrade mechanism. Your WebSocket
handshake will fail with a 400 Bad Request from the backend.
Always set proxy_http_version 1.1 in your WebSocket location
block.
Frequently Asked Questions
Section titled “Frequently Asked Questions”What is the difference between ws and wss?
Section titled “What is the difference between ws and wss?”ws:// is unencrypted WebSocket on port 80. wss:// is
WebSocket over TLS on port 443. The framing format and
message semantics are identical — the only difference is
whether the TCP connection is wrapped in TLS. Use wss://
for everything except localhost during development.
Do I need SSL for WebSocket?
Section titled “Do I need SSL for WebSocket?”Yes. Beyond encryption, wss:// is required for
compatibility. Browsers block ws:// from HTTPS pages
(mixed content policy), and network intermediaries regularly
interfere with unencrypted WebSocket traffic. The only
environment where ws:// works reliably is localhost.
Does wss add latency?
Section titled “Does wss add latency?”The TLS 1.3 handshake adds one round trip — roughly 1-2ms on modern hardware. After that, per-frame encryption adds microseconds with hardware-accelerated AES. WebSocket connections are long-lived, so the handshake cost is paid once and amortized across thousands of messages. TLS latency will not appear in your performance budget.
Can browsers connect to ws:// from an HTTPS page?
Section titled “Can browsers connect to ws:// from an HTTPS page?”No. Every modern browser enforces mixed content blocking.
An HTTPS page cannot open a ws:// connection. Chrome,
Firefox, Safari, and Edge all block it silently — no
prompt, no override, no workaround. The only fix is to use
wss://. This has been enforced since Chrome 61 (2017) and
is now universal.
How do I set up TLS for WebSocket in production?
Section titled “How do I set up TLS for WebSocket in production?”Terminate TLS at your load balancer or reverse proxy, not
in your application. Use Let’s Encrypt with Certbot for free
certificates, or use your cloud provider’s certificate
management (AWS ACM, Cloudflare). Proxy plain ws:// to your
backend over the internal network. See the
Nginx configuration above for a
working example.
Related Content
Section titled “Related Content”- WebSocket API Reference — browser API for creating and managing WebSocket connections
- WebSocket Port Numbers — why WebSocket uses ports 80 and 443, and why custom ports fail in production
- The Road to WebSockets — how WebSocket fits into the evolution of realtime web protocols
- Nginx WebSocket Proxy — how to configure TLS termination and WebSocket proxying in Nginx
- WebSocket Protocol Deep Dive — the framing, handshake, and protocol internals behind ws and wss