It starts as HTTP
A WebSocket connection begins life as an HTTP request.
Your client sends a GET carrying three headers that matter: one saying it wants to upgrade, one naming websocket as the target, and one carrying a random key. A server that agrees answers with a 101 and a value derived from that key.
Understand why that odd little hash exists. It proves your server actually speaks WebSocket, rather than being a cache blindly replaying a stored response.
After the exchange, HTTP is over. The same connection now carries frames in both directions until somebody closes it.
Get everything in between to cooperate with the upgrade, because it will not happen by itself. The major balancers and proxies support it, and Nginx needs the two headers explicitly forwarded in its config, a detail that has burned nearly everyone once.
Fear the older corporate proxies most. Some strip the upgrade header from plain traffic, and some kill anything that stops looking like request and response.
Run it over TLS on port 443 and most of that goes away, because the proxy sees only ciphertext and passes it along like any other secure session. Running it unencrypted in production is a support queue waiting to happen.
Authentication, once, for hours
Authenticate at the handshake, where browsers make your life awkward. The WebSocket constructor cannot set custom headers, so you have no authorization header to use.
Choose between cookies, which flow automatically and need you to think about cross-site requests since WebSocket ignores the usual rules, a short-lived token in the query string kept out of your access logs, or an auth exchange as the first message after connecting.
Whichever you pick, there is exactly one moment of authentication for a connection that might live for hours. Plan for a session revoked halfway through, which usually means your server closing the connection with a specific code.
Worked example
Sana's fintech launches a live portfolio view over ws:// on port 80 in the first beta, and 8 percent of users, mostly on corporate networks, never connect: their proxies strip the Upgrade header and the handshake gets back a 200 with an HTML error page instead of a 101. Moving to wss:// on 443 drops the failure rate below 1 percent, because the proxies now pass opaque TLS. The stragglers turn out to be one client whose middlebox kills any connection idle for 30 seconds, solved with ping frames every 20. Her other find came from the load balancer: Nginx returned plain 200s to upgrade requests until the proxy_set_header Upgrade and Connection lines went in, a two-line fix that took a day to spot because it looked exactly like a client bug.