Packet
A buffer-based networking library. Instead of one RemoteEvent per message, you declare
named packets with a fixed list of value types; every Fire is serialised into a shared
buffer, and everything queued during a frame is flushed through a single RemoteEvent.
This gives far smaller payloads than sending Lua tables, and makes the shape of every
message explicit.
The module must live somewhere both sides can require (for example ReplicatedStorage):
the server creates the RemoteEvent inside the module and the client waits for it.
Declaring packets
Packet.new(name, ...types) declares a packet. Declare the same packet with the same name
and types on the server and on the client (a shared ModuleScript is the easiest way).
Declaring a name twice returns the existing packet. The value types are read from the
module itself, e.g. Packet.NumberU8, Packet.String, Packet.Vector3F32.
Plain Luau tables describe structured values:
-
{ T }(a table holding exactly one type) is a variable-length array ofT(up to 65535 elements, 2 bytes of length prefix). -
Any other table, e.g.
{ Name = Packet.String, Level = Packet.NumberU8 }or{ Packet.NumberU8, Packet.NumberU8 }, is a fixed table: every key is always sent, in sorted key order, with no per-field overhead. All keys must be of one kind (all strings or all numbers), and tables can be nested.
Value types
Numbers are not range-checked: writing a value outside the range of its type corrupts it, so pick the smallest type that fits.
| Type | Bytes | Range / notes |
|---|---|---|
NumberS8 |
1 | -128 to 127 |
NumberS16 |
2 | -32768 to 32767 |
NumberS24 |
3 | -8388608 to 8388607 |
NumberS32 |
4 | -2147483648 to 2147483647 |
NumberU8 |
1 | 0 to 255 |
NumberU16 |
2 | 0 to 65535 |
NumberU24 |
3 | 0 to 16777215 |
NumberU32 |
4 | 0 to 4294967295 |
NumberF16 |
2 | Half float; integers exact up to ±2048, largest value ±65520 |
NumberF24 |
3 | Custom float; integers exact up to ±262144, largest value ±4294959104 |
NumberF32 |
4 | Single float; integers exact up to ±16777216, largest value about ±1.7e38 |
NumberF64 |
8 | Double float; integers exact up to ±9007199254740992, no practical limit |
NumberU4 |
1 | {number, number}, two values from 0 to 15 packed in one byte |
Boolean8 |
1 | A single boolean |
Boolean1 |
1 | {boolean}, exactly eight booleans packed into one byte |
BooleanNumber |
1 | {Boolean: boolean, Number: number}, the number from 0 to 127 |
String |
1 + n | string of up to 255 bytes |
StringLong |
2 + n | string of up to 65535 bytes |
Characters |
1 + 6 bits/char | string of up to 255 characters using only space, ., 0-9, A-Z, a-z (see Types/Characters.luau) |
Buffer |
1 + n | buffer of up to 255 bytes |
BufferLong |
2 + n | buffer of up to 65535 bytes |
Instance |
0 | Instance, sent alongside the buffer; arrives as nil if the receiver cannot see it |
Nil |
0 | Always nil |
Any |
1 + value | Any supported value, with a 1-byte type tag: nil, number, string (255 bytes max), buffer (255 bytes max), Instance, boolean, NumberRange, BrickColor, Color3, UDim, UDim2, Rect, Vector2, Vector3, CFrame, Region3, NumberSequence, ColorSequence, EnumItem (listed enums only) and tables whose keys and values are any of these |
NumberRange |
8 | Min and Max as F32 |
BrickColor |
2 | Any BrickColor |
Color3 |
3 | 8 bits per channel |
UDim |
4 | Scale to 3 decimal places (±32.767), Offset -32768 to 32767 |
UDim2 |
8 | Two UDims as above |
Rect |
16 | Min and Max as F32 pairs |
Vector2S16 |
4 | Components -32768 to 32767, integers only |
Vector2F24 |
6 | Components as F24 |
Vector2F32 |
8 | Components as F32 |
Vector3S16 |
6 | Components -32768 to 32767, integers only |
Vector3F24 |
9 | Components as F24 |
Vector3F32 |
12 | Components as F32 |
CFrameF24U8 |
12 | Position as F24, rotation as three 8-bit Euler angles (about 1.4 degree steps) |
CFrameF32U8 |
15 | Position as F32, rotation as three 8-bit Euler angles (about 1.4 degree steps) |
CFrameF32U16 |
18 | Position as F32, rotation as three 16-bit Euler angles (about 0.006 degree steps) |
Region3 |
24 | Min and Max corners as F32 |
NumberSequence |
1 + 3n | Up to 255 keypoints; Time, Value and Envelope quantised to 1/255 (0 to 1 only) |
ColorSequence |
1 + 4n | Up to 255 keypoints; Time quantised to 1/255, colour 8 bits per channel |
EnumItem |
3 | Any item of an Enum listed in Types/Enums.luau (add more there, 255 max) |
Static1, Static2, Static3 |
1 | A value from the matching Types/Static1.luau (etc.) list of up to 255 constants of any type; values not in the list arrive as nil |
Events and requests
A packet without a response is an event: Fire on the client sends to the server and
OnServerEvent fires there with the player first; Fire on the server broadcasts to every
client and FireClient(player, ...) sends to one, firing OnClientEvent. Sends are
batched and flushed once per frame, so Fire never yields for an event.
Calling :Response(...types) on a packet (on both sides) turns it into a request: the
receiving side must set OnServerInvoke / OnClientInvoke, and Fire (client) or
FireClient (server) yields until the reply arrives or ResponseTimeout seconds pass,
then returns the reply or ResponseTimeoutValue. Request packets do not fire the event
signals.
The server drops incoming data from a player that exceeds roughly 8 KB per frame (each received RemoteEvent call counts as at least 800 bytes), so keep client sends small.
Example
-- ReplicatedStorage/Packets.luau, required by both sides
local Packet = require(ReplicatedStorage.Packages.Packet)
return {
-- client -> server: which hotbar slot was used and where the player aimed
UseItem = Packet.new("UseItem", Packet.NumberU8, Packet.Vector3F32),
-- server -> clients: an array of fixed tables
Scores = Packet.new("Scores", {{Name = Packet.String, Score = Packet.NumberU16}}),
-- client -> server request; the server answers with an array of strings
GetInventory = Packet.new("GetInventory"):Response({Packet.String}),
}
-- Server
local Packets = require(ReplicatedStorage.Packets)
Packets.UseItem.OnServerEvent:Connect(function(player, slot, aimPosition)
print(player.Name, "used slot", slot, "towards", aimPosition)
end)
Packets.GetInventory.OnServerInvoke = function(player)
return {"Sword", "Shield"}
end
Packets.Scores:Fire({{Name = "Kash", Score = 10}, {Name = "Ana", Score = 7}})
-- Client
local Packets = require(ReplicatedStorage.Packets)
Packets.Scores.OnClientEvent:Connect(function(scores)
for _, entry in scores do print(entry.Name, entry.Score) end
end)
Packets.UseItem:Fire(1, workspace.CurrentCamera.CFrame.LookVector * 100)
local inventory = Packets.GetInventory:Fire() -- yields until the server replies (10 s timeout)
Credits: Packet was created by Suphi Kaner (original release on the DevForum). This build is his library with a few quality-of-life changes by KashTheKing, published to Wally as kashtheking/packet so it can be installed like the rest of the library. The bundled Signal, Task and Types modules are his as well. All credit for the design and the serialization engine goes to Suphi.
Installation and guide: Packet package page.
Properties
Name
This item is read only and cannot be modified. Read OnlyPacket.Name: stringThe name the packet was declared with. Packets are cached by name, so declaring the same name again returns the same object.
Id
This item is read only and cannot be modified. Read OnlyPacket.Id: number
The one-byte id written at the start of every message for this packet. The server assigns
it in declaration order and publishes it as an attribute on the RemoteEvent; on the client
it is nil until the server has declared a packet with the same name.
ResponseTimeout
Packet.ResponseTimeout: number
How many seconds Fire / FireClient wait for a reply on a request packet before giving
up. Response sets it to 10 unless you assigned a value first; you can change it at any
time.
ResponseTimeoutValue
Packet.ResponseTimeoutValue: anyWhat Fire / FireClient return when a request times out. Defaults to nil.
OnServerEvent
This item only works when running on the server. ServerThis item is read only and cannot be modified. Read Only
Fires on the server when a client Fires this event packet. Handlers receive the sending
player followed by the packet's values. The signal has Connect, Once and Wait;
Connect returns a connection with Disconnect. Not used by request packets.
OnClientEvent
This item only works when running on the client. ClientThis item is read only and cannot be modified. Read OnlyPacket.OnClientEvent: Signal<A...>
Fires on the client when the server Fires or FireClients this event packet. Handlers
receive the packet's values. The signal has Connect, Once and Wait; Connect returns
a connection with Disconnect. Not used by request packets.
OnServerInvoke
This item only works when running on the server. Server
Callback the server must set on a request packet. It receives the requesting player and
the packet's values and returns the response values. If it is nil when a request arrives
the request is discarded (with a warning in Studio) and the client waits for its timeout.
OnClientInvoke
This item only works when running on the client. ClientPacket.OnClientInvoke: ((A...) → B...)?
Callback the client must set on a request packet sent with FireClient. It receives the
packet's values and returns the response values. If it is nil when a request arrives the
request is discarded with a warning and the server waits for its timeout.
Functions
new
Packet.new(name: string,--
A unique name for the packet, shared by both sides.
...: any--
The value types of the packet, in order (none for a packet without data).
) → Packet<A...,B...>--
The packet object.
Declares a packet, or returns the one already declared with that name. Declare it on both
the server and the client with identical types. The types are the Packet.* value types
listed in the class description, or Luau tables of them for arrays and fixed tables.
On the server this assigns the packet's Id and publishes it to clients; on the client the
packet is inert until the server has declared the same name. This is the only way to
declare a packet: the module table itself is not callable.
local Damage = Packet.new("Damage", Packet.Instance, Packet.NumberU16)
local Spawn = Packet.new("Spawn", {Position = Packet.Vector3F32, Skin = Packet.NumberU8})
local Ping = Packet.new("Ping"):Response(Packet.NumberF32)
newParams
Packet.newParams(...: any--
The value types to bundle, in order.
) → () → ...any--
A function that returns the bundled types.
Bundles a list of value types into a function that returns them, so the same parameter
list can be reused across several packets or shared between a request and its response.
Call the returned function inside Packet.new or Response to expand it.
local Position = Packet.newParams(Packet.NumberU8, Packet.Vector3F32)
local Move = Packet.new("Move", Position())
local Teleport = Packet.new("Teleport", Position()):Response(Position())
Response
Packet:Response(...: any--
The value types of the response, in order (see the class description).
) → Packet<A...,B...>--
The same packet, for chaining.
Declares the response types of the packet, turning it into a request/response packet.
Call it on both sides, right after Packet.new, with the same syntax used for the packet's
own types. Once a packet has a response, Fire (client) and FireClient (server) yield for
the reply, the receiving side handles it through OnServerInvoke / OnClientInvoke, and
the event signals are no longer fired.
Also sets ResponseTimeout to 10 if it has not been set yet.
local GetStats = Packet.new("GetStats", Packet.NumberU16):Response(Packet.NumberU32, Packet.String)
Fire
This is a yielding function. When called, it will pause the Lua thread that called the function until a result is ready to be returned, without interrupting other scripts. YieldsPacket:Fire(...: A...--
The values to send, matching the types given to Packet.new.
) → B...--
The response values (request packets only); nothing for event packets.
Sends the packet. Which way it goes depends on where it is called:
-
Client: sends to the server. For an event packet this returns immediately and
OnServerEventfires on the server. For a request packet it yields until the server'sOnServerInvokereply arrives and returns those values, or returnsResponseTimeoutValueafterResponseTimeoutseconds. -
Server: broadcasts an event packet to every connected client (
OnClientEventfires on each). Request packets cannot be broadcast; useFireClientinstead.
Sends are queued and flushed once per frame in a single RemoteEvent call, so nothing is transmitted until the end of the current frame.
Errors
| Type | Description |
|---|---|
| "You must use FireClient(player)" | When called on the server for a request packet. |
| "Cannot have more than 128 yielded threads" | When 128 requests from this client are already waiting for a reply. |
FireClient
This item only works when running on the server. ServerThis is a yielding function. When called, it will pause the Lua thread that called the function until a result is ready to be returned, without interrupting other scripts. YieldsPacket:FireClient(...: A...--
The values to send, matching the types given to Packet.new.
) → B...--
The response values (request packets only); nothing for event packets.
Sends the packet to one client. For an event packet this returns immediately and
OnClientEvent fires on that client. For a request packet it yields until the client's
OnClientInvoke reply arrives and returns those values, or returns ResponseTimeoutValue
after ResponseTimeout seconds. If the player has already left the game nothing is sent
and the call returns immediately with no values.
Like Fire, the data is queued and flushed at the end of the frame.
Errors
| Type | Description |
|---|---|
| "Cannot have more than 128 yielded threads" | When 128 requests to this player are already waiting for a reply. |
Serialize
Packet:Serialize(...: A...--
The values to encode, matching the types given to Packet.new.
) → (buffer,--
The encoded bytes, sized exactly to the data.
{Instance}?--
The Instances that were referenced, in order; only returned when at least one Instance was written.
)
Encodes values with this packet's types into a standalone buffer without sending
anything, using the same format as Fire. Useful for compact storage (for example in a
DataStore) or for sending through your own remotes. Works on either side.
local SaveData = Packet.new("SaveData", Packet.NumberU32, {Packet.String})
local data = SaveData:Serialize(1500, {"Sword", "Shield"})
local coins, items = SaveData:Deserialize(data)
Deserialize
Packet:Deserialize(serializeBuffer: buffer,--
A buffer returned by Serialize.
) → A...--
The decoded values, in the order the packet declares them.
Decodes a buffer produced by Serialize on a packet with the same types, returning the
original values. Works on either side.