> ## Documentation Index
> Fetch the complete documentation index at: https://iii.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Channels

> Stream large or binary payloads between iii workers.

Channels move large or binary payloads between iii workers without putting the data in a JSON
function payload. Use one for files, images, datasets, payloads above the JSON message limit, audio
or video streams, or incremental progress updates during long-running work. For small JSON, use a
regular `worker.trigger(...)` call.

> [!NOTE]
>
> For the underlying model (how channels are addressed, multiplexed, and torn down), see [Channels
> architecture](https://iii.dev/docs/0-25-0/understanding-iii/channels.md).

## Payload size

The limit applies to the complete serialized UTF-8 JSON envelope, measured in bytes. The Rust,
Node, Python, and Go SDKs support messages up to **64 MiB**. The browser SDK has a **16 MiB**
outbound limit and does not fragment messages because browser WebSocket APIs do not expose frame
control. Browser workers also have a **16 MiB** receive limit.

For Rust, Node, Python, and Go, messages up to **16 MiB** are sent as one text frame. Larger
messages, up to and including 64 MiB, are sent as one fragmented WebSocket text message using
multiple frames. The engine sends every message larger than **1 MiB** to a worker as one fragmented
text message of 1 MiB frames, so no frame it sends exceeds a worker's frame size limit.
Applications send and receive these messages through the usual function APIs.

The receiving worker must run SDK version **0.24.5 or later** to accept messages larger than
16 MiB. An updated engine checks the serialized envelope against the target worker's receive limit
and returns `payload_too_large` to the caller when the target cannot accept it. The engine sets
that limit when the worker registers:

* A browser worker gets **16 MiB**.
* A worker that declares the `large-messages.v1` protocol feature gets **64 MiB**, whatever
  version it reports. The Rust, Node, Python, and Go SDKs declare it from 0.25.0; the browser SDK
  does not.
* Any other worker gets **64 MiB** when the version it reports is an SDK version of 0.24.5 or
  later, written as semver (`0.24.6`, `0.25.0-rc.5`) or PEP 440 (`0.25.0rc5`, `0.24.5.post1`). It
  gets **16 MiB** when the version is missing, cannot be parsed, or is older, including
  pre-releases of 0.24.5. Other features, such as `observe.v1`, do not count.

SDK 0.24.5 through 0.25.0-rc.5 do not declare `large-messages.v1`, so the engine relies on the
version they report. A Rust worker on one of them that sets `WorkerMetadata.version` to its own
version instead of the SDK version therefore stays at 16 MiB. Upgrade it to SDK 0.25.0 or later.

Above **64 MiB**, the Rust, Node, Python, and Go SDKs reject the message locally with
`payload_too_large`; the error message links to this [Payload size](#payload-size) section. Engines
older than the updated size check forward messages as a single frame without checking the target's
limit. An older worker may then disconnect if the message is too large for it. Use an engine and
SDKs from the same release, or newer.

The engine reads up to **64 MiB** per message from a worker, whether the message arrives as one
frame or as fragments, so it also accepts the single large frames of SDKs older than 0.24.5. When a
worker sends a larger message, the engine drops it and the connection stays open. A call gets
`payload_too_large` as its result, and when a function returns a result that is too large, its
caller gets `payload_too_large` instead. Other calls on the connection are not affected. A message
the engine cannot answer, such as a call that expects no result, is dropped and logged.

Observer activations follow the same limits, and the engine keeps at most **64 MiB** of one observed
event (every field its observers capture, input and output counted separately). An event over either
limit is not delivered: the observer counts a `TOO_LARGE` drop and the engine logs a warning. See
[Observers / Event size](https://iii.dev/docs/0-25-0/using-iii/observers.md#event-size).

Use a channel for payloads above **64 MiB**. Channels stream payload bytes separately from the JSON
function envelope. See [Using channels](#using-channels).

## Using channels

A channel is created by one worker and has two local stream ends: `writer` and `reader`; plus two
serializable refs (`writerRef` / `readerRef`) that can be handed to another function. The
subsections below cover the local-end API: creating a channel, writing bytes into its `writer`, and
reading bytes from its `reader`.

### Create a channel

`worker.createChannel()` returns a channel with two local stream objects and two serializable refs:
`writer` and `reader` are the local stream ends, and `writerRef` / `readerRef` are the tokens you
pass to another function so it can read or write the other end.

> [tab] Node / TypeScript
>
> ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> const channel = await worker.createChannel();
>
> // channel.writer
> // channel.reader
> // channel.writerRef
> // channel.readerRef
> ```

> [tab] Python
>
> ```python theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> channel = iii_client.create_channel()
>
> # channel.writer
> # channel.reader
> # channel.writer_ref
> # channel.reader_ref
> ```

> [tab] Rust
>
> ```rust theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> let channel = worker.create_channel(None).await?;
>
> // channel.writer
> // channel.reader
> // channel.writer_ref
> // channel.reader_ref
> ```

### Write to a channel

Write the payload to the local `writer` and close it when finished. The bytes flow through the
engine to whichever worker holds the matching `reader`.

> [tab] Node / TypeScript
>
> ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> const channel = await worker.createChannel();
>
> channel.writer.stream.end(Buffer.from("file contents"));
> ```

> [tab] Python
>
> ```python theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> channel = await iii_client.create_channel_async()
>
> await channel.writer.write(b"file contents")
> await channel.writer.close_async()
> ```

> [tab] Rust
>
> ```rust theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> let channel = worker.create_channel(None).await?;
>
> channel.writer.write(b"file contents").await?;
> channel.writer.close().await?;
> ```

### Read from a channel

Read the local `reader` until the other end closes. The bytes arrive in the order they were written
by whichever worker holds the matching `writer`.

> [tab] Node / TypeScript
>
> ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> const channel = await worker.createChannel();
>
> let bytes = 0;
> for await (const chunk of channel.reader.stream) {
>   bytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
> }
> ```

> [tab] Python
>
> ```python theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> channel = await iii_client.create_channel_async()
>
> bytes_total = 0
> async for chunk in channel.reader:
>     bytes_total += len(chunk)
> ```

> [tab] Rust
>
> ```rust theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> let channel = worker.create_channel(None).await?;
>
> let mut bytes = 0;
> while let Some(chunk) = channel.reader.next_binary().await? {
>     bytes += chunk.len();
> }
> ```

## Using channels across functions

A channel only becomes useful once both ends are owned by different code paths. Typically one
function that holds the local `writer` / `reader` and another that receives the matching ref in its
payload. The two subsections below cover that handoff: first how to send a ref alongside a normal
trigger call, then how the receiving function turns it back into a live stream to read or write.

### Pass a channel ref to another function

Pass the `readerRef` (or `writerRef`) as part of a normal function invocation. The receiving
function uses the ref to read from (or write to) the channel.

> [tab] Node / TypeScript
>
> ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> const result = await worker.trigger({
>   function_id: "files::process",
>   payload: {
>     filename: "report.csv",
>     reader: channel.readerRef,
>   },
> });
> ```

> [tab] Python
>
> ```python theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> result = await iii_client.trigger_async({
>     "function_id": "files::process",
>     "payload": {
>         "filename": "report.csv",
>         "reader": channel.reader_ref.model_dump(),
>     },
> })
> ```

> [tab] Rust
>
> ```rust theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> use iii_sdk::TriggerRequest;
> use serde_json::json;
>
> let result = worker
>     .trigger(TriggerRequest {
>         function_id: "files::process".to_string(),
>         payload: json!({
>             "filename": "report.csv",
>             "reader": channel.reader_ref,
>         }),
>         action: None,
>         timeout_ms: None,
>     })
>     .await?;
> ```

Node and Python deserialize incoming channel refs into live `ChannelReader` / `ChannelWriter`
objects before the handler runs, so the ref arrives ready to iterate or write to. Rust receives the
ref in JSON and reconstructs the reader or writer explicitly with `ChannelReader::new(...)` or
`ChannelWriter::new(...)`.

> [!TIP]
>
> The channel belongs to the worker that created it. Keep that worker connected until the receiving
> function has attached to the ref. If the creator disconnects first, unattached ends are dropped.
> See [Channel ownership](https://iii.dev/docs/0-25-0/understanding-iii/channels.md#channel-ownership).

### Read from a channel ref

> [tab] Node / TypeScript
>
> ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> import type { ChannelReader } from "iii-sdk";
>
> worker.registerFunction("files::process", async (input: { reader: ChannelReader }) => {
>   let bytes = 0;
>   for await (const chunk of input.reader.stream) {
>     bytes += Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk);
>   }
>   return { bytes };
> });
> ```

> [tab] Python
>
> ```python theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> async def process_file(input: dict) -> dict:
>     reader = input["reader"]
>     total = 0
>     async for chunk in reader:
>         total += len(chunk)
>     return {"bytes": total}
>
> worker.register_function("files::process", process_file)
> ```

> [tab] Rust
>
> ```rust theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> use iii_sdk::{ChannelDirection, ChannelReader, IIIError};
> use serde_json::json;
>
> let refs = iii_sdk::extract_channel_refs(&input);
> let (_, reader_ref) = refs
>     .iter()
>     .find(|(k, r)| k == "reader" && matches!(r.direction, ChannelDirection::Read))
>     .ok_or_else(|| IIIError::Handler("missing reader channel ref".into()))?;
>
> let reader = ChannelReader::new(worker.address(), reader_ref);
> let mut bytes = 0;
> while let Some(chunk) = reader.next_binary().await? {
>     bytes += chunk.len();
> }
> Ok(json!({ "bytes": bytes }))
> ```

### Write to a channel ref

> [tab] Node / TypeScript
>
> ```typescript theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> import type { ChannelWriter } from "iii-sdk";
>
> worker.registerFunction("files::generate", async (input: { writer: ChannelWriter }) => {
>   input.writer.stream.write(Buffer.from("hello "));
>   input.writer.stream.end(Buffer.from("world"));
>   return { ok: true };
> });
> ```

> [tab] Python
>
> ```python theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> async def generate_file(input: dict) -> dict:
>     writer = input["writer"]
>     await writer.write(b"hello ")
>     await writer.write(b"world")
>     await writer.close_async()
>     return {"ok": True}
>
> worker.register_function("files::generate", generate_file)
> ```

> [tab] Rust
>
> ```rust theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
> use iii_sdk::{ChannelDirection, ChannelWriter, IIIError};
> use serde_json::json;
>
> let refs = iii_sdk::extract_channel_refs(&input);
> let (_, writer_ref) = refs
>     .iter()
>     .find(|(k, r)| k == "writer" && matches!(r.direction, ChannelDirection::Write))
>     .ok_or_else(|| IIIError::Handler("missing writer channel ref".into()))?;
>
> let writer = ChannelWriter::new(worker.address(), writer_ref);
> writer.write(b"hello ").await?;
> writer.write(b"world").await?;
> writer.close().await?;
> Ok(json!({ "ok": true }))
> ```

> [!NOTE]
>
> For the per-language channel API surface, see the SDK reference:
> [Node](https://iii.dev/docs/0-25-0/reference/sdk-node.md), [Python](https://iii.dev/docs/0-25-0/reference/sdk-python.md), and
> [Rust](https://iii.dev/docs/0-25-0/reference/sdk-rust.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.