Skip to main content

Packet Format And Insights

Strings are great for serializing data, but we need to know when to serialize. Certain kinds of data is cheaper to send over the network than others. This page is very technical, as it introduces the raw binary format remotes use internally and discusses the overhead of each type of data. If you are looking for a more high-level overview of the topic, check out the Why SerDes? or How To SerDes? pages.

How Are Packets Sent And Recieved?

Roblox remotes sends its data in the form of packets. Every roblox packet is packed densely with data to reduce their size. Every byte is important and has meaning. To verify this information for yourself, source code is available here and instructions on the hooking process are provided in this ZIP folder.

After writing this documentation, we've discovered rbx-dom, a site dedicated to serialization and deserialization implementations. Sadly, it does not contain any information about remotes, but some information found in the other specifications has been very encouraging as they support our findings.

Variable Length Quantities

Many of the values packets use can scale in size drastically, but still need to be kept as small as possible. There is a common format or strategy for doing this called Variable Length Quantities that only use as much space as they need to represent a number. This is used for the length of strings, the number of arguments in a remote call, and the number of remotes in a session. It is labeled with the acronym VLQ in parenthesis since it takes the place of a size. We know these are VLQs and not just regular numbers because we have tested them with numbers that are too big to fit in their initial size and looked at the binary; they scale in size as the number grows.

The Enigmas

Even after literal days of looking at the binary, we still can't quite figure out some of the meanings behind some of the bytes. We have appropriately called these bytes "Enigmas". Over time these enigma bytes have been reduced, but they aren't quite gone yet. If you would like to help identify these bytes, you may download the packet viewer, run the tests you need to, and make a pull request improving Squash's documentation!

Packet Structure

Packet Type 0×83 (byte)Packet Data 1Packet Data 2...Packet Delimiter 0×00 (byte)

Every packet of Packet Type 0×83 ends with a Packet Delimiter 0×00 (null) byte, but not all packet types do this.

Single Remote Optimization Strategy

When multiple packet datas are sent within a short enough time period using the same remote, they are merged into a single packet in the format above. This is why the packet delimiter is important; it says when to stop reading the packet. This is a strategy used to reduce packet overhead, such as the packet type, delimiter, and header necessary due to internet protocols.

It is a strategy to use only one remote and funnel all data through it, appending all packet datas into a single packet. This is why we recommend using a single remote for all data, and not using multiple remotes for different kinds of data.

Packet Data Types

Client To Server

Remote Event

Packet Subtype 0×0701 (2 bytes)Remote Id (3 bytes)Enigma 0×000b (2 bytes)Client To Server Id 0×70 (byte)User Information (5 bytes)Argument Count (byte)Data 1Data 2...

When creating remotes in studio before starting a session, each remote gets an incrementing Remote Id starting from an unpredictable number. This number is 3 bytes long. When creating remotes in a session, each remote gets a Remote Id that increments at different rates, and starts from a different value.

We are unsure of the format of the User Information, or what it actually contains. We hypothesize that it contains the player's UserId, but we have not been able to verify this.

The maximum Argument Count is 255 because the argument count is a single byte. This happens to also align with the maximum number of arguments a function can have in Luau.

There is no difference in size when firing remotes with different ids, or in short, the number of remotes does not affect the size of packets.

Remote Function

Packet Subtype 0×0701 (2 bytes)Remote Id (3 bytes)Enigma 0×000b (2 bytes)Client To Server Id 0×7b (byte)Call Count (VLQ = 2)User Information (5 bytes)Argument Count (byte)Data 1Data 2...

The Call Count is the number of times the remote function has been invoked since the start of the session. It increments by 2 every invocation, and is sent both ways from the Server -> Client -> Server or Client -> Server -> Client. We hypothesize that this is used to prevent duplicate packets from being processed, to prevent packets from being processed out of order, or to know if a packet was dropped.

Server To Client

Remote Event

Packet Subtype 0×0701 (2 bytes)Remote Id (3 bytes)Enigma 0×000b (2 bytes)Server To Client Id 0×6f (byte)Argument Count (byte)Data 1Data 2...

Remote Function

Packet Subtype 0×0701 (2 bytes)Remote Id (3 bytes)Enigma 0×000b (2 bytes)Server To Client Id 0×79 (byte)Call Count (VLQ = 2)Argument Count (byte)Data 1Data 2...

Data

Below are the different ways types of data are formatted in memory when packed into remotes.

Void (Just Remote Overhead)

Server -> Client

Remote Events
CreatedRemote NamePacket TypePacket SubtypeRemote IdEnigmaServer To Client IdArgument CountPacket Delimiter
Before"A"0×830×07010×7f6d110×000b0×6f0×000×00
Before"B"0×830×07010×7d6d110×000b0×6f0×000×00
Before"C"0×830×07010×7e6d110×000b0×6f0×000×00
After"D"0×830×07010×430d190×000b0×6f0×000×00
After"E"0×830×07010×470d190×000b0×6f0×000×00
After"F"0×830×07010×4a0d190×000b0×6f0×000×00
Remote Functions
CreatedRemote NamePacket TypePacket SubtypeRemote IdEnigmaServer To Client IdCall CountArgument CountPacket Delimiter
Before"A"0×830×07010×9ef1040×000b0×790×020×000×00
Before"B"0×830×07010×9ff1040×000b0×790×020×000×00
Before"C"0×830×07010×a0f1040×000b0×790×020×000×00
After"D"0×830×07010×27f5060×000b0×790×020×000×00
After"E"0×830×07010×2bf5060×000b0×790×020×000×00
After"F"0×830×07010×2ef5060×000b0×790×020×000×00

Client -> Server

Remote Events
CreatedRemote NamePacket TypePacket SubtypeRemote IdEnigmaClient To Server IdUser InformationArgument CountPacket Delimiter
Before"A"0×830×07010×6d91090×000b0×700×0159890a000×000×00
Before"B"0×830×07010×6b91090×000b0×700×0159890a000×000×00
Before"C"0×830×07010×6c91090×000b0×700×0159890a000×000×00
After"D"0×830×07010×67960b0×000b0×700×0159890a000×000×00
After"E"0×830×07010×6b960b0×000b0×700×0159890a000×000×00
After"F"0×830×07010×6e960b0×000b0×700×0159890a000×000×00
Remote Functions
CreatedRemote NamePacket TypePacket SubtypeRemote IdEnigmaClient To Server IdCall CountUser InformationArgument CountPacket Delimiter
Before"A"0×830×07010×d9be070×000b0×7b0×020×01c5b608000×000×00
Before"B"0×830×07010×dabe070×000b0×7b0×020×01c5b608000×000×00
Before"C"0×830×07010×dbbe070×000b0×7b0×020×01c5b608000×000×00
After"D"0×830×07010×6fc3090×000b0×7b0×020×01c5b608000×000×00
After"E"0×830×07010×73c3090×000b0×7b0×020×01c5b608000×000×00
After"F"0×830×07010×76c3090×000b0×7b0×020×01c5b608000×000×00

Nil

Type 0×01 (byte)

(nil)

1
0×01

Booleans

Type 0×09 (byte)Value (byte)

(true)

9true
0×090×01

(false)

9false
0×090×00

(true, true)

9true9true
0×090×010×090×01

Numbers (Double, F64)

Type 0×0c (byte)Value (8 bytes)

(-5)

12-5
0×0c0×c014000000000000

(5)

125
0×0c0×4014000000000000

(0, 0)

120120
0×0c0×00000000000000000×0c0×0000000000000000

Strings

Type 0×02 (byte)Length (VLQ = 1)Value (Length bytes)

("Hello World!")

212'H''e''l''l''o'' ''W''o''r''l''d''!'
0×020×0c0×480×650×6c0×6c0×6f0×200×570×6f0×720×6c0×640×21

("swous", "bibbity")

25's''w''o''u''s'27'b''i''b''b''i''t''y'
0×020×050×730×770×6f0×750×730×020×070×620×690×620×620×690×740×79

Vector2int16s

Type 0×18 (byte)X (2 bytes)Y (2 bytes)

(Vector2int16.new())

2400
0×180×00000×0000

(Vector2int16.new(-5, 7))

24-57
0×180xfffb0×0007

Vector2s

Type 0×15 (byte)X (4 bytes)Y (4 bytes)

(Vector2.zero) or (Vector2.new())

2100
0×150×000000000×00000000

(Vector2.new(5, -1))

215-1
0×150×40a000000×bf800000

Vector3int16s

Type 0×19 (byte)X (2 bytes)Y (2 bytes)Z (2 bytes)

(Vector3int16.new())

25000
0×190×00000×00000×0000

(Vector3int16.new(-5, 7, 9))

25-579
0×190xfffb0×00070×0009

Vector3s

Type 0×16 (byte)X (4 bytes)Y (4 bytes)Z (4 bytes)

(Vector3.zero) or (Vector3.new())

22000
0×160×000000000×000000000×00000000

(Vector3.new(59.2, -1.101, 9.3))

2259.2-1.1019.3
0×160x426ccccd0xbf8ced910x4114cccd

CFrames

General Case

In the general case, CFrames have some arbitrary rotation that is not clean multiples of 90 degrees. This means that the rotation will not or cannot be enumerated, and therefore must be sent entirely. We do not understand the rotation format, but it is 6 bytes long, so forgive the elusive formatting we use. If you would like to help us figure out the rotation format, you may download the packet viewer, run the tests you need to, and make a pull request improving Squash's documentation!

Type 0×1b (byte)X (4 bytes)Y (4 bytes)Z (4 bytes)Id 0×00 (byte)Rotation (6 bytes)

(CFrame.fromEulerAnglesYXZ(5, -1, 9))

2700005, -1, 9
0×1b0×000000000×000000000×000000000×000×54009391a6cc

(CFrame.fromEulerAnglesYXZ(-1, 2, 3) + Vector3.new(-1, 2, -3))

27-12-30-1, 2, 3
0×1b0×bf8000000×400000000×c04000000×000×1c1d16b1de9e

Special Case

In the special case, CFrames have rotations that are clean multiples of 90 degrees. This means that the rotation can be enumerated, and so only the enum is sent, and the rotation is reconstructed on the other side.

Type 0×1b (byte)X (4 bytes)Y (4 bytes)Z (4 bytes)Id (byte)

(CFrame.identity) or (CFrame.new())

270002
0×1b0×000000000×000000000×000000000×02

(CFrame.new(938, 0, -2)) or (CFrame.identity + Vector3.new(938, 0, -2))

279380-22
0×1b0×446a800×000000000×c00000000×02

Below are all of the different axis-angle representation of the rotation matrices that map to each rotation id. We do not know why there are holes in the Ids but have verified through exhaustive testing that these are the only Ids. Below are the orientations using EulerAnglesYXZ in degrees. These values are supported by Rojo's documentation of the RBXM file format. It has a slightly different format than packets, but these specific values are the same.

IdXYZ
0×02000
0×039000
0×050180180
0×06-9000
0×0709090
0×0909090
0×0a0090
0×0c0-9090
0×0d-90-900
0×0e0-900
0×1090-900
0×11090180
0×1401800
0×15-90-1800
0×1700180
0×18901800
0×1900-90
0×1b0-90-90
0×1c0-180-90
0×1e090-90
0×1f90900
0×200900
0×22-90900
0×230-90180

Tables

Tables are separated into two types: Arrays and Dictionaries. This is because internally they use different types. This allows them to optimize how they read each case, at the cost of less flexibility of what the table can contain. Arrays may only have numerical indices, and must be contiguous. If you send an array with a hole, it will stop reading at the first hole. Dictionaries may only have string indices, and don't have an internal order; holes don't exist in dictionaries. Using any other kind of key will result in an error.

Arrays

Type 0×1e (byte)Element Count (VLQ = 1)Element 1Element 2...

({})

300
0×1e0×00

({true})

3019true
0×1e0×010×090×01

({[1] = true, [3] = false})

The array is cut off at the first nil value.

3019true
0×1e0×010×090×01

({"sofa is ", 8})

30228's''o''f''a'' ''i''s'' '128
0×1e0×020×020×080×730×6f0×660×610×200×690×730×200×0c0×4020000000000000

Dictionaries

Type 0×1f (byte)Pair Count (VLQ)Key 1Value 1Key 2Value 2...

({sword = true})

3115's''w''o''r''d'9true
0×1f0×010×050×730×770×6f0×720×640×090×01

({stamina = "high", ["health"] = 82.1})

3127's''t''a''m''i''n''a'24'h''e''a''l''t''h'6'h''e''a''l''t''h'1282.1
0×1f0×020×070×730×740×610×6d0×690×6e0×610×020×040×680×650×610×6c0×740×680×060×680×650×610×6c0×740×680×0c0×4054866666666666

Instances

Instances seem to use an Id system which is shared across the client-server boundary. There isn't much we can say about them other than rbx-dom's mention of Referents.

Type 0×1c (byte)Instance Id (5 bytes)

(workspace)

280×0190c80600

(game.ReplicatedStorage.Part)

280×01546b0900