Serverless-first. Built for cold starts, tight memory, and one binary.Deploy guide →

title: udp API description: udp.socket reference — POSIX-style datagram sockets: bind, sendto, recvfrom, and connected send/recv. order: 6

udp API

Native UDP datagram sockets, exposed with the literal POSIX call names: sock.bind, sock.connect, sock.sendto, sock.recvfrom, sock.send, sock.recv, sock.getsockname, sock.getpeername, sock.close. If you've written a UDP socket in C, the shape is already familiar — only the promise-based sendto / recvfrom are new.

Payloads are binary only (Uint8Array / ArrayBuffer) and addresses are IP literals only — there is no DNS lookup on this path.

Quick look

// echo.js — two sockets in one process
(async () => {
  const server = udp.socket();
  server.bind({ address: "127.0.0.1" });          // port 0 → ephemeral
  const port = server.getsockname().port;

  const client = udp.socket();
  const sent = await client.sendto(new TextEncoder().encode("ping"), {
    address: "127.0.0.1",
    port,
  });

  const buf = new Uint8Array(1500);
  const [data, from] = await server.recvfrom(buf);
  console.log(from.address, from.port, sent, new TextDecoder().decode(data));

  server.close();
  client.close();
})();
ff echo.js
# 127.0.0.1 54321 4 ping

Modules vs scripts

ff treats a file as an ES module only when its name ends in .mjs or its first code is import/export. A plain .js file runs as a classic script, where top-level await is a syntax error — so the examples on this page wrap async code in (async () => { ... })(). Save any of them with a .mjs extension if you'd rather use top-level await as written.

udp.socket(family?)

Creates a UDP socket — the equivalent of socket(2).

const a = udp.socket();        // "inet"  — AF_INET
const b = udp.socket("inet");  // explicit IPv4
const c = udp.socket("inet6"); // AF_INET6
ArgumentValuesDefault
family"inet", "inet6""inet"
SituationResult
Unknown family stringthrows udp.socket: family must be "inet" or "inet6"
All 64 sockets in usethrows udp.socket: no socket slots available

Methods

MethodKindDescription
bind(opts)syncbind(2) — set the local address/port
connect(opts)syncconnect(2) — set the default peer
sendto(data, addr)Promise<number>sendto(2) — send one datagram to addr
recvfrom(buf)Promise<[view, addr]>recvfrom(2) — receive one datagram + sender
send(data)Promise<number>send(2) — connected sockets only
recv(buf)Promise<view>recv(2) — connected sockets only
getsockname()syncgetsockname(2) — local { address, port, family }
getpeername()syncgetpeername(2) — peer { address, port, family }
close()syncclose(2) — idempotent; rejects pending ops

bind({ address?, port? })

sock.bind({});                              // 0.0.0.0, ephemeral port
sock.bind({ address: "127.0.0.1" });        // IPv4 loopback, ephemeral port
sock.bind({ address: "127.0.0.1", port: 9000 });
sock.bind({ address: "::1", port: 9000 });  // on an "inet6" socket
OptionTypeDefault
addressstring (IP literal)"0.0.0.0" ("::" for inet6)
portnumber 0–655350 (kernel picks an ephemeral port)

Read the chosen port back with sock.getsockname().port. Invalid input throws (bind: invalid address, bind: invalid port, or bind: failed (address in use?)).

connect({ address, port })

Both fields are required. connect(2) on a UDP socket doesn't do any network handshake — it just records the default peer so you can use send / recv:

sock.connect({ address: "127.0.0.1", port: 9000 });
sock.getpeername(); // { address: "127.0.0.1", port: 9000, family: "inet" }

await sock.sendto(data, { address, port })

Sends one datagram and resolves with the number of bytes sent:

const n = await sock.sendto(new Uint8Array([1, 2, 3]), {
  address: "127.0.0.1",
  port: 9000,
});

await sock.recvfrom(buf)

Receives up to buf.length bytes and resolves with [data, sender]:

const buf = new Uint8Array(1500);
const [data, from] = await sock.recvfrom(buf);
// data — Uint8Array view of the received bytes (see Zero-copy below)
// from — { address: "127.0.0.1", port: 54321, family: "inet" }

Only one receive may be pending per socket; a second recvfrom before the first settles throws recvfrom: RecvPending.

await sock.send(data) / await sock.recv(buf)

Connected-socket forms that skip the address argument. Both throw send: socket is not connected / recv: socket is not connected when the socket hasn't been connected. recv resolves the data view directly (no sender object):

sock.connect({ address: "127.0.0.1", port: 9000 });
await sock.send(new TextEncoder().encode("ping"));

const buf = new Uint8Array(1500);
const data = await sock.recv(buf);

getsockname() / getpeername()

Both return an address object. getpeername() throws getpeername: socket is not connected until connect is called:

const addr = sock.getsockname();
// { address: "0.0.0.0", port: 0, family: "inet" } — before bind

close()

Returns undefined immediately; safe to call twice. Pending sendto / recvfrom promises reject with ...: socket closed, further method calls throw ...: socket is closed, and the slot is recycled once the kernel side drains. Closing is also what the GC finalizer does if you leak a socket.

const p = sock.recvfrom(new Uint8Array(1500));
sock.close();
await p; // rejects: recvfrom: socket closed

Data and buffers

DirectionAccepted typesResult
send / sendto dataArrayBuffer, Uint8Array, any TypedArray viewbytes sent (number)
recv / recvfrom bufferUint8Array or ArrayBufferzero-copy view of the received bytes

recvfrom is zero-copy: the resolved view aliases your buffer.

  • Uint8Array input → buf.subarray(0, n) (same backing ArrayBuffer, same byteOffset).
  • ArrayBuffer input → new Uint8Array(buf, 0, n).

If the datagram is larger than your buffer, it is truncated to buf.length and the rest is discarded (standard UDP behavior — size your buffer to 65507 if that matters).

Addresses

Address objects always have this shape:

{ address: "2001:db8::1", port: 9000, family: "inet6" }
  • address — IP literal. IPv6 uses the RFC 5952 compressed form (::1, 2001:db8::1).
  • family — "inet" or "inet6".
  • No DNS: "example.com" is rejected with ...: invalid address.

Errors

MessageThrown by
udp.socket: family must be "inet" or "inet6"udp.socket(family)
udp.socket: no socket slots availableudp.socket() — 64 in use
bind: invalid address / bind: invalid portbind argument validation
bind: failed (address in use?)bind(2) failed
connect: address required / connect: port requiredconnect without both fields
sendto: address required / sendto: port requiredsendto destination validation
send: socket is not connected / recv: socket is not connectedconnected-only methods
send: MessageTooLongdata > 65507 bytes
recvfrom: NotABufferbuffer isn't Uint8Array / ArrayBuffer
...: socket is closedany method after close()
...: socket closedrejection of a promise pending at close()

Full example

// udp-echo.js — connected client pings an echo server; both in one process.
(async () => {
  const server = udp.socket();
  server.bind({ address: "127.0.0.1" });
  const port = server.getsockname().port;

  // Server: reply to one datagram, then stop.
  const serving = (async () => {
    const buf = new Uint8Array(1500);
    const [data, from] = await server.recvfrom(buf);
    await server.sendto(data, { address: from.address, port: from.port });
    server.close();
  })();

  // Connected client
  const client = udp.socket();
  client.connect({ address: "127.0.0.1", port });

  await client.send(new TextEncoder().encode("ping"));
  const reply = await client.recv(new Uint8Array(1500));
  console.log(new TextDecoder().decode(reply)); // ping

  await serving;
  client.close();
})();

Limits

LimitValue
Concurrent sockets64 (shared pool)
In-flight sendto calls64
Maximum datagram65507 bytes
Pending receives per socket1
AddressesIP literals only (no DNS)
Missing featuresno setsockopt, multicast, or broadcast helpers

See also