Stream: Server Side
Everything Stream does on the server — defining channels, receiving client packets, all six send methods, rate limiting, delta compression, and when to use reliable vs unreliable channels.
Stream.Init(). See Stream: Client Side for the client half.How the server bootstraps
On the first call to Stream.Init() from the server, Stream creates a Folder in ReplicatedStorage named RoExpressStream and places two remotes inside it:
| Remote | Type | Used for |
|---|---|---|
StreamUnreliable | UnreliableRemoteEvent | All channels with reliable = false (the default) |
StreamReliable | RemoteEvent | Channels created with reliable = true |
The client's Stream.Init() uses WaitForChild on these remotes with a 10-second timeout, so the server must call Stream.Init() first — typically in a Script that runs before any LocalScript.
Defining channels
Call Stream.Channel() for every channel your game needs, then call Stream.Init() once. Order of Channel() calls does not matter — IDs are derived from a hash of the channel name.
local RoExpress = require(game.ReplicatedStorage.RoExpress)
local Stream = RoExpress("Stream")
local moveSchema = Stream.Schema.New({
{ "userId", "f64" },
{ "cf", "CFrameLight" },
{ "health", "u8" },
})
local chatSchema = Stream.Schema.New({
{ "userId", "f64" },
{ "message", "string" }, -- variable-size: no delta compression
})
local move = Stream.Channel("playerMove", moveSchema, { maxRate = 30 })
local chat = Stream.Channel("chat", chatSchema, { reliable = true })
Stream.Init() -- creates remotes; clients can now connect
Receiving from clients
channel:On is how the server listens for packets sent by clients. The callback receives (data, player) — player is always a Player instance on the server, never nil.
move:On(function(data, player)
-- data: the unpacked table — { userId, cf, health }
-- player: the Player who fired — always non-nil on the server
print(player.Name, data.cf.Position, data.health)
end)
-- One-shot: fires exactly once then auto-removes
move:Once(function(data, player)
print(player.Name, "first packet received")
end)
-- Unsubscribe manually at any time
local unsub = move:On(function(data, player) end)
unsub() -- removes this callback
userId or similar identity field, always overwrite it on the server with the real player.UserId before relaying. Clients can put any value in a schema field.Server send API
All six send methods are server-only. Calling any of them from a client will error.
| Method | When to use |
|---|---|
channel:SendTo(player, data) | One specific player — e.g., sending that player's private inventory |
channel:SendExcept(except, data) | All players except one — the standard relay pattern after receiving a client update |
channel:Broadcast(data) | Every connected player — server-authoritative world events |
channel:SendToList(players, data) | An explicit subset — e.g., players in the same zone or team |
channel:SendToDelta(player, data) | One player with delta compression — only changed fields sent |
channel:BroadcastDelta(data) | All players with delta compression — each player gets its own delta against its own last-known state |
Relay pattern (most common)
Receive from one client, relay to everyone else. This is the standard movement replication pattern.
move:On(function(data, player)
data.userId = player.UserId -- inject real sender
move:SendExcept(player, data) -- relay to all others
end)
Targeted send
-- Send only to one player (e.g. private data on join)
move:SendTo(player, { userId = 0, cf = spawnCFrame, health = 100 })
-- Send to a zone's player list
move:SendToList(zoneMembers, data)
Rate limiting
Rate limiting applies to incoming client→server packets only. Server→client sends are never rate-limited by Stream.
local move = Stream.Channel("playerMove", moveSchema, {
maxRate = 30, -- drop anything above 30 fires/sec per client
onDrop = function(player, reason)
warn(player.Name, "dropped:", reason)
end,
})
maxRate is implemented with a TokenBucket per player. When the bucket runs dry, the packet is silently dropped (or onDrop is called if provided).
Delta compression
Delta compression sends only the fields that changed since the last packet. It requires a fixed-size schema — no string fields. Schema size is pre-computed at channel definition time.
-- SendToDelta: per-player delta (each player tracks its own last-known state)
move:SendToDelta(player, newData)
-- BroadcastDelta: per-player delta broadcast
-- Each player may have joined at a different time and have different state,
-- so each gets its own independent delta rather than a shared one.
move:BroadcastDelta(newData)
A full packet is automatically forced on the first send to each player, and every deltaInterval packets after that (default: 10). This ensures clients resync even after unreliable packet loss.
| Schema has string fields? | Delta available? | Size known at compile time? |
|---|---|---|
| No (all fixed types) | ✓ | ✓ |
| Yes | ✗ | ✗ |
Reliable channels
Use reliable = true for data that must arrive in order and is not time-sensitive enough to tolerate drops. Examples: chat messages, inventory grants, ability activations.
local chat = Stream.Channel("chat", chatSchema, { reliable = true })
-- Reliable channels use RemoteEvent (ordered, guaranteed delivery)
-- No sequence numbers — delta compression is still available for fixed schemas
Unreliable channels (the default) use UnreliableRemoteEvent. Packets may arrive out-of-order or not at all, but they have lower latency and no head-of-line blocking. Stream filters duplicate and out-of-order packets via a rolling sequence number.
PlayerRemoving cleanup
Stream automatically frees all per-player state (rate-limit buckets, delta caches, sequence numbers) when a player leaves. You only need to clean up your own application-level state:
-- Stream handles its own cleanup. Clean up your game state:
game:GetService("Players").PlayerRemoving:Connect(function(player)
myHistory[player] = nil
myInventory[player] = nil
end)
See also
Stream | full API reference · Stream: Client Side | sending, throttling, subscribing · FPS Guide | end-to-end example with lag compensation · TokenBucket | rate limiting internals · Stream Benchmarks | bandwidth and delta compression measurements