runtime:net

TCP sockets. connect() follows the WinterTC Sockets API; listen() yields inbound connections. Bytes move over web streams. bind() is UDP — messages, not streams. resolveDns() looks up DNS records.

Capability: Net (connect, send, resolveDns) / NetListen (listen, bind)

Exposed as an ES module under the runtime: scheme. Status: Available.

All I/O is async

All I/O is async over web streams — nothing blocks the event loop. Closing a socket's writable half-closes (sends FIN) while reads continue.

Import

JavaScript
import { connect, listen, bind, resolveDns } from "runtime:net";

Functions

FunctionTypeDescriptionExample
connect(address, options?)(Address, { secureTransport?, sni?, alpn?, ca?, cert?, key?, allowHalfOpen? }) => SocketOpen an outbound TCP or TLS connection (the WinterTC Sockets API). Returns a Socket synchronously; .opened settles once connected. address is "host:port" or { hostname, port }. secureTransport: "on" negotiates TLS, "starttls" opens plaintext for a later startTls(); sni overrides the server name (default: the host); alpn offers protocols (the negotiated one is SocketInfo.alpn); ca is a PEM certificate authority trusted for this connection in addition to the process store (--tls-ca); cert + key are a client certificate to present; allowHalfOpen keeps writing after the peer's FIN.const sock = connect({ hostname: "db.internal", port: 5432 })
listen(options)({ hostname?, port, secureTransport?, cert?, key?, alpn?, requestCert?, ca?, crl?, reusePort? }) => ListenerBind a listening socket. hostname defaults to 0.0.0.0 (all interfaces — pass 127.0.0.1 for a loopback-only server; a locked-down host may refuse a wildcard bind). port 0 picks an ephemeral port (read it from listener.addr). Returns an async-iterable Listener of inbound Sockets. secureTransport: "on" terminates TLS on each accept — pass a PEM cert + key (and optional alpn); the cert/key are inline, so server TLS needs no capability beyond NetListen. requestCert: true requires a client certificate that chains to ca, checked against crl (see Client certificates). reusePort shares the port across processes (SO_REUSEPORT, Unix only).const server = listen({ hostname: "127.0.0.1", port: 8080 })
connect({ path }, options?)({ path }, { allowHalfOpen? }) => SocketConnect to a Unix domain socket at an absolute path, given alone. Plaintext only. The path must be named in --allow-net=unix:<path>; a bare --allow-net does not cover it. SocketInfo.remoteAddress is the path. Unix only.const sock = connect({ path: "/var/run/docker.sock" })
listen({ path })({ path }) => UnixListenerListen on a Unix domain socket at an absolute path, given alone. The path must be named in --allow-listen=unix:<path>. addr resolves to { path }. A taken path (including a stale file) is ERR_ADDRESS_IN_USE; close() removes the file this listener created. Unix only.const server = listen({ path: "/run/app.sock" })
bind(options)({ hostname?, port, reusePort?, reuseAddress?, broadcast?, ttl?, multicastTtl?, multicastLoopback? }) => DatagramSocketBind a UDP socket. hostname defaults to 0.0.0.0 (all interfaces — pass 127.0.0.1 for loopback-only; a locked-down host may refuse a wildcard bind). port 0 picks an ephemeral port (read it from sock.addr). Returns a DatagramSocket — send/receive, and async-iterable of datagrams. Binding needs NetListen; sending needs Net, checked per destination. Options are set at the bind and an omitted one leaves the OS default.const sock = bind({ hostname: "0.0.0.0", port: 5353 })
resolveDns(name, type, options?)(string, RecordType, { nameServer?, signal? }) => Promise<Record[]>Look up DNS records of one type. Deno's shape: see DNS.await resolveDns("example.com", "MX")

Client

JavaScript
import { connect } from "runtime:net";

// WinterTC connect() — returns a Socket immediately; .opened settles on connect.
const sock = connect({ hostname: "example.com", port: 80 });
await sock.opened;

const writer = sock.writable.getWriter();
await writer.write(new TextEncoder().encode("GET / HTTP/1.0\r\n\r\n"));
await writer.close();

// Decode through TextDecoderStream so a multi-byte character split across two
// chunks is still decoded correctly.
let body = "";
for await (const chunk of sock.readable.pipeThrough(new TextDecoderStream())) {
  body += chunk;
}

Socket

MemberTypeDescription
readableReadableStream<Uint8Array>Incoming bytes.
writableWritableStream<Uint8Array>Outgoing bytes; closing the writer half-closes (FIN).
openedPromise<SocketInfo>Resolves once connected: { remoteAddress, remotePort, localAddress, localPort, alpn, peerCertificate }. alpn is the negotiated TLS protocol, else null. peerCertificate is the verified client certificate on a socket accepted by a listener with requestCert, else null — the same shape as runtime:http's.
closedPromise<void>Resolves when the socket is fully closed.
close(reason?)Promise<void>Fully close the socket. reason is advisory (WinterTC) and ignored.
startTls()SocketUpgrade a secureTransport: "starttls" socket to TLS in place; returns a new encrypted Socket (the original is consumed).
upgradedbooleanTrue only after a startTls() upgrade.

TLS

JavaScript
import { connect } from "runtime:net";

// TLS client — secureTransport "on" (certificate verification on), offering ALPN.
const sock = connect({ hostname: "example.com", port: 443 }, {
  secureTransport: "on",
  alpn: ["h2", "http/1.1"],
});
const { alpn } = await sock.opened; // negotiated protocol, e.g. "h2" (or null)

Client certificates

JavaScript
// Present one: a server that requires it verifies this chain.
const sock = connect({ hostname: "db.internal", port: 5432 }, {
  secureTransport: "on", ca: internalCa, cert: clientCert, key: clientKey,
});

// Require one: a client without a certificate `ca` signed fails the handshake.
const server = listen({
  port: 8443, secureTransport: "on", cert, key, requestCert: true, ca: clientCa, crl,
});
for await (const conn of server) {
  const { peerCertificate } = await conn.opened; // { subject, subjectaltname, … }
}

cert and key go together, and the key must belong to the certificate. ca is required with requestCert; there is no way to accept a client whose certificate did not verify.

Server

JavaScript
import { listen } from "runtime:net";

const server = listen({ hostname: "127.0.0.1", port: 8080 });
const { port } = await server.addr;

for await (const conn of server) {
  conn.readable.pipeTo(conn.writable); // echo each connection
}
JavaScript
import { listen } from "runtime:net";

// Terminate TLS on accept — cert/key are inline PEM (no extra capability).
const server = listen({
  hostname: "127.0.0.1", port: 8443,
  secureTransport: "on", cert: certPem, key: keyPem, alpn: ["h2", "http/1.1"],
});
for await (const conn of server) {
  const { alpn } = await conn.opened; // negotiated protocol
}

Listener

MemberTypeDescription
addrPromise<{ hostname, port }>The bound address (resolves after bind).
accept()Promise<Socket | null>The next connection, or null once closed.
close()Promise<void>Stop listening.
setSecureContext({ cert?, key?, ca?, crl? })Promise<void>Replace a TLS listener's credentials for connections accepted from now on; open ones keep theirs. Rejects, keeping the current ones, when the certificate does not load or the key does not match.
[Symbol.asyncIterator]AsyncIterable<Socket>for await (const conn of server) { … }

UDP

JavaScript
import { bind } from "runtime:net";

const sock = bind({ hostname: "0.0.0.0", port: 5353 });
const { port } = await sock.addr;

for await (const { data, address, port } of sock) {
  await sock.send(data, { hostname: address, port }); // echo to the sender
}
JavaScript
import { bind } from "runtime:net";

// A connected socket: sends need no address, and only the peer is heard.
const statsd = bind({ hostname: "0.0.0.0", port: 0 });
await statsd.connect("127.0.0.1:8125");
await statsd.send("page.view:1|c");

// Multicast: membership is dynamic, the socket options are set at the bind.
const mdns = bind({
  hostname: "0.0.0.0", port: 5353,
  reuseAddress: true, multicastTtl: 1, multicastLoopback: false,
});
await mdns.joinMulticast("224.0.0.251", { interface: "192.168.1.10" });

DatagramSocket

MemberTypeDescription
send(data, address?)Promise<number>Send one datagram; resolves with the bytes sent. address is "host:port" or { hostname, port }, required unless connected. Net, checked per destination.
sendMany(messages, address?)Promise<number>Send a batch in one crossing; resolves with how many left. Entries are payloads or { data, address }. Saves the crossing, not the syscalls.
receive()Promise<Datagram | null>The next { data, address, port, truncated }, or null once closed. One call is one message — a zero-length datagram is a message, not an EOF.
receiveMany(max?)Promise<Datagram[] | null>A datagram plus up to max - 1 more already queued (default 32). Never waits to fill a batch.
connect(address)Promise<SocketInfo>Fix the peer: sends need no address, datagrams from anyone else are discarded. No packet is sent, so it succeeds against a host that is not listening.
joinMulticast(group, options?)Promise<void>Join a group. options.interface is an IPv4 address for a v4 group, an interface index for a v6 one; options.source makes it source-specific (RFC 4607, IPv4) so the network filters everyone else out.
leaveMulticast(group, options?)Promise<void>Leave a group — with the same source, if it had one.
setTtl(n) / setMulticastTtl(n)Promise<void>Hop limits, after the bind.
setBroadcast(on) / setMulticastLoopback(on)Promise<void>The two toggles, after the bind.
setMulticastInterface(iface)Promise<void>Which local interface carries outgoing multicast. No bind-time twin.
ref() / unref()thisWhether a pending receive() keeps the process alive.
addrPromise<{ hostname, port }>The bound address.
close()Promise<void>Close; a parked receive() resolves to null.
closedPromise<void>Resolves once closed.
[Symbol.asyncIterator]AsyncIterable<Datagram>for await (const d of sock) { … }, ending at close().

Bind options

OptionTypeDescription
reusePortbooleanShare the address across processes (SO_REUSEPORT). Unix only — refused elsewhere.
reuseAddressbooleanShare it with another socket (SO_REUSEADDR) — what two processes receiving one multicast group need.
broadcastbooleanPermit sending to the broadcast address. IPv4 only; on a v6 socket it is an error.
ttlnumberHop limit for unicast datagrams, 0–255.
multicastTtlnumberHop limit for multicast datagrams, 0–255. The OS default is 1: the local segment.
multicastLoopbackbooleanWhether multicast sends come back to this host. Off means a sender does not hear its own announcements.
ipv6OnlybooleanFor a v6 bind: IPv6 only, or also IPv4 through v4-mapped addresses. Omitted leaves the platform default, which differs between platforms.

reusePort, reuseAddress and ipv6Only are bind-time only — they must be set between socket() and bind(). The rest have setters.

DNS

JavaScript
import { resolveDns } from "runtime:net";

const txt = await resolveDns("_dmarc.example.com", "TXT");
const [srv] = await resolveDns("_imaps._tcp.example.com", "SRV", {
  nameServer: { ipAddr: "10.0.0.2", port: 53 },
  signal: AbortSignal.timeout(3000),
});

The name is asked as written — never extended by the system's search domains — and must be ASCII (an international name in its xn-- form). Names in records are fully qualified, with a trailing dot. Each call asks DNS; nothing is cached.

TypeEach record
A, AAAAaddress string
CNAME, NS, PTRname string
TXTstring[] — the record's character-strings, in order
MX{ preference, exchange }
SRV{ priority, weight, port, target }
CAA{ critical, tag, value }
NAPTR{ order, preference, flags, services, regexp, replacement }
SOA{ mname, rname, serial, refresh, retry, expire, minimum }
OptionTypeDescription
nameServer{ ipAddr, port? }Ask this server (port default 53) instead of the system's. It must be allowed by --allow-net, since the query is sent there.
signalAbortSignalAbandons the lookup; the promise rejects with the signal's reason.

The system's servers are the nameserver lines of /etc/resolv.conf on Linux and macOS, and the DNS servers of the active network adapters on Windows. A scoped --allow-net permits a query about a host it names, ignoring leading _-labels: --allow-net=example.com permits _dmarc.example.com, not api.example.com.

Errors

ErrorWhen
TypeErrorAny socket failure — invalid options, or a connect / TLS / I/O error (the latter reject .opened or the streams). The message is prefixed "SocketError: " (WinterTC SocketError).
DOMExceptionname "NotAllowedError" — the Net (connect, send) or NetListen (listen, bind) capability is not granted.
Error, code ERR_NOT_FOUNDresolveDns: the name does not exist (NXDOMAIN). A name with no records of the type resolves to [] instead.
Error, code ERR_DNSresolveDns: no server answered — timeouts, SERVFAIL, REFUSED — or the name cannot be asked.
Error, code ERR_PERMISSION_DENIEDresolveDns: a scoped --allow-net does not name the host, or the nameServer.
Last updated on
Edit this page