What are Data Streams?
A Data Stream is a low-latency, two-way connection that carries raw bytes from one Ditto peer to another. A peer is any running copy of your app that uses Ditto, such as on a phone, tablet, or laptop. Each stream is opened on a named topic, a short label that both peers agree on, similar to a channel name.How streams reach other peers
Ditto peers connect to each other over whatever is available nearby, such as Bluetooth Low Energy or a local Wi-Fi network. Together, these connected peers form a mesh: a network in which peers can pass data along for each other, so it isn’t limited to the devices that are directly connected. To learn more, see Mesh Networking. A stream connects two peers even when they aren’t directly connected. For example, if your app on device A can reach device C only through device B, Ditto sends the stream through B for you. Your code names the peer it wants to reach, and Ditto finds the path. The devices in between only relay the bytes. Their apps don’t need to do anything, and unlike multi-hop sync, they don’t store any of the data.Device A connects to device C. They're out of range of each other, so Ditto routes the stream through device B, which passes the bytes along in both directions without storing them.
ngn_data_streams_enabled setting you add during setup.
Data Streams work differently from the document and attachment sync you may already know. Document sync converges structured data through the Ditto store: every peer that subscribes eventually sees the same data, and Ditto persists it for you. Data Streams are the opposite of that model — they carry raw bytes, are ephemeral, and never write anything to the Ditto store. Ditto doesn’t know or care what’s inside the bytes you send; your application defines its own message format on top of the channel, whether that’s Protobuf, JSON, or a custom raw frame format.
Document sync keeps a copy of the data on every peer that subscribes. Data Streams deliver the bytes without storing them on any peer.
When to use Data Streams
Data Streams and document sync solve different problems, and most apps end up using both. Reach for Data Streams when:- You need real-time, high-frequency, or ephemeral data delivered to another reachable peer, such as sensor readings, telemetry, live cursor or position updates, or audio frames.
- The data doesn’t need to be stored, queried, or converged across the mesh — it just needs to get to the other peer, fast.
- You want to define your own wire format and manage delivery guarantees yourself.
- The data must be persisted, so peers can read it later, offline or otherwise.
- The data must be queried with DQL, or must converge consistently across every peer in the mesh, not just one direct link.
Core concepts
Data Streams introduce a small vocabulary of their own. This table covers the terms used throughout the rest of this guide. For terms used across all of Ditto, see the Glossary.How a stream works, end to end
Before looking at any code, it helps to walk through the full lifecycle of a stream — who does what, and in what order.The receiving peer binds a topic
The sending peer discovers the receiver
The sender connects on the topic
The receiver's bind callback fires
take() to hold onto it and open it later.Both peers send and receive
Either side closes the stream
Reliable vs. unreliable streams
Every stream is opened as either reliable or unreliable, and both sides of the connection must agree on which one they’re using.Prerequisites and setup
Opt in to the preview API
DittoSwift with the PreviewDataStreams SPI — without it, the Data Streams types aren’t visible.Turn on Data Streams when Ditto opens
ngn_data_streams_enabled system parameter to the DittoConfig you already create in your install-guide setup:- The parameter is read once, when the
Dittoinstance opens. Setting it later has no effect. - Document and attachment sync are unaffected: they keep using your existing transports. For this preview release, leave the related
ngn_sync_enabledparameter at its default (false).
DITTO_NETWORK_ENABLE_NGN and DITTO_REPLICATION_OVER_NGN environment variables for this. They still work in 5.2 but are deprecated; use the system parameter instead.Start sync
Quickstart: stream sensor readings between two peers
This quickstart builds the two peer roles from the previous section into working code: one peer binds thesensor_data topic and receives readings, the other connects to it and sends them. Both sides use the reliable mode, and sensor_data satisfies the topic pattern ^[a-zA-Z0-9_ ]{1,15}$.
Receive: own the acceptor and its streams
DittoStream that nothing references is released immediately and stops delivering messages.SensorReceiverSession (and therefore its acceptor) alive for as long as you want to accept connections. Only one acceptor may bind a given topic at a time — close the existing session before binding the same topic again.Send: connect once, reuse one stream
connect waits for and lends an accepted candidate to your closure — the candidate is not opened for you, so your closure must open it. The SDK closes the candidate when the closure returns; the stream you opened from it stays open. (In Kotlin, call take() first if you want to keep the candidate itself beyond the closure.) Get receiverPeerKey from presence (the receiver’s peer key), and don’t call connect from inside the presence callback itself — see Cancellation and reconnect.Key patterns
Candidate ownership with take()
Candidates are callback-scoped and close when the callback returns. To use one after the callback (for example, on another coroutine or task), call take() and close the returned candidate yourself:
Send modes and maxSendSize
Whatever you send, first make sure the payload fits in a single send. A payload larger than the stream’s maxSendSize is rejected, so check it before sending. Unreliable streams carry each payload in a single network packet, so their limit is small (on the order of a kilobyte); reliable streams accept far larger payloads.
ByteArray in Kotlin, Data in Swift).
1. send — await delivery status
Use this when you want to know how the send finished. It suspends (Kotlin) or awaits (Swift) until the message’s lifecycle ends, giving you a DittoSendStatus (for example sent, failed, or cancelled). This is the default choice when you care whether a message made it out.
2. sendAndForget — fire-and-forget
Use this for high-frequency data where you don’t need per-message status and can tolerate the occasional drop — for example live sensor frames on an unreliable stream. It returns immediately and does not report a status, so it has the least overhead.
3. send with a monitoring closure — track progress
Use this when you need to observe the send as it progresses, such as implementing backpressure. You pass a closure that receives a DittoSendOperation; the closure runs while the send is in flight, and its return value becomes the result of the call.
Backpressure
Use the monitoring overload’sawaitStatusChange() to slow a fast producer to what the link can carry. Treat the unknown status as an indeterminate terminal result (don’t loop on it), and treat sent as “sent to the remote peer,” not proof the receiver’s app code processed it.
Closing resources
Every Data Streams handle — acceptors, candidates, and streams — has aclose() method (and in Kotlin is AutoCloseable, so use { } works). Retain acceptors and streams in a clear owner — like the SensorReceiverSession and SensorSenderSession above — and close them explicitly. Closing an acceptor unbinds the topic but does not close streams that are already open, so an owner that holds both must close the streams itself.
To stop sends, drain the queue, and wait for closure, use closeSync() in Kotlin or closeAndWait() in Swift. waitUntilClosed() only observes closure and can wait indefinitely. On shutdown, cancel jobs or tasks, close streams, close acceptors, then close (Kotlin) or release (Swift) Ditto.
Cancellation and reconnect
Data Streams do not reconnect automatically. If a stream closes — because the peer went away, the link dropped, or a reliable stream failed — it stays closed until your application opens a new one. Build reconnection into your own logic. Preserve cancellation — rethrowCancellationException in Kotlin, and stop retrying when Task.isCancelled is true in Swift — and use bounded exponential backoff with jitter when retrying failed connections. An in-flight cancellation can surface as a Data Streams error rather than a cancellation error, so check for cancellation before you retry. Avoid opening multiple simultaneous connects for the same peer and topic.
API at a glance
The two SDKs share the same types. Kotlin exposes most accessors as functions, Swift as (throwing) properties.AI Agent Prompts
If you use an AI coding assistant, expand and copy the prompt for your platform. Each is a self-contained skill for writing correct Ditto Data Streams code against the 5.2.0 preview API — the concepts on this page, the ownership and lifecycle rules, and a review checklist, in one block.Copy AI Data Streams Prompt for Kotlin (Click to Expand)
Copy AI Data Streams Prompt for Kotlin (Click to Expand)
Copy AI Data Streams Prompt for Swift (Click to Expand)
Copy AI Data Streams Prompt for Swift (Click to Expand)
Limitations and preview caveats
- Requires Ditto SDK 5.2.0 or later, on Swift (iOS and macOS) or Kotlin (Android).
- You must opt in to the preview API:
@file:OptIn(com.ditto.kotlin.PreviewDataStreams::class)in Kotlin,@_spi(PreviewDataStreams) import DittoSwiftin Swift. - Data Streams must be turned on with the
ngn_data_streams_enabledsystem parameter when Ditto opens. - Topics must match
^[a-zA-Z0-9_ ]{1,15}$. - The receive callback you pass to
openruns on the connection’s driver thread and must return quickly. - A message the binding peer sends right after opening a stream can be lost, because the connecting peer may not be ready yet. Let the connecting peer send first.
- Streams carry raw bytes and are never persisted to the Ditto store.
- We recommend leaving
ngn_sync_enabledat its default (false) for this preview release. - The API may change during Preview.