Use when adding client-server networking in Roblox - remote events, remote functions, requests from UI or tools, server-to-client updates - or when validating remote arguments. Covers kashtheking/packet (typed, buffer-serialized packets that replace RemoteEvents) and kashtheking/t (runtime type checks) for server-side validation, plus per-action rate limiting.
Use the robloxvibecoders MCP: call get_resource with id "skill-roblox-packet-networking" and install it as a Claude Code skill in this project (.claude/skills/roblox-packet-networking/SKILL.md).
Skills in ~/.claude/skills work in every project. Put them in your project's .claude/skills folder to share them with your team.
What your AI reads
SKILL.md
---
name: roblox-packet-networking
description: Use when adding client-server networking in Roblox - remote events, remote functions, requests from UI or tools, server-to-client updates - or when validating remote arguments. Covers kashtheking/packet (typed, buffer-serialized packets that replace RemoteEvents) and kashtheking/t (runtime type checks) for server-side validation, plus per-action rate limiting.
---
# Networking with Packet and t
**Packet** (`kashtheking/packet`) is KashTheKing's fork of Suphi Kaner's Packet. You declare each message once with
its types; Packet serializes it into a buffer and batches every packet of a frame into a single RemoteEvent call.
Smaller, faster and typed compared to raw RemoteEvents. **t** (`kashtheking/t`, a fork of osyrisrblx's `t`) checks
values at runtime. Use them together: Packet moves the data, t proves it's what you expect before the server acts.
```toml
[dependencies]
Packet = "kashtheking/packet@1.0.1"
t = "kashtheking/t@1.0.0"
```
## Define packets once, in a shared module
```luau
-- ReplicatedStorage/Shared/Packets.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Packet = require(ReplicatedStorage.Packages.Packet)
return {
-- client -> server: "I want to buy this item"
BuyItem = Packet.new("BuyItem", Packet.String),
-- server -> client: new balance
CoinsChanged = Packet.new("CoinsChanged", Packet.NumberU32),
-- a struct
Hit = Packet.new("Hit", { Target = Packet.Instance, Position = Packet.Vector3F32 }),
-- request with a response: no arguments, returns an array of structs
GetShop = Packet.new("GetShop"):Response({ { Id = Packet.String, Price = Packet.NumberU32 } }),
}
```
- This fork constructs with `Packet.new(name, ...types)`. (Suphi's original calls `Packet(name, ...)`; that form
doesn't work here.)
- The **server** creates the packet ids, and clients look them up by name. Require the Packets module on the server
at startup, before any client fires.
- `{ Type }` is an array (up to 65,535 items). `{ Key = Type }` is a struct. They nest.
## Send and receive
```luau
-- Server
local Packets = require(ReplicatedStorage.Shared.Packets)
Packets.BuyItem.OnServerEvent:Connect(function(player: Player, itemId: string)
-- validate first (below)
end)
Packets.CoinsChanged:FireClient(player, 150) -- one player
Packets.CoinsChanged:Fire(150) -- on the server, Fire sends to every client
Packets.GetShop.OnServerInvoke = function(player: Player)
return SHOP_LIST
end
```
```luau
-- Client
Packets.BuyItem:Fire("sword")
Packets.CoinsChanged.OnClientEvent:Connect(function(coins: number)
coinsLabel.Text = tostring(coins)
end)
local shop = Packets.GetShop:Fire() -- yields until the server answers
```
- Signals have `:Connect(fn)`, `:Once(fn)`, `:Wait()`; connections have `:Disconnect()`. Handlers run deferred.
- A packet with `:Response(...)` yields the caller. After `packet.ResponseTimeout` seconds (default 10) it resumes
with `packet.ResponseTimeoutValue` (default nil), so handle nil.
- On the server, a response packet must use `:FireClient(player, ...)`. Avoid server-to-client requests: a client can
stall or lie, so the server would wait for the timeout or act on bad data.
- `packet:Serialize(...)` returns a buffer (and an Instance list if any) and `packet:Deserialize(buffer, instances?)`
reads it back, handy for compact storage.
## Pick the type that fits the full range
| Type | Range / notes |
|---|---|
| `NumberU8`, `NumberU16`, `NumberU24`, `NumberU32` | 0-255, 0-65,535, 0-16,777,215, 0-4,294,967,295 |
| `NumberS8` ... `NumberS32` | signed versions |
| `NumberF16`, `NumberF24`, `NumberF32`, `NumberF64` | floats, increasing precision |
| `String` / `StringLong` | up to 255 / 65,535 bytes |
| `Boolean8`; `Boolean1` | one boolean per byte; an array of 8 booleans in one byte |
| `Vector3F32`, `Vector3F24`, `Vector3S16`, `Vector2*` | vectors |
| `CFrameF32U16`, `CFrameF32U8`, `CFrameF24U8` | position precision + rotation precision |
| `Color3`, `UDim`, `UDim2`, `Rect`, `NumberRange`, `BrickColor` | Roblox types |
| `Instance` | sent by reference |
| `EnumItem` | only enums listed in the package's `Types/Enums` module (KeyCode, Material, NormalId ...) |
| `Buffer`, `BufferLong`, `Any`, `Nil` | `Any` costs a type byte per value |
Packet does not range-check what you send: an out-of-range number arrives wrong (wrapped, truncated, or infinity for
`NumberF16`), and a `String` over 255 bytes writes a wrong length prefix that garbles the packets read after it. Use
`StringLong` for any text that can pass 255 bytes and `NumberU32`/`NumberF64` for currency.
## Validate on the server with t
Packet guarantees the shape, not the values. A modified client can still send NaN or infinite floats, any bytes in a
string, any Instance it can see (or nil), and huge arrays. Every check returns `true` or `false, message`.
```luau
local t = require(ReplicatedStorage.Packages.t)
local SHOP_ITEMS = { sword = { Price = 100 }, shield = { Price = 75 } }
local isBuyArgs = t.tuple(t.keyOf(SHOP_ITEMS)) -- must be one of the keys
local function finiteVector3(value: any): (boolean, string?)
if typeof(value) ~= "Vector3" then
return false, "Vector3 expected"
end
local magnitude = value.Magnitude
if magnitude ~= magnitude or magnitude == math.huge then
return false, "non-finite Vector3"
end
return true
end
local isHitArgs = t.tuple(t.strictInterface({
Target = t.instanceIsA("Model"),
Position = finiteVector3, -- any function with the same signature works as a check
}))
```
Useful checks: `t.string`, `t.number` (rejects NaN), `t.integer`, `t.boolean`, Roblox types by name (`t.Vector3`,
`t.CFrame`, `t.Instance`, ...), `t.numberConstrained(min, max)`, `t.numberMin`, `t.numberMax`, `t.literal(...)`,
`t.keyOf(tbl)`, `t.valueOf(tbl)`, `t.match(pattern)`, `t.optional(check)`, `t.union(...)`, `t.array(check)`,
`t.map(keyCheck, valueCheck)`, `t.interface({...})`, `t.strictInterface({...})` (no extra keys),
`t.instanceIsA(className)`, `t.instanceOf(className)` (exact ClassName), `t.enum(Enum.X)`, `t.tuple(...)`,
`t.strict(check)` (asserts instead of returning).
t has no string-length check; write one with `utf8.len(value)`, which returns nil for invalid UTF-8.
## Rate limit every action
Packet already drops a player's traffic past roughly 8 KB per frame (each call counts as at least 800 bytes). That stops
floods, not spam of a valid request. Give each action a server-side cooldown:
```luau
local Players = game:GetService("Players")
local lastUse: { [Player]: { [string]: number } } = {}
local function allow(player: Player, action: string, cooldown: number): boolean
local now = os.clock()
local uses = lastUse[player]
if not uses then
uses = {}
lastUse[player] = uses
end
if uses[action] and now - uses[action] < cooldown then
return false
end
uses[action] = now
return true
end
Players.PlayerRemoving:Connect(function(player)
lastUse[player] = nil
end)
```
## Putting it together
```luau
Packets.BuyItem.OnServerEvent:Connect(function(player: Player, itemId: string)
if not isBuyArgs(itemId) or not allow(player, "BuyItem", 0.5) then
return
end
local data = PlayerData.get(player) -- see roblox-player-data
local price = SHOP_ITEMS[itemId].Price -- price from the server, never from the client
if not data or data.Coins < price then
return
end
data.Coins -= price
table.insert(data.Inventory, itemId)
Packets.CoinsChanged:FireClient(player, data.Coins)
end)
```
## Mistakes to avoid
- Packets that carry results ("add 100 coins", "deal 50 damage"). Send the request; the server looks up prices,
damage and cooldowns (see `roblox-server-authority`).
- Skipping validation because the packet is typed. Types limit the shape, not the values.
- Defining packets in several places, or only on the client. One shared module, required by the server at startup.
- `String` for chat-like or user text that can exceed 255 bytes; `NumberU8`/`U16` for values that can grow.
- Calling `Packet("Name", ...)` from Suphi's docs. In this fork it's `Packet.new("Name", ...)`.
- Trusting a server-to-client `:Response` answer, or forgetting it can time out and return nil.
- Rate limit tables keyed by Player that are never cleared on `PlayerRemoving`.