Options
Options configure how BlinkBlox generates your modules: where they are written, what they are called, and how much a client is allowed to make the server do. They go at the top of a schema, before any declaration.
option [OPTION] = [VALUE]A few rules hold for all of them:
- Options come first. An
optionafter the first declaration is refused (E3016, “Option set after start of file”). - Each option is set once. Setting one twice is refused (
E3024), unless the two settings sit under different profiles:@profile("dev")and@profile("release")may each setMaxPacketSize, but not alongside an unmarked setting of it. - Values are typed. A boolean option takes
trueorfalse, a path takes a quoted string,Casingtakes a bare word, and every numeric option takes a positive whole number –0,-5and0.5are all refused (E2003), except thatMaxInstancesPerPacketmay be0andClientConnectTimeouttakes any number of seconds above zero,0.5included. An unknown option name is refused too, with the list of the ones that exist.
At a glance
Section titled “At a glance”| Option | Value | Default | Section |
|---|---|---|---|
ServerOutput, ClientOutput |
path | required by the CLI | Output |
TypesOutput |
path | not generated | Output |
Typescript |
boolean | false |
Output |
Casing |
Pascal, Camel, Snake |
Pascal |
Output |
RemoteScope |
string | "" |
Output |
RemotesFolder |
string | none | Output |
ExportLimits |
boolean | false |
Output |
ExportRates |
boolean | false |
Output |
FutureLibrary, PromiseLibrary |
Luau path | none | Output |
ManualReplication |
boolean | false |
Runtime behaviour |
ClientFlushRate |
sends a second | 60 |
Runtime behaviour |
BatchUnreliable |
boolean | false |
Runtime behaviour |
UsePolling |
boolean | false |
Runtime behaviour |
Predict |
boolean | false |
Runtime behaviour |
PackedBooleanReader |
"Auto", "Bits", "Rows" |
"Auto" |
Runtime behaviour |
ClientConnectTimeout |
seconds | wait for ever | Starting up |
AutoStart |
boolean | true |
Starting up |
WriteValidations |
boolean | false |
Validation |
SyncValidation |
boolean | true |
Validation |
MaxUnreliableSize |
bytes | 980 |
Validation |
MaxPacketSize |
bytes | 8192 |
Inbound limits |
MaxEventsPerPacket |
events | 64 |
Inbound limits |
MaxInstancesPerPacket |
instances | 256 |
Inbound limits |
InboundBytesPerSecond |
bytes | 8 * MaxPacketSize |
Inbound limits |
InboundBurst |
bytes | the larger of the above and MaxPacketSize |
Inbound limits |
OutboundBytesPerSecond |
bytes | none | Outbound budget |
OutboundBurst |
bytes | one second of the above | Outbound budget |
DefaultRate |
events a second | none | Rate limits |
RequireRates |
boolean | false |
Rate limits |
InvocationTimeout |
seconds | 10 |
Invocations |
TrafficStats |
boolean | false |
Diagnostics |
UseColon |
boolean | – | Deprecated |
Output
Section titled “Output”ServerOutput, ClientOutput, TypesOutput
Section titled “ServerOutput, ClientOutput, TypesOutput”Where BlinkBlox writes the generated modules. A relative path is resolved against the directory of the schema file, not the directory you run the CLI from.
option TypesOutput = "../Network/Types.luau"option ServerOutput = "../Network/Server.luau"option ClientOutput = "../Network/Client.luau"The CLI needs ServerOutput and ClientOutput and stops without
them. The server and client paths always get a .luau extension, whatever extension you wrote.
TypesOutput is optional. When set, BlinkBlox also writes a third, shared module that holds the Luau
types of your declarations and the Read/Write pairs of your exports,
but no events or functions – useful for code that has to name a schema type on both sides without
requiring either network module.
The Studio plugin places its output itself and does not read these paths.
Typescript
Section titled “Typescript”Default: false
Also generate TypeScript declaration files for the server and client modules, for use with roblox-ts.
Each .d.ts is written beside its module and named after it – Server.luau gets Server.d.ts, and a
module called init.luau gets index.d.ts. No declaration file is written for TypesOutput.
option Typescript = trueSee roblox-ts for the full setup.
Casing
Section titled “Casing”Default: Pascal
Values: Pascal, Camel, Snake
The casing of the functions BlinkBlox generates. It changes the generated API, not the names of your events, functions or types.
option Casing = CamelPascal |
Camel |
Snake |
|---|---|---|
Fire, FireAll, FireList, FireExcept |
fire, fireAll, fireList, fireExcept |
fire, fire_all, fire_list, fire_except |
On, Invoke, Predict, Iter, Next |
on, invoke, predict, iter, next |
on, invoke, predict, iter, next |
Read, Write |
read, write |
read, write |
StepReplication |
stepReplication |
step_replication |
SetRateLimitHandler |
setRateLimitHandler |
set_rate_limit_handler |
SetDecodeErrorHandler |
setDecodeErrorHandler |
set_decode_error_handler |
SetPacketDropHandler |
setPacketDropHandler |
set_packet_drop_handler |
SetListenerErrorHandler |
setListenerErrorHandler |
set_listener_error_handler |
Remotes, Start, Connected |
remotes, start, connected |
remotes, start, connected |
Limits, with ExportLimits |
limits |
limits |
Rates, with ExportRates |
rates |
rates |
RemoteScope
Section titled “RemoteScope”Default: ""
A prefix for the two remotes this schema creates. With "PACKAGE" they are named
PACKAGE_BLINK_RELIABLE_REMOTE and PACKAGE_BLINK_UNRELIABLE_REMOTE; without it,
BLINK_RELIABLE_REMOTE and BLINK_UNRELIABLE_REMOTE.
option RemoteScope = "PACKAGE"Every generated module registers its scope in _G._BLINK when it is required, and errors if a
module with the same scope is already running in that Luau VM. So two schemas in one game – a library
that ships its own networking inside your game’s, for instance – need different scopes. The scope is
already part of the remote names, so it is not part of the
schema signature: a client with the wrong scope never finds the remote at all.
Both modules export the two names as Remotes.Reliable and Remotes.Unreliable, so code that has to
find the remotes itself never spells them out.
RemotesFolder
Section titled “RemotesFolder”Default: none – the remotes sit directly in ReplicatedStorage
Puts the two remotes in a folder of this name in ReplicatedStorage, rather than beside everything else there.
option RemotesFolder = "Network"The server module makes the folder when it is missing and uses one that is already there, such as a
folder Rojo syncs in. The client waits for the folder, then for the remotes in it; under
ClientConnectTimeout all of that waiting counts against the one timeout.
Both modules must be built with the same folder, or the client never finds the remotes. An empty
name is refused.
ExportLimits
Section titled “ExportLimits”Default: false
Adds a frozen Limits table to every generated module – server, client and TypesOutput – holding
the bounds the schema declares, so the game reads them instead of repeating them.
option ExportLimits = true
struct CrawlHead { Id: u8, Back: boolean }struct CrawlPacket { At: u16, Heads: CrawlHead[..16] }struct Tally { Count: u8<saturate>(1..255), Frame: u16<0.1> }event Crawls { From: Server, Type: Reliable, Call: SingleSync, Data: CrawlPacket }Net.Limits.CrawlPacket.Heads.Length.Max -- 16Net.Limits.Tally.Count.Min -- 1Net.Limits.Tally.Frame.Max -- 6553.5Net.Limits.Tally.Frame.Step -- 0.1Net.Limits.Crawls.Heads.Length.Max -- 16, the event's dataThe table mirrors the schema. A declaration is keyed by its name and a scope nests its own; a struct
keys its fields, a tagged enum its variants, a type pack its named elements or their positions, and a
function its Data and Return. A stream holds the bounds of the type
it streams, not of the (Value, ServerTime) it travels as. At the bottom is a range, { Min, Max }:
| Declared | What { Min, Max } holds |
|---|---|
a number with a range, a scale or saturate |
its bounds in the schema’s units, and Step for a scaled one |
a ranged string or buffer |
its length in bytes |
a ranged vector |
its magnitude |
| an array | { Length, Element }, whichever is bounded |
| a map | { Key, Value }, whichever is bounded |
What the schema bounds nowhere inside is left out, so a key exists only where there is a number to
read, and an open side is math.huge. The structural words – Limits, Min, Max, Step,
Length, Element, Key, Value – follow Casing (limits.CrawlPacket.Heads.length.max
under Camel or Snake); names from the schema are kept as written. The TypeScript output declares
it readonly. With the option on, Limits in the spelling Casing gives it is a reserved top-level
name (E3005). With it off, nothing is emitted and no name is taken.
ExportRates
Section titled “ExportRates”Default: false
Adds a frozen Rates table to every generated module – server, client and TypesOutput – holding
the rate limits the server enforces, so a client that paces
what it sends reads them instead of repeating them.
option ExportRates = true
event Report { From: Client, Type: Reliable, Call: SingleSync, Rate: 1, Burst: 4, Data: string(..64) }event Tap { From: Client, Type: Reliable, Call: SingleSync, Rate: 20, Data: u8 }event Score { From: Server, Type: Reliable, Call: SingleSync, Data: u32 }function Search { Yield: Coroutine, Rate: 2, Concurrency: 1, Data: string(..32), Return: u8[..10] }Net.Rates.Report.Rate -- 1, events a secondNet.Rates.Report.Burst -- 4, at onceNet.Rates.Tap.Burst -- 20: none was written, so the defaultNet.Rates.Search.Concurrency -- 1Net.Rates.Score -- nil: the server sends it, and nothing limits thatThe table mirrors the schema. A declaration is keyed by its name and a scope nests its own. Each entry holds what the server holds that declaration to, whether or not the schema spells it out:
| Key | What it holds |
|---|---|
Rate |
The declaration’s Rate, or DefaultRate where it has none. |
Burst |
The declaration’s Burst, or the default where it has none: Rate rounded up, and at least 1. |
Concurrency |
A function’s Concurrency, where it declares one. |
Only what is limited has an entry. Limits apply to what a client sends, so an event or function the
server sends is left out, and so is an inbound one with no Rate, no DefaultRate and no
Concurrency. A function limited by Concurrency alone has that key and no other. The Rate of a
stream is how often the server sends, not a limit, and is not in the
table. The structural words – Rates, Rate, Burst, Concurrency – follow Casing
(rates.Report.burst under Camel or Snake); names from the schema are kept as written. The
TypeScript output declares it readonly. With the option on, Rates in the spelling Casing gives
it is a reserved top-level name (E3005). With it off, nothing is emitted and no name is taken.
The numbers are the same on both sides, but only the server enforces them: a client that sends faster is refused there, as before.
FutureLibrary and PromiseLibrary
Section titled “FutureLibrary and PromiseLibrary”A function declared Yield: Future or Yield: Promise needs
the library it yields through. The value is pasted into a require(...) at the top of the server and
client modules, so it is a Luau expression rather than a file path. The modules define
ReplicatedStorage themselves, so you can start from it.
option FutureLibrary = "ReplicatedStorage.Packages.Future"option PromiseLibrary = "ReplicatedStorage.Packages.Promise"Without it, a schema that uses the yield type fails to generate, and an empty string counts as
without it:
Cannot use yield type: "Future", without providing a path to the future library.
The require is emitted only into a module that invokes such a function. The side that answers the
call never touches the library, so it does not load it.
Runtime behaviour
Section titled “Runtime behaviour”ManualReplication
Section titled “ManualReplication”Default: false
Reliable events and function calls are not sent the moment you fire them. Each side batches them into
one buffer (the server keeps one per player) and sends the batch once a frame, from
RunService.Heartbeat. Unreliable events are the exception: each one is sent at once, in a packet of
its own, unless BatchUnreliable gathers them too.
ManualReplication = true removes that Heartbeat connection, and nothing is sent until you call
StepReplication yourself. The function is exported either way; with this option it is the only thing
that flushes.
option ManualReplication = truelocal RunService = game:GetService("RunService")local Net = require(path.to.Server)
RunService.PostSimulation:Connect(function() -- ... fire this frame's events ... Net.StepReplication()end)ClientFlushRate
Section titled “ClientFlushRate”Default: 60
The most times a second a client’s Heartbeat sends what it has gathered. A client running at 60 frames a second or fewer sends every frame; one at 144 or 240 sends 60 times a second, and what a frame fires waits for the next send.
Every remote call costs about 11 bytes beside its payload – measured in Studio, 240 calls of 40 bytes a second went out as 13.2 KB/s, and 60 calls of 160 bytes as 11.2 KB/s – and the server’s inbound budget charges each packet at least 128 bytes, so a client at 500 frames a second that sent every frame spent the whole default budget on those floors alone. Roblox’s own network step runs 60 times a second, so a send made more often does not arrive sooner.
option ClientFlushRate = 120It limits the Heartbeat only: StepReplication called by the game still sends at once, and under
ManualReplication there is no Heartbeat to limit. An unreliable event outside
BatchUnreliable is sent when it is fired, as before.
BatchUnreliable
Section titled “BatchUnreliable”Default: false
Gathers every unreliable event a side sends in a frame into as few packets as
MaxUnreliableSize allows, sent at the same flush as the reliable batch, rather
than sending each at once in a remote call of its own. On the server, an event fired to everyone goes
into one packet that every player is sent in a single FireAllClients, and an event to one player
into that player’s; an OrderedUnreliable event carries each player’s own sequence number, so it is
always gathered player by player. A client’s packet is also cut at the server’s
MaxEventsPerPacket.
A frame’s worth of small events then costs one remote call and one packet header instead of one each, and a client stays further inside the server’s inbound budget, which charges every packet at least 128 bytes. The price is time: an event waits for the flush, up to a frame, as a reliable one always has. It pays where a recipient gets several unreliable events a frame – a client’s input, a server’s effects. Where each player gets one, there is nothing to gather: the call count stays the same, and the batch’s bookkeeping costs an interpreted side a little more than sending at once did.
A stream to everyone is gathered this way whatever the option says, since it is sent from the flush already. A stream held per player sends each player one state a step, so it joins the player’s batch only under this option, beside their other events.
option BatchUnreliable = trueUsePolling
Section titled “UsePolling”Default: false
Receive every event through the polling API, as if each had been declared Call: Polling. The Call
field is still required on each event, and its value is ignored. See
polling.
option UsePolling = truePredict
Section titled “Predict”Default: false
Adds a Predict entry point to every event on the side that listens to it with On. It hands the
event straight to that side’s own listeners without touching a remote.
option Predict = true
event Damaged { From: Server, Type: Reliable, Call: ManyAsync, Data: u8}
event Jumped { From: Client, Type: Reliable, Call: SingleSync, Data: u8}-- An event declared `From: Client` is listened to on the server, so Predict lives there.Net.Jumped.Predict(Player, 5)-- An event declared `From: Server` is listened to on the client.Net.Damaged.Predict(5)It takes exactly what a listener receives and is delivered the way an arriving event is: to every
listener of a Many event, and, for a reliable event with no listener connected yet, into the queue
that is handed over when one connects. It does not pass through the event’s rate limit – nothing
arrived from a client.
Two uses. Tests can drive receiving code without standing up a network, and Roblox stories – which
run in edit mode, where no remotes exist – can exercise UI that reacts to events. A polled event has
no Predict, since it has no listeners to hand the event to.
PackedBooleanReader
Section titled “PackedBooleanReader”Default: "Auto"
Values: "Auto", "Bits", "Rows"
How a received boolean[] is unpacked. Two ways
are fastest in different places:
"Bits"tests each bit. Natively compiled code, which a Roblox server runs, inlines the test, and nothing measured faster there."Rows"copies a byte’s eight elements from a table of 256 rows. Interpreted code, which most players’ clients run, decodes a thousand 1000-element arrays about 30% faster this way, and native code about a third slower.
"Auto" emits both, and the module measures which is faster as it loads – both read the same 1024
elements eight times, which takes tens of microseconds, once – then uses that one. The server, which
is compiled natively, and the client, which is not, each get the faster one. "Bits" or
"Rows" fixes the choice and emits only that reader. The wire is the same whichever is chosen, so
the two sides may differ. A schema without a boolean[] emits neither.
option PackedBooleanReader = "Bits"Starting up
Section titled “Starting up”Requiring the server module creates the remotes; requiring the client module waits for them. Two options change that, for places where no server module is ever required – a showroom build, a test place. Places without a server walks through both.
ClientConnectTimeout
Section titled “ClientConnectTimeout”Default: none – the client waits for the remotes for as long as it takes
Without it, a client still waiting after 5 seconds warns once, saying the server module creates the remotes and naming this option, then keeps waiting.
How many seconds the client module waits for the two remotes. Any number above zero is taken.
option ClientConnectTimeout = 10If they have not appeared by then, requiring the client module still returns, with Connected
false and the edit-mode stubs in place of the API:
Fire does nothing, a listener is never called, and Invoke fails at once with
The client is not connected to a server. When they appear in time, Connected is true and the
module is the ordinary one. Connected exists only under this option.
Remotes that appear after the timeout are not picked up. The module has been returned and cached by
require, and a client that joined in half way would have missed what the server sent before it; a
game that wants to connect later has to reload. The schema signature is still checked once the remotes
are found, and a mismatch still errors.
AutoStart
Section titled “AutoStart”Default: true
AutoStart = false makes requiring the server module free of side effects on the network: it
creates no remote and connects neither the remotes nor Heartbeat. The game calls Start() in the
build that should talk, and code shared with a build that should not can require the module anyway.
option AutoStart = falselocal Net = require(ServerScriptService.Network.Server)
if not IsShowroom then Net.Start()endStart is idempotent: the first call creates the remotes, connects them and the Heartbeat, and a
second call does nothing. Until it is called:
On,SetRateLimitHandlerand the other setters register as usual, and take effect atStart.- Every
Fireis dropped, not queued, and the first one warns once:Something was sent before Start() was called, and was dropped.A build that never starts would otherwise hold every player’s events for as long as they stayed, and an event queued for a minute is rarely still true when it arrives. Invokeon a server-to-client function fails at once withCannot invoke <player> before Start() was called, rather than waiting outInvocationTimeout.
The option concerns the server module only. The client creates nothing; its counterpart is
ClientConnectTimeout.
Validation
Section titled “Validation”WriteValidations
Section titled “WriteValidations”Default: false
Check what you pass to Fire and Invoke before it is written: that each value has the right Luau
type, that numbers, string lengths, buffer sizes, array lengths and vector magnitudes are inside their
ranges, and that an Instance is of its declared class.
Helpful while developing; it costs a comparison per field, so it is commonly enabled only in
development builds.
@profile("dev")option WriteValidations = trueWhat it does not switch is anything the receiver relies on. Whatever this option says:
- The receiving side always checks ranges and Instance classes on what it decodes, and drops a packet that fails.
- A string, buffer or array length, or a map’s entry count, outside its bounds is always refused on send. The length prefix is sized from the bounds, so such a value would not fit it: it would go out with a wrapped length and every event behind it would decode from the wrong offset.
- A fixed-length array (
u8[4]) or exact-length string (string(4)) sends no length prefix, so a longer value cannot corrupt the packet, and this option is what decides its fate. WithWriteValidations, a value longer (or shorter) than its length is refused on send. Without it, a longer value is cut to its first elements or bytes and sent, silently. - An enum value or tagged-enum variant that does not exist always errors on send.
SyncValidation
Section titled “SyncValidation”Default: true
Reports a Sync listener that yielded.
It is worth knowing exactly when it fires, because the obvious guess is wrong. BlinkBlox does not
notice at the moment your listener yields. It notices when the next packet arrives and finds the
previous decode still suspended inside one. So a listener that yields with no further traffic behind
it is never reported – which is why adding a yield to a SingleSync callback and seeing no error is
a correct observation rather than a broken check.
The check costs one coroutine.status per packet, which is why it is on by default.
MaxUnreliableSize
Section titled “MaxUnreliableSize”Default: 980 bytes
The largest payload an unreliable event may send: its buffer, counting its one-byte event id and,
for OrderedUnreliable other than a stream, its two-byte sequence number, plus 6 bytes for each
Instance sent beside it.
Roblox drops an UnreliableRemoteEvent whose payload is over
1000 bytes. Studio’s
Output reports it, but a live game drops the event silently, and only when a value happens to be
large. So the failure shows up in production for some players and never in testing. That payload is
everything the call carries, as Roblox encodes it: the buffer, the table of instances sent beside it,
and the bytes that describe each. Roblox does not say what the rest costs, so
benchmark/Probe.luau
measured it in Studio, the same in both directions:
| Instances | Largest buffer delivered |
|---|---|
| 0 | 994 |
| 1 | 988 |
| 4 | 970 |
| 16 | 898 |
So the call itself costs 6 bytes and each Instance 6 more, and that is what this option counts. The default of 980 keeps 14 bytes of the measured 994 in reserve, against the encoding changing between Roblox versions.
BlinkBlox checks this twice. At compile time it measures every unreliable event: one that can never
fit is a hard error (E3018), and one that merely might not is a warning (W3019) naming the
field that has no upper bound. At runtime the send path checks the finished buffer, and drops an event
that is too large with a warning naming it rather than letting the packet vanish.
option MaxUnreliableSize = 980Inbound limits
Section titled “Inbound limits”These bound what a single client can make the server do, and all of them are checked before a packet is decoded. Only inbound server traffic is checked: the client trusts the server by construction, and capping what the server sends would drop legitimate replication. The securing the server guide puts them together with the rest.
MaxPacketSize, MaxEventsPerPacket, MaxInstancesPerPacket
Section titled “MaxPacketSize, MaxEventsPerPacket, MaxInstancesPerPacket”Defaults: 8192 bytes, 64 events, 256 instances
Bound what a single inbound packet can make the server do.
The server decodes a client’s buffer in a loop, one event per iteration. Without a limit, a client
that sends buffer.create(1000000) buys up to a million decode-and-dispatch cycles inside one remote
callback – enough to stall or crash the server.
A packet larger than MaxPacketSize, or carrying more instance references than
MaxInstancesPerPacket, is dropped before any of it is parsed. Once MaxEventsPerPacket events have
been decoded from one packet, the rest is discarded. An unrecognised event id also stops the parse:
the cursor is no longer at an event boundary, so nothing after it can be trusted.
option MaxPacketSize = 16384option MaxEventsPerPacket = 128option MaxInstancesPerPacket = 512Before any of that, both remotes check that what arrived is a buffer and a table at all – a client
can call FireServer with any arguments it likes. Each kind of refusal is reported at most once a
second per player, so a client sending garbage every frame cannot flood the output: to the
packet-drop handler when the game installed
one, as a warning when it did not.
MaxInstancesPerPacket may be 0, the one limit that can: a game whose clients never send an
Instance or unknown says so, and the server refuses every packet that carries one before reading
it. A client-sent declaration that carries one – the data of a From: Client event or function, or
the return of a From: Server function – is then a compile error (E3031), since none of its
packets could arrive. Every other numeric option is at least 1.
option MaxInstancesPerPacket = 0InboundBytesPerSecond, InboundBurst
Section titled “InboundBytesPerSecond, InboundBurst”Defaults: 8 * MaxPacketSize (65536) bytes a second, and a burst of one second of that or one
MaxPacketSize, whichever is larger
Bound how many bytes a second each player’s packets may cost the server, before any of them is decoded.
The limits above are per packet. A client firing hundreds of small, well-formed packets a frame passes
every one of them, and the server would decode them all. The budget is a token bucket per player,
shared by the reliable and the unreliable remote: each packet costs its size in bytes, and never less
than 128, so a flood of tiny packets is not free. A packet that finds the bucket short is dropped
whole and unread.
option InboundBytesPerSecond = 131072option InboundBurst = 131072At the defaults that is eight full-size packets a second, or 512 small ones. The default rate follows
MaxPacketSize, so raising that raises the budget with it. InboundBurst must be at least
MaxPacketSize, and never below 128 (E2003 otherwise), or the largest packet could never be
afforded however long the client waited. The burst is a whole second because the clock is the server’s: after a network hitch,
Roblox delivers the packets that queued up together, and a refused reliable packet takes every event
in it along.
Refusals are reported at most once per player per second. With a
packet-drop handler installed they go to it,
with the reason Budget and the refused packet’s length. Without one they are reported the way
refused events are: to SetRateLimitHandler’s handler
with Event set to nil, or, with no handler either, as a warning.
Outbound budget
Section titled “Outbound budget”OutboundBytesPerSecond, OutboundBurst
Section titled “OutboundBytesPerSecond, OutboundBurst”Defaults: none, and a burst of one second of the rate
Bound how many bytes a second the server sends each player, by leaving out what the schema marked as expendable.
option OutboundBytesPerSecond = 20000option OutboundBurst = 40000Every byte the server sends a player is charged to that player’s bucket: the reliable batch, the
unreliable batches, every unreliable send, and each player’s share of a send to everyone. Nothing is
refused for it but a Priority: Low event or stream, which is
left out while the player’s bucket is empty. Everything else still goes, and still spends: a frame of
reliable traffic that overdraws the bucket keeps the low sends out until it has been paid back.
This is for a game that sends more than a slow connection takes. Roblox queues reliable traffic a player cannot keep up with, and their ping climbs with it; the budget gives the bytes to the events that matter first. A server that stays under what its players can take never notices it.
Without the option the module carries no budget at all, and a send costs what it always did.
Rate limits
Section titled “Rate limits”DefaultRate, RequireRates
Section titled “DefaultRate, RequireRates”Defaults: none, false
Rate limiting for inbound events and functions.
Per-event limits are declared on the event itself with
Rate and Burst. DefaultRate supplies a rate, in events
per second per player, for every inbound event and function that does not declare one. RequireRates
turns a missing rate into a compile error (E3020) – the flag a project sets so that a forgotten
limit fails the build instead of shipping. With a DefaultRate, nothing is missing.
option DefaultRate = 30option RequireRates = trueUnlike an event’s own Rate, which may be fractional, DefaultRate is a whole number like every
numeric option.
The packet limits, the inbound budget and this are different axes and none replaces the others.
MaxEventsPerPacket bounds the work one packet can cause; the inbound budget bounds how many bytes of
packets a second each player gets decoded at all; the event bucket bounds events per second, per
player, per event, and so the game logic behind each listener.
Invocations
Section titled “Invocations”InvocationTimeout
Section titled “InvocationTimeout”Default: 10 seconds
How long an Invoke waits for its answer before failing.
A call that is never answered would otherwise wait forever, and it costs more than the one hung
thread: each outstanding invocation holds one of thirty-two slots, and a call that never returns never
gives its slot back. Thirty-two of those and every later Invoke is refused outright.
When the timeout fires, the caller is failed the same way an errored handler fails it – so a pcall
around Invoke, or a rejected promise, catches it – the slot is returned, and BlinkBlox warns naming
the function. The timer belongs to that one call: a call answered in time cancels it, so it can never
fail a later call, and an answer that arrives after the timeout is ignored rather than handed to
whichever call came next.
option InvocationTimeout = 30Diagnostics
Section titled “Diagnostics”TrafficStats
Section titled “TrafficStats”Default: false
Counts what each module sends and receives, per event and per channel, and adds two members to the
server and client modules: GetTrafficStats() and ResetTrafficStats(). Roblox reports only the
game’s total (Stats.DataSendKbps); this says how much of it is BlinkBlox’s and which event costs
the most.
option TrafficStats = true
event Chat { From: Client, Type: Reliable, Call: ManySync, Data: string(0..200) }local Stats = Net.GetTrafficStats()print(Stats.Events.Chat.Received, Stats.Events.Chat.ReceivedBytes, Stats.Reliable.ReceivedBytes)The counters are integer additions into one table the module allocates when it loads, at indices fixed at compile time: sending or receiving an event allocates nothing and looks no string up. An event whose every value has the same size counts only how many were sent, and the bytes are worked out when you ask. With the option off – the default – the module is exactly what it would be without this option, not one line longer.
The returned shape, what counts as a byte and how a broadcast is counted are on the Generated API page; how to use it to find what is expensive is in Bandwidth. The option does not change the wire, so a module built with it talks to one built without.
Deprecated
Section titled “Deprecated”UseColon
Section titled “UseColon”Accepted and ignored: no generator has read it since it was added. It warns (W3017,
Option "UseColon" is deprecated) and will be removed in 1.0. Delete the line.
Schema signatures
Section titled “Schema signatures”Not an option – it is always on, and there is nothing to configure. It is documented here because you will meet its error message.
Every generated pair carries a short signature of the schema it was built from. The server publishes it on the reliable remote; the client checks it before anything else and errors on require if they disagree:
[BlinkBlox]: This client was built from a different schema than the server(client 18198d53..., server 64b832be...). Recompile both sides from the same .blink file.This exists because the alternative failure is silent. Event ids are positional – the first event
declared is 0, the next is 1 – so inserting an event renumbers every event after it. A client
one build behind then reads index 7 as whatever index 7 means now and dispatches one event’s
payload to another’s listener. Nothing errors. The usual symptoms are
invalid argument #3 to 'writeu8' or a listener that receives data it cannot make sense of.
A profile changes which declarations are compiled, so the client and the server must be built with the same one.
If you see the “did not publish a schema signature” variant, the server is running a build from before this check existed, or something other than BlinkBlox created a remote with the same name.
It is a safety check, not a security one: the client owns its copy of the module and can edit the constant out. It catches drift between two builds, which is a mistake, not an attack.
