Skip to content

Functions

Functions are BlinkBlox’s version of Roblox’s RemoteFunction. They let one side ask the other a question and wait for the answer. Unlike a RemoteFunction, a call always ends: it is answered, or it fails – the listener errored, the call was refused, the player left, or nobody answered within InvocationTimeout.

Functions are declared with the function keyword and a block of fields.

function GetBalance {
Yield: Coroutine,
Data: u8,
Return: u32
}

Yield is required. From, Data, Return, Rate, Burst and Concurrency are optional.

Calls and replies travel on the reliable channel, batched with reliable events and sent once a frame, so a call and its answer each wait for the next flush on their side on top of the network’s own latency.

Default: Client

The side that invokes. The other side listens and answers.

-- The client asks the server. The default, and what every function did before `From` existed.
function GetBalance {
Yield: Coroutine,
Data: u8,
Return: u32
}
-- The server asks a client, and names which one.
function GetClientSetting {
From: Server,
Yield: Coroutine,
Data: u8,
Return: string
}
Server.luau
local Answer = Net.GetClientSetting.Invoke(Player, 1)
Client.luau
Net.GetClientSetting.On(function(Which)
return Settings[Which]
end)

A client can only answer a call that was made to it, for the function it was made for: the server keeps each player’s calls apart, so a reply naming a call that belongs to somebody else, or answering one function with another’s return type, is ignored.

How the caller waits for the answer.

  • Coroutine – the built-in Luau coroutine library. Invoke yields and returns the answer, or throws if the call failed.
  • Future – a future library you provide with option FutureLibrary. redblox’s Future library is recommended. Invoke returns a future built with Future.Try, which completes with a success flag followed by the answer.
  • Promise – a promise library you provide with option PromiseLibrary. evaera’s Promise library, or a fork of it, is recommended. Invoke returns a promise that resolves with the answer or rejects if the call failed. Cancelling it gives its slot back.

A schema that uses Future or Promise without the matching option fails to generate.

The data the caller sends with the call – the client, unless the function is From: Server, in which case it is the server. Any type, or a type pack. Omit it when the call carries nothing.

The data the listener sends back to the caller – the server, unless the function is From: Server, in which case it is the client. Any type or type pack, and omitted when the answer carries nothing.

Data and Return may name their type pack elements the same way – Data: (id: u8, label: string) alongside Return: (id: u16, label: string) is fine. The generated code keeps the two sets apart, so there is no need to invent distinct names for the same thing going each way.

function Rename {
Yield: Coroutine,
Data: (id: u8, label: string(1..32)),
Return: (id: u8, label: string(1..32))
}

Limits how often one player may invoke this function, exactly as on events. They apply only to a From: Client function – the default. On a From: Server function they are ignored with a warning, exactly as on a From: Server event: rate limiting applies to inbound traffic, and the server is the party being protected.

A refused invocation is answered with a failure rather than dropped, so the caller raises promptly instead of waiting out the timeout.

How many of one player’s calls to this function the server runs at once. Rate limits how many calls start each second; it says nothing about how many are still running. A listener that waits – a DataStore request, an HTTP call, an invocation back to the same client – holds each call open for as long as the wait, so calls arriving at an allowed rate can still pile up into hundreds of suspended threads, each holding whatever its listener holds. A client that sends the requests itself is not held to the thirty-two calls in flight an honest client module is.

function SaveLoadout {
Yield: Coroutine,
Rate: 2,
Concurrency: 1,
Data: u8[..8],
Return: boolean
}

With Concurrency: 1, a second SaveLoadout from a player whose first is still saving is refused. It is answered with a failure straight away, like a call refused by Rate, and reported through the same rate-limit handler, once a second per player. The place is given back when the call is answered – whether the listener returned, threw, or returned the wrong type – and a call waiting in the queue for a listener that has not connected yet counts as running. When the player leaves, their count goes with them.

Concurrency is a whole number of at least 1. Like Rate, it applies to a From: Client function and warns (W3020) on a From: Server one.

The module holds one table per function, named after it. The caller’s side gets Invoke, the listener’s side gets On; the names follow Casing.

Client.luau (Coroutine)
local Ok, Balance = pcall(Net.GetBalance.Invoke, 1)
Client.luau (Future)
local Success, Balance = Net.GetBalance.Invoke(1):Await()
Client.luau (Promise)
Net.GetBalance.Invoke(1)
:andThen(function(Balance)
-- ...
end)
:catch(warn)

On the server, Invoke of a From: Server function takes the player to ask first: Net.GetClientSetting.Invoke(Player, 1).

Server.luau
Net.GetBalance.On(function(Player, Account)
return Balances[Player][Account]
end)

The listener receives the calling Player first on the server; on a client, answering a From: Server function, it receives only the data. Each call runs on a thread of its own, so the listener may yield – a datastore read, say – and whatever it returns is the answer.

A function has one listener. On returns nothing, and calling it again replaces the listener.

Calls that arrive before a listener connects are queued, up to 256 on the server, and run when it does. Past that the server answers each further call with a failure, so the caller’s Invoke raises promptly instead of hanging. A client’s queue is filled by the server, and warns once it passes 256 instead.

Every failure reaches the caller the same way: a Coroutine invoke throws There was an exception while processing "Name"., a Future completes with false, and a Promise rejects. What causes it:

Cause What else happens
The listener threw The listening side warns "Name" encountered an error, ... with the error.
The listener returned something its Return type does not allow The same warning. The reply is taken back out of the batch rather than sent half-written.
The call was refused by Rate Reported through the rate-limit handler.
The player already had Concurrency calls running Reported through the same handler, with the function’s name.
The server’s queue for an unconnected listener was full Nothing is reported.
Nobody answered within InvocationTimeout The caller’s side warns, naming the function and, on the server, the player.
The player being asked left the game Every call outstanding to them fails at once rather than timing out.

A late answer, arriving after its call has already failed, is ignored.

Some calls fail before anything is sent, and throw from Invoke itself (or reject, or complete with false, for the other yield types):

  • The arguments do not serialise – see WriteValidations.
  • The server invokes a player who is no longer in the game.
  • Thirty-two calls are already in flight.

A call is known by a one-byte id that its answer carries back, and at most 32 calls may be outstanding at once – per client module for calls a client makes, and per player for calls the server makes, so asking every player a question at once does not exhaust anything. A thirty-third concurrent call throws 32 calls are already awaiting a response, this call has been dropped.

A slot is given back when its call is answered, fails, times out or – for a promise – is cancelled, so reaching the cap takes thirty-two calls genuinely stuck at once. Without the timeout they would be stuck forever; with it, the worst case is InvocationTimeout seconds, ten by default.