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). 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
FutureLibrary, PromiseLibrary Luau path none Output
ManualReplication boolean false Runtime behaviour
UsePolling boolean false Runtime behaviour
Predict boolean false Runtime behaviour
WriteValidations boolean false Validation
SyncValidation boolean true Validation
MaxUnreliableSize bytes 900 Validation
MaxPacketSize bytes 8192 Inbound limits
MaxEventsPerPacket events 64 Inbound limits
MaxInstancesPerPacket instances 256 Inbound limits
InboundBytesPerSecond bytes 8 * MaxPacketSize Inbound limits
InboundBurst bytes one second of the above Inbound limits
DefaultRate events a second none Rate limits
RequireRates boolean false Rate limits
InvocationTimeout seconds 10 Invocations
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 on, invoke, predict, iter on, invoke, predict, iter
Read, Write read, write read, write
StepReplication stepReplication step_replication
SetRateLimitHandler setRateLimitHandler set_rate_limit_handler
SetDecodeErrorHandler setDecodeErrorHandler set_decode_error_handler

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.

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.

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: 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: 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.
  • 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: 900 bytes

The largest payload an unreliable event may carry, counting its one-byte event id and, for OrderedUnreliable, its two-byte sequence number.

Roblox drops an UnreliableRemoteEvent payload past roughly this size. It does so silently, and only under the conditions that produce a large packet, so the failure shows up in production for some players and never in testing.

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

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 warns at most once a second per player, so a client sending garbage every frame cannot flood the output.

Defaults: 8 * MaxPacketSize (65536) bytes a second, and a burst of one second of that

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 (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 the way refused events are: a warning at most once per player per second, and a call to SetRateLimitHandler’s handler with Event set to nil.

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

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.