WebSocket

technology

WebSocket

Definition

WebSocket is a communication protocol standardized as RFC 6455 that provides full-duplex, persistent, bidirectional message exchange between a client and server over a single TCP connection. Unlike HTTP's request-response model where each interaction requires a new connection (or reuses one through keep-alive with strict request-then-response ordering), WebSocket establishes a long-lived connection through which either party can send messages at any time, independently and simultaneously. This architectural shift from pull-based polling to push-based messaging enables real-time applications -- live dashboards, collaborative editors, chat systems, streaming data feeds, and interactive gaming -- that would be impractical or inefficient with traditional HTTP.

The WebSocket protocol was designed to be complementary to HTTP rather than a replacement. It reuses HTTP's infrastructure (ports 80 and 443, proxy traversal, TLS support) by starting every WebSocket connection as a standard HTTP request that is then "upgraded" to the WebSocket protocol through a handshake mechanism. This design ensures compatibility with existing web infrastructure -- firewalls, load balancers, and CDNs that understand HTTP can typically pass through WebSocket upgrade requests without modification. Once upgraded, the connection operates over raw TCP with minimal framing overhead (as little as 2 bytes per frame), providing dramatically lower latency than HTTP polling or long-polling approaches.

The protocol supports both text and binary message types, enabling applications to transmit structured data (JSON, MessagePack, Protocol Buffers) alongside raw binary payloads (images, audio, video) over the same connection. Built-in ping/pong frames provide connection health monitoring, and close frames enable graceful connection teardown with status codes and reason phrases.

In the Elixir ecosystem, WebSocket is the foundational transport layer for Phoenix Channels and LiveView, enabling the real-time features that distinguish Phoenix applications from traditional request-response web frameworks.

Handshake Process

Every WebSocket connection begins with an HTTP upgrade handshake that transitions the connection from HTTP to the WebSocket protocol:

Client Request:
GET /socket/websocket HTTP/1.1
Host: prismatic.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: phoenix

Server Response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: phoenix
StepDescriptionProtocol
1. HTTP RequestClient sends GET with Upgrade: websocket headerHTTP/1.1
2. Server ValidationServer verifies Sec-WebSocket-Key and capabilitiesHTTP/1.1
3. 101 ResponseServer responds with 101 Switching ProtocolsHTTP/1.1
4. Protocol SwitchConnection upgrades from HTTP to WebSocketWebSocket
5. BidirectionalBoth parties can send messages independentlyWebSocket

The Sec-WebSocket-Key / Sec-WebSocket-Accept exchange prevents caching proxies from mistaking WebSocket traffic for cacheable HTTP responses. The key is a random base64 value, and the accept is its SHA-1 hash concatenated with a magic GUID (258EAFA5-E914-47DA-95CA-C5AB0DC85B11), proving the server understands the WebSocket protocol.

Subprotocol Negotiation

WebSocket supports subprotocol negotiation through the Sec-WebSocket-Protocol header. Phoenix uses this to establish the Phoenix-specific messaging format over the raw WebSocket connection:

SubprotocolDescriptionUsed By
phoenixPhoenix Channel protocol (JSON-encoded)Phoenix Channels, LiveView
graphql-wsGraphQL over WebSocketAbsinthe Subscriptions
mqttMQTT over WebSocketIoT device communication
stompSimple Text Oriented MessagingMessage brokers

Frame Format

WebSocket messages are transmitted as frames with a lightweight binary header:

FieldSizeDescription
FIN1 bitFinal fragment flag (1 = last frame of message)
RSV1-33 bitsReserved for extensions (compression)
Opcode4 bitsFrame type (text, binary, close, ping, pong)
MASK1 bitMasking flag (required for client-to-server)
Payload Length7/16/64 bitsMessage size (7-bit, 16-bit extended, or 64-bit extended)
Masking Key0 or 32 bitsXOR mask for payload (client-to-server only)
PayloadVariableApplication data

Frame opcodes define the message type:

OpcodeTypePurpose
0x0ContinuationFragment of a multi-frame message
0x1TextUTF-8 encoded text data
0x2BinaryRaw binary data
0x8CloseConnection close with status code
0x9PingConnection health check (client or server)
0xAPongResponse to ping

The minimal frame overhead (2-14 bytes depending on payload size and masking) is a significant advantage over HTTP, where headers alone typically consume 200-800 bytes per request. For applications sending many small messages (real-time updates, telemetry, keystrokes), this overhead reduction translates directly into higher throughput and lower latency.

Close Status Codes

CodeMeaningDescription
1000NormalClean close initiated by either party
1001Going AwayServer shutting down or client navigating away
1002Protocol ErrorMalformed frame or protocol violation
1003UnsupportedReceived data type the endpoint cannot process
1006AbnormalNo close frame received (connection dropped)
1008Policy ViolationMessage violates server policy
1009Too LargeMessage exceeds size limit
1011Internal ErrorServer encountered an unexpected condition
4001Auth ExpiredCustom: authentication token expired (Phoenix)

Connection Lifecycle Management

Production WebSocket deployments require robust lifecycle management to handle network instability, server restarts, and client disconnections:

defmodule PrismaticWeb.UserSocket do
  @moduledoc """
  WebSocket handler for authenticated real-time connections.
  Manages channel multiplexing, authentication verification,
  and connection lifecycle for all real-time features.
  """

  use Phoenix.Socket

  # Channel topic routing
  channel "security:*", PrismaticWeb.SecurityChannel
  channel "assets:*", PrismaticWeb.AssetChannel
  channel "quality:*", PrismaticWeb.QualityChannel
  channel "perimeter:*", PrismaticWeb.PerimeterChannel

  @impl true
  @spec connect(map(), Phoenix.Socket.t(), map()) :: {:ok, Phoenix.Socket.t()} | :error
  def connect(%{"token" => token}, socket, _connect_info) do
    case Phoenix.Token.verify(PrismaticWeb.Endpoint, "user", token, max_age: 86_400) do
      {:ok, user_id} ->
        {:ok, assign(socket, :user_id, user_id)}
      {:error, _reason} ->
        :error
    end
  end

  def connect(_params, _socket, _connect_info), do: :error

  @impl true
  @spec id(Phoenix.Socket.t()) :: String.t()
  def id(socket), do: "user_socket:#{socket.assigns.user_id}"
end
Lifecycle EventPhoenix HandlingClient Recovery
Connection LossProcess terminates, resources freedAutomatic reconnection with backoff
Server RestartGraceful close frames sent to clientsClient detects close, reconnects
Heartbeat TimeoutServer closes inactive connectionsClient sends periodic heartbeats
Authentication ExpiryServer closes with 4001 statusClient refreshes token, reconnects
Topic LeaveChannel process terminatesClient can rejoin topic
DeploymentGraceful drain during blue-green switchClient reconnects to new environment

Phoenix's WebSocket transport includes built-in heartbeat management (default 30-second intervals), automatic reconnection with exponential backoff in the JavaScript client, and topic rejoin on reconnection. This means temporary network disruptions are handled transparently without application-level reconnection logic.

Phoenix Channel Implementation

Phoenix provides a high-level abstraction over raw WebSocket connections through Channels, which multiplex multiple topic subscriptions over a single WebSocket connection:

defmodule PrismaticWeb.SecurityChannel do
  @moduledoc """
  Channel for real-time security intelligence delivery.
  Handles subscription to security alerts, rating changes,
  and compliance updates within the Perimeter bounded context.
  """

  use PrismaticWeb, :channel

  @impl true
  @spec join(String.t(), map(), Phoenix.Socket.t()) ::
          {:ok, Phoenix.Socket.t()} | {:error, map()}
  def join("security:" <> domain, _params, socket) do
    if authorized?(socket.assigns.user_id, domain) do
      send(self(), :after_join)
      {:ok, assign(socket, :domain, domain)}
    else
      {:error, %{reason: "unauthorized"}}
    end
  end

  @impl true
  def handle_info(:after_join, socket) do
    # Push current state to newly joined client
    case PrismaticPerimeter.security_rating(socket.assigns.domain) do
      {:ok, rating} ->
        push(socket, "rating:current", %{
          grade: rating.grade,
          score: rating.score,
          measured_at: rating.measured_at
        })
      {:error, _} ->
        :ok
    end
    {:noreply, socket}
  end

  @impl true
  def handle_in("scan:request", %{"type" => scan_type}, socket) do
    case PrismaticPerimeter.request_scan(socket.assigns.domain, scan_type) do
      {:ok, scan_id} ->
        {:reply, {:ok, %{scan_id: scan_id}}, socket}
      {:error, reason} ->
        {:reply, {:error, %{reason: reason}}, socket}
    end
  end

  defp authorized?(_user_id, _domain), do: true
end

WebSocket vs Alternatives

TechnologyDirectionLatencyComplexityConnectionUse Case
WebSocketBidirectionalLowestMediumPersistentReal-time interactive apps
SSEServer-to-client onlyLowLowPersistentLive feeds, notifications
HTTP PollingClient-initiatedHigh (interval)LowPer-requestSimple status checks
HTTP Long-PollingServer-heldMediumMediumHeld openFallback for WebSocket
gRPC StreamingBidirectionalLowHighPersistentMicroservice communication
HTTP/2 Server PushServer-initiatedLowMediumMultiplexedAsset preloading

Server-Sent Events (SSE) are a simpler alternative when only server-to-client push is needed, using standard HTTP with automatic reconnection and event IDs. WebSocket is necessary when the client also needs to send messages to the server (interactive dashboards, collaborative editing, bidirectional command channels).

Security Considerations

ConcernMitigationImplementation
AuthenticationValidate tokens during handshake (connect/3 callback)Phoenix.Token.verify/4
AuthorizationCheck permissions per-channel in join/3 callbackRBAC checks per topic
Origin ValidationVerify Origin header to prevent cross-site hijackingEndpoint configuration
TLSUse wss:// (WebSocket Secure) for encryptionMandatory in production
Rate LimitingApply rate limits per-connection and per-messageCustom Channel middleware
Message SizeConfigure maximum frame size to prevent memory exhaustionCowboy transport options
CSRFWebSocket handshake is not subject to CORS; use token-based authToken in connect params
Connection LimitsCap maximum concurrent connections per userSocket connect/3 callback

Rate Limiting WebSocket Messages

defmodule PrismaticWeb.RateLimitedChannel do
  @moduledoc """
  Channel with per-user rate limiting on incoming messages.
  Prevents individual clients from overwhelming the server
  with excessive requests through the WebSocket connection.
  """

  use PrismaticWeb, :channel

  @max_messages_per_second 10
  @window_ms 1_000

  @impl true
  def handle_in(event, payload, socket) do
    user_id = socket.assigns.user_id
    now = System.monotonic_time(:millisecond)

    window_start = now - @window_ms
    messages = socket.assigns[:rate_limit_messages] || []
    recent = Enum.filter(messages, fn t -> t > window_start end)

    if length(recent) >= @max_messages_per_second do
      {:reply, {:error, %{reason: "rate_limited"}}, socket}
    else
      socket = assign(socket, :rate_limit_messages, [now | recent])
      dispatch_message(event, payload, socket)
    end
  end

  defp dispatch_message("scan:request", payload, socket) do
    # Handle scan request
    {:reply, {:ok, %{status: "queued"}}, socket}
  end

  defp dispatch_message(_event, _payload, socket) do
    {:noreply, socket}
  end
end

Context in Prismatic

The Prismatic Platform leverages WebSockets extensively through Phoenix Channels and LiveView, making WebSocket the foundational transport for all real-time features.

Phoenix Channel Transport: Phoenix manages WebSocket connections as the primary transport for its Channel abstraction. Each WebSocket connection can multiplex multiple topic subscriptions, allowing a single connection to simultaneously receive security alerts, asset discovery updates, and quality metric changes. The Phoenix JavaScript client handles connection lifecycle (heartbeats, reconnection, topic rejoin) automatically.

LiveView Real-Time UI: LiveView dashboards at /perimeter, /perimeter/assets, /perimeter/compliance, and /perimeter/easm use WebSocket connections to push server-rendered HTML diffs to the browser in real-time. When a security rating changes, an asset is discovered, or a compliance score updates, the server pushes only the changed DOM fragments over the existing WebSocket connection, providing real-time updates without page reloads or client-side state management.

Distributed Broadcasting: WebSocket connections on individual nodes receive events through PubSub, which propagates messages across cluster nodes. A security alert generated on one node reaches all connected WebSocket clients across all nodes through PubSub-backed broadcasting.

Connection Architecture:

Browser --- wss:// ---> Phoenix Endpoint
                          |
                    WebSocket Transport (Cowboy/Bandit)
                          |
                    Socket Handler (auth + assign)
                          |
                  +-------+-------+-------+
                  |       |       |       |
            Channel A  Channel B  Channel C  Channel D
            (security) (assets)  (quality)  (perimeter)
                  |       |       |       |
                PubSub (distributed across cluster nodes)
                  |
            +-----+-----+
            |     |     |
          Node1  Node2  Node3

Performance Characteristics:

MetricValueNotes
Connections per node100,000+Limited by BEAM process count and memory
Message latency<1ms (local), <5ms (cross-node)Via PubSub distribution
Heartbeat interval30 secondsConfigurable per socket
Reconnect backoff1s, 2s, 5s, 10s (max)Exponential with cap
Frame overhead2-14 bytesMinimal vs HTTP's 200-800 bytes
Memory per connection~10-50 KBDepends on assigns and channel count

WebSocket Extensions

Per-Message Compression (RFC 7692)

The permessage-deflate extension compresses WebSocket messages using zlib, reducing bandwidth for text-heavy payloads:

Content TypeUncompressedCompressedSavings
JSON (small)200 bytes150 bytes25%
JSON (large)10 KB2 KB80%
HTML diff5 KB500 bytes90%
Binary dataVariableVariable10-50%

Phoenix enables compression through Cowboy transport configuration:

# config/config.exs
config :prismatic_web, PrismaticWeb.Endpoint,
  http: [
    port: 4000,
    transport_options: [
      socket_opts: [
        # Enable WebSocket compression
        compress: true
      ]
    ]
  ]

Testing WebSocket Connections

defmodule PrismaticWeb.SecurityChannelTest do
  use PrismaticWeb.ChannelCase

  setup do
    user = create_test_user()
    token = Phoenix.Token.sign(PrismaticWeb.Endpoint, "user", user.id)
    {:ok, socket} = connect(PrismaticWeb.UserSocket, %{"token" => token})
    {:ok, socket: socket, user: user}
  end

  describe "join/3" do
    test "joins security channel for authorized domain", %{socket: socket} do
      {:ok, _reply, _socket} = subscribe_and_join(socket, "security:example.com")
    end

    test "rejects unauthorized domain", %{socket: socket} do
      {:error, %{reason: "unauthorized"}} =
        subscribe_and_join(socket, "security:restricted.com")
    end
  end

  describe "handle_in/3" do
    test "processes scan request", %{socket: socket} do
      {:ok, _, socket} = subscribe_and_join(socket, "security:example.com")
      ref = push(socket, "scan:request", %{"type" => "dns"})
      assert_reply ref, :ok, %{scan_id: _}
    end
  end
end
  • Phoenix - Web framework managing WebSocket connections and transport layer
  • LiveView - Server-rendered UI using WebSocket for real-time DOM updates
  • Channel - Phoenix abstraction for multiplexed topic-based communication over WebSocket
  • PubSub - Distributed message broadcasting powering WebSocket event delivery
  • Server-Sent Events - Simpler unidirectional alternative to WebSocket
  • TLS - Encryption layer for secure WebSocket connections (wss://)
  • Rate Limiting - Traffic control applied to WebSocket messages
  • Cluster - Multi-node deployment with distributed WebSocket broadcasting
  • GraphQL - API layer using WebSocket for subscription delivery
  • Blue-Green Deployment - Deployment strategy requiring graceful WebSocket connection handling

See Also

  • Architecture - Platform real-time communication architecture
  • Technologies - Communication technology stack
  • Apps - Applications using WebSocket for real-time features

Connect & Contribute

Created by Tomas Korcak (korczis) | Open Source under GHL