![Demián Renzulli](https://web.dev/images/authors/demianrenzulli.jpg) Demián Renzulli [X](https://twitter.com/drenzulli) [GitHub](https://github.com/demianrenzulli) ![Andrew Rayskiy](https://web.dev/images/authors/greengrape.png) Andrew Rayskiy [LinkedIn](https://www.linkedin.com/in/andrew-rayskiy-6a55b7154) ![Vlad Krot](https://web.dev/images/authors/vkrot.jpg) Vlad Krot [LinkedIn](https://www.linkedin.com/in/vkrot)

<br />

Standard web applications are typically restricted to specific communication
protocols like HTTP and APIs like
[WebSocket](https://developer.mozilla.org/docs/Web/API/WebSockets_API) and
[WebRTC](https://web.dev/articles/webrtc-basics). While these are powerful, they
are designed to be tightly constrained to prevent abuse. They cannot establish
raw TCP or UDP connections, which limits the ability of web apps to communicate
with legacy systems or hardware devices that use their own non-web protocols.
For example, you might want to build a web-based SSH client, connect to a local
printer, or manage a fleet of IoT devices. Historically, this required browser
plugins or native helper applications.

The Direct Sockets API addresses this limitation by enabling [Isolated Web Apps
(IWAs)](https://chromeos.dev/en/web/isolated-web-apps) to establish direct TCP
and UDP connections without a relay server. With IWAs, thanks to additional
security measures---such as strict Content Security Policy (CSP) and cross-origin
isolation--- this API can be safely exposed.

## Use cases

When should you use Direct Sockets over standard WebSockets?

- **IoT and smart devices:** Communicating with hardware that uses raw TCP/UDP rather than HTTP.
- **Legacy systems:** Connecting to older mail servers (SMTP/IMAP), IRC chat servers, or printers.
- **Remote desktop and terminals:** Implementing SSH, Telnet, or RDP clients.
- **P2P systems:** Implementing Distributed Hash Tables (DHT) or resilient collaboration tools (like IPFS).
- **Media broadcasting:** Leveraging UDP to stream content to multiple endpoints at once (multicasting), enabling use cases like coordinated video playback across a network of retail kiosks.
- **Server and listener capabilities:** Configuring the IWA to act as a receiving endpoint for incoming TCP connections or UDP datagrams using `TCPServerSocket` or bound `UDPSocket`.

## Prerequisites for Direct Sockets

Before using Direct Sockets, you'll need to [set up a functional
IWA](https://chromeos.dev/en/web/isolated-web-apps). You can then integrate
Direct Sockets into your pages.

## Add permission policy

To use Direct Sockets, you must configure the `permissions_policy` object in
your IWA manifest. You need to add the `direct-sockets` key to explicitly enable
the API. Additionally, you must include the `cross-origin-isolated` key. This
key is not specific to Direct Sockets, but is required for all IWAs and
determines whether the document can access APIs that require cross-origin
isolation.

    {
      "permissions_policy": {
        "direct-sockets": ["self"],
        "cross-origin-isolated": ["self"]
      }
    }

The direct-sockets key determines whether calls to `new TCPSocket(...)`, `new
TCPServerSocket(...)` or `new UDPSocket(...)` are allowed. If this policy is not
set, these constructors will immediately reject with a `NotAllowedError`.

## Implement TCPSocket

Applications can request a TCP connection by creating a `TCPSocket` instance.

### Open a connection

To open a connection, use the `new` operator and `await` the opened promise.

The `TCPSocket` constructor initiates the connection using the specified
`remoteAddress` and `remotePort`.

    const remoteAddress = 'example.com';
    const remotePort = 7;

    // Configure options like keepAlive or buffering
    const options = {
      keepAlive: true,
      keepAliveDelay: 720000
    };

    let tcpSocket = new TCPSocket(remoteAddress, remotePort, options);

    // Wait for the connection to be established
    let { readable, writable } = await tcpSocket.opened;

The optional configuration object allows for fine-grained network control; in
this specific case, `keepAliveDelay` is set to 720000 milliseconds to maintain
the connection during periods of inactivity. Developers can also configure other
properties here, such as `noDelay`, which disables Nagle's algorithm to stop the
system from batching small packets---potentially reducing latency---or
`sendBufferSize` and `receiveBufferSize` to manage throughput.

In the last part of the preceding snippet, the code awaits the opened promise,
which only resolves once the handshake is complete, returning a
`TCPSocketOpenInfo` object containing the readable and writable streams required
for data transmission.

### Read and write

Once the socket is open, interact with it using standard [Streams
API](https://developer.mozilla.org/docs/Web/API/Streams_API) interfaces.

- **Writing:** The writable stream accepts a `BufferSource` (like an `ArrayBuffer`).
- **Reading:** The readable stream yields `Uint8Array` data.

    // Writing data
    const writer = writable.getWriter();
    const encoder = new TextEncoder();
    await writer.write(encoder.encode("Hello Server"));

    // Call when done
    writer.releaseLock();

    // Reading data
    const reader = readable.getReader();
    const { value, done } = await reader.read();
    if (!done) {
        const decoder = new TextDecoder();
        console.log("Received:", decoder.decode(value));
    }

    // Call when done
    reader.releaseLock();

### Optimized reading with BYOB

For high-performance applications where managing memory allocation is critical,
the API supports "Bring Your Own Buffer" (BYOB) reading. Instead of letting the
browser allocate a new buffer for every chunk of data received, you can pass a
pre-allocated buffer to the reader. This reduces garbage collection overhead by
writing data directly into your existing memory.

    // 1. Get a BYOB reader explicitly
    const reader = readable.getReader({ mode: 'byob' });

    // 2. Allocate a reusable buffer (e.g., 4KB)
    let buffer = new Uint8Array(4096);

    // 3. Read directly into the existing buffer
    const { value, done } = await reader.read(buffer);

    if (!done) {
      // 'value' is a view of the data written directly into your buffer
      console.log("Bytes received:", value.byteLength);
    }

    reader.releaseLock();

## Implement UDPSocket

The `UDPSocket` class allows for UDP communication. It operates in two distinct
modes depending on how you configure the options.

### Connected mode

In this mode, the socket communicates with a single specific destination. This
is useful for standard client-server tasks.

    // Connect to a specific remote host
    let udpSocket = new UDPSocket({
        remoteAddress: 'example.com',
        remotePort: 7 });

    let { readable, writable } = await udpSocket.opened;

### Bound mode

In this mode, the socket is bound to a local IP endpoint. It can receive
datagrams from arbitrary sources and send them to arbitrary destinations. This
is often used for local discovery protocols or server-like behavior.

    // Bind to all interfaces (IPv6)
    let udpSocket = new UDPSocket({
        localAddress: '::'
        // omitting localPort lets the OS pick one
    });

    // localPort will tell you the OS-selected port.
    let { readable, writable, localPort } = await udpSocket.opened;

### Handle UDP messages

Unlike the TCP stream of bytes, UDP streams deal in `UDPMessage` objects, which
contain the data and the remote address info. The following code demonstrates
how to handle Input/Output operations when using a `UDPSocket` in "bound mode."

    // Writing (Bound Mode requires specifying destination)
    const writer = writable.getWriter();
    await writer.write({
        data: new TextEncoder().encode("Ping"),
        remoteAddress: '192.168.1.50',
        remotePort: 8080
    });

    // Reading
    const reader = readable.getReader();
    const { value } = await reader.read();
    // value contains: { data, remoteAddress, remotePort }
    console.log(`Received from ${value.remoteAddress}:`, value.data);

Unlike "connected mode," where the socket is locked to a specific peer, bound
mode allows the socket to communicate with arbitrary destinations. Consequently,
when writing data to the writable stream, you must pass a `UDPMessage` object
that explicitly specifies the `remoteAddress` and `remotePort` for each packet,
instructing the socket exactly where to route that specific datagram. Similarly,
when reading from the readable stream, the returned value includes not just the
data payload but also the sender's `remoteAddress` and `remotePort`, enabling
your application to identify the origin of every incoming packet.

**Note:** When using `UDPSocket` in "connected mode," the socket is effectively
locked to a specific peer, simplifying the I/O process. In this mode, the
`remoteAddress` and `remotePort` properties are effectively no-ops when writing,
as the destination is already fixed. Similarly, when reading messages, these
properties will return null, since the source is guaranteed to be the connected
peer.

## Multicast support

For use cases like synchronizing video playback across multiple kiosks or
implementing local device discovery (for example, mDNS), Direct Sockets supports
Multicast UDP. This allows messages to be sent to a "group" address and received
by all subscribers on the network, rather than a single specific peer.

### Multicast permissions

To use multicast capabilities, you must add the specific
`direct-sockets-multicast` permission to your IWA manifest. This is distinct
from the standard direct-sockets permission and it's necessary because multicast
is used only in private networks.

    {
      "permissions_policy": {
        "direct-sockets": ["self"],
        "direct-sockets-multicast": ["self"],
        "direct-sockets-private": ["self"],
        "cross-origin-isolated": ["self"]
      }
    }

### Send multicast datagrams

Sending to a multicast group is very similar to standard UDP "connected mode,"
with the addition of specific options to control packet behavior.

    const MULTICAST_GROUP = '239.0.0.1';
    const PORT = 12345;

    const socket = new UDPSocket({
      remoteAddress: MULTICAST_GROUP,
      remotePort: PORT,
      // Time To Live: How many router hops the packet can survive (default: 1)
      multicastTimeToLive: 5,
      // Loopback: Whether to receive your own packets (default: true)
      multicastLoopback: true
    });

    const { writable } = await socket.opened;
    // Write to the stream as usual...

### Receive multicast datagrams

To receive multicast traffic, you must open a `UDPSocket` in "bound mode"
(typically binding to `0.0.0.0` or `::`) and then join a specific group using
the `MulticastController`. You may also use the `multicastAllowAddressSharing`
option (similar to `SO_REUSEADDR` on Unix), which is essential for device
discovery protocols where multiple applications on the same device need to
listen to the same port.

    const socket = new UDPSocket({
      localAddress: '0.0.0.0', // Listen on all interfaces
      localPort: 12345,
      multicastAllowAddressSharing: true // Allow multiple applications to bind to the same address / port pair.
    });

    // The open info contains the MulticastController
    const { readable, multicastController } = await socket.opened;

    // Join the group to start receiving packets
    await multicastController.joinGroup('239.0.0.1');

    const reader = readable.getReader();

    // Read the stream...
    const { value } = await reader.read();
    console.log(`Received multicast from ${value.remoteAddress}`);

    // When finished, you can leave the group (this is an optional, but recommended practice)
    await multicastController.leaveGroup('239.0.0.1');

## Create a server

The API also supports TCPServerSocket for accepting incoming TCP connections,
effectively allowing your IWA to act as a local server. The following code
illustrates how to establish a TCP server using the `TCPServerSocket` interface.

    // Listen on all interfaces (IPv6)
    let tcpServerSocket = new TCPServerSocket('::');

    // Accept connections via the readable stream
    let { readable } = await tcpServerSocket.opened;
    let reader = readable.getReader();

    // Wait for a client to connect
    let { value: clientSocket } = await reader.read();

    // 'clientSocket' is a standard TCPSocket you can now read/write to

By instantiating the class with the `'::'` address, the server binds to all
available IPv6 network interfaces to listen for incoming attempts. Unlike
traditional callback-based server APIs, this API utilizes the web's Streams API
pattern: incoming connections are delivered as a `ReadableStream`. When you call
`reader.read()`, the application waits for and accepts the next connection from
the queue, resolving to a value that is a fully functional `TCPSocket` instance
ready for two-way communication with that specific client.

## Debug Direct Sockets with Chrome DevTools

From Chrome 138, you can debug Direct Sockets traffic directly within the
**Network** panel in Chrome DevTools, eliminating the need for external packet
sniffers. This tooling lets you monitor `TCPSocket` connections as well as
`UDPSocket` traffic (in both bound and connected modes) alongside your standard
HTTP requests.

To inspect your app's network activity:

1. Open the **Network** panel in Chrome DevTools.
2. Locate and select the socket connection in the request table.
3. Open the **Messages** tab to view a log of all transmitted and received data.

![The data in the Messages tab in DevTools.](https://developer.chrome.com/static/docs/iwa/direct-sockets/image/devtools.png)

This view provides a Hex Viewer, allowing you to inspect the raw binary payload
of your TCP and UDP messages, ensuring your protocol implementation is
byte-perfect.

## Demo

[IWA Kitchen Sink](https://github.com/chromeos/iwa-sink) features an app with
multiple tabs, each demonstrating a different IWA API such as Direct Sockets,
[Controlled Frame](https://developer.chrome.com/docs/iwa/controlled-frame) and more.

Alternatively, [the telnet client
demo](https://github.com/GoogleChromeLabs/telnet-client) contains an Isolated
Web App that allows the user to connect to a TCP/IP server through an
interactive terminal. In other words, a Telnet client.

## Conclusion

The **Direct Sockets API** closes a critical functionality gap by enabling web
applications to handle raw network protocols that were previously impossible to
support without native wrappers. It goes beyond simple client connectivity; with
`TCPServerSocket`, applications can listen for incoming connections, while
`UDPSocket` offers flexible modes for both peer-to-peer communication and local
network discovery.

By exposing these raw TCP and UDP capabilities through the modern Streams API,
you can now build full-featured implementations of legacy protocols---like SSH,
RDP, or custom IoT standards---directly in JavaScript. Because this API grants
low-level network access, it carries significant security implications.
Therefore, it is restricted to **Isolated Web Apps (IWAs)**, ensuring that such
power is only granted to trusted, explicitly installed applications that enforce
strict security policies. This balance lets you build powerful, device-centric
applications while maintaining the safety users expect from the web platform.

## Resources

- [Direct Sockets Explainer](https://github.com/WICG/direct-sockets/blob/main/docs/explainer.md)
- [Multicast Explainer](https://github.com/WICG/direct-sockets/blob/main/docs/multicast-explainer.md)
- [Spec](https://wicg.github.io/direct-sockets/)
- [ChromeStatus Entry](https://chromestatus.com/feature/6398297361088512)