Skip to content

wss vs ws: Secure WebSocket vs Unencrypted Explained

ws://wss://
EncryptionNoneTLS (same as HTTPS)
Default port80443
Works on HTTPS pagesNo (blocked by mixed content)Yes
Passes through proxiesOften stripped or blockedYes (encrypted tunnel)
Production useLocal development onlyRequired

Encryption is reason enough, but two practical issues make ws:// unusable in production even if you don’t care about security.

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 loading HTTP images on HTTPS pages. The connection silently fails. No error dialog, no user prompt, just a failed WebSocket in the console.

Mixed Content: The page was loaded over HTTPS, but attempted
to connect to the insecure WebSocket endpoint 'ws://...'.
This request has been blocked.

There is no workaround. If your page is HTTPS, your WebSocket must be wss://.

Corporate proxies, transparent proxies, and some 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 fine on your home network and fails silently for a subset of 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.

The TLS handshake adds roughly 1-2ms of latency on modern hardware. After the handshake, per-frame encryption overhead is negligible — AES-GCM on modern CPUs with hardware acceleration adds microseconds per frame, not milliseconds.

The performance argument for ws:// made sense in 2010. It doesn’t in 2026.

Most production deployments don’t terminate TLS at the application level. The typical pattern:

Client (wss://) --> Load Balancer/Proxy (TLS termination)
--> Backend (ws://)

The load balancer or reverse proxy handles the TLS certificate and encryption. Your application server receives plain ws:// connections on an internal network. This is simpler, faster, and lets you manage certificates in one place.

Common TLS termination points:

  • Nginx — handles TLS and proxies ws:// to your backend using proxy_pass with Upgrade headers
  • Cloudflare — terminates TLS at the edge, proxies to your origin over ws:// or wss://
  • AWS ALB — terminates TLS with ACM certificates, forwards WebSocket connections to target groups
  • HAProxy — TLS termination with WebSocket-aware connection handling

Browsers silently reject WebSocket connections to servers with self-signed or invalid TLS certificates. Unlike HTTPS, there is no “click to proceed anyway” dialog. The connection just fails with a generic error.

For development, use mkcert to create locally-trusted certificates. For production, use Let’s Encrypt (free, automated) or any real certificate authority.

Hardcoding ws:// in client code is the most common mistake. Match the page protocol instead:

// Bad: hardcoded ws:// will fail on HTTPS pages
const ws = new WebSocket('ws://example.com/ws');
// Good: match the page protocol
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${window.location.host}/ws`);

What is the difference between ws and wss?

Section titled “What is the difference between ws and wss?”

ws:// is unencrypted WebSocket. wss:// is WebSocket over TLS — identical framing and message format, different transport security. The relationship mirrors HTTP vs HTTPS. Use wss:// for everything except localhost during development.

Yes. Beyond encryption, wss:// is required for compatibility. Browsers block ws:// from HTTPS pages, and network intermediaries regularly interfere with unencrypted WebSocket connections. The only place ws:// works reliably is localhost.

The TLS handshake adds 1-2ms on modern hardware. Per-frame encryption overhead is in the microsecond range with hardware-accelerated AES. TLS latency is not a factor in your performance budget.