Skip to content

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 option after 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 set MaxPacketSize, but not alongside an unmarked setting of it.
  • Values are typed. A boolean option takes true or false, a path takes a quoted string, Casing takes a bare word, and every numeric option takes a positive whole number – 0, -5 and 0.5 are all refused (E2003), except that MaxInstancesPerPacket may be 0 and ClientConnectTimeout takes any number of seconds above zero, 0.5 included. An unknown option name is refused too, with the list of the ones that exist.
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

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.

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 = true

See roblox-ts for the full setup.

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 = Camel
Pascal 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

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.

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.

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 -- 16
Net.Limits.Tally.Count.Min -- 1
Net.Limits.Tally.Frame.Max -- 6553.5
Net.Limits.Tally.Frame.Step -- 0.1
Net.Limits.Crawls.Heads.Length.Max -- 16, the event's data

The 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.

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 second
Net.Rates.Report.Burst -- 4, at once
Net.Rates.Tap.Burst -- 20: none was written, so the default
Net.Rates.Search.Concurrency -- 1
Net.Rates.Score -- nil: the server sends it, and nothing limits that

The 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.

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.

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 = true
Server.luau
local RunService = game:GetService("RunService")
local Net = require(path.to.Server)
RunService.PostSimulation:Connect(function()
-- ... fire this frame's events ...
Net.StepReplication()
end)

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 = 120

It 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.

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 = true

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 = true

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
}
Server.luau
-- An event declared `From: Client` is listened to on the server, so Predict lives there.
Net.Jumped.Predict(Player, 5)
Client.luau
-- 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.

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"

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.

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 = 10

If 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.

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 = false
Server.server.luau
local Net = require(ServerScriptService.Network.Server)
if not IsShowroom then
Net.Start()
end

Start is idempotent: the first call creates the remotes, connects them and the Heartbeat, and a second call does nothing. Until it is called:

  • On, SetRateLimitHandler and the other setters register as usual, and take effect at Start.
  • Every Fire is 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.
  • Invoke on a server-to-client function fails at once with Cannot invoke <player> before Start() was called, rather than waiting out InvocationTimeout.

The option concerns the server module only. The client creates nothing; its counterpart is ClientConnectTimeout.

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 = true

What 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. With WriteValidations, 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.

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.

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 = 980

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 = 16384
option MaxEventsPerPacket = 128
option MaxInstancesPerPacket = 512

Before 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 = 0

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 = 131072
option InboundBurst = 131072

At 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.

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 = 20000
option OutboundBurst = 40000

Every 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.

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 = 30
option RequireRates = true

Unlike 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.

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 = 30

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) }
Server.luau
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.

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.

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.