Guide

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.

Read this alongside the client guide. Both sides must define identical channels before calling 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:

RemoteTypeUsed for
StreamUnreliableUnreliableRemoteEventAll channels with reliable = false (the default)
StreamReliableRemoteEventChannels 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
Never trust client-provided identity fields. If your schema includes a 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.

MethodWhen 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