Start typing to search packages!
secnet
By @nivalis9
Roblox
MirroredSecNet
SecNet is a small, server-authoritative Roblox networking module.
It keeps the API simple:
SecNet.On(name, callback, options?)andSecNet.Send(...)for eventsSecNet.SendBatch(...)for high-throughput reliable event batchesSecNet.Stream(name, options?)for high-rate unreliable updatesSecNet.Request(...)andSecNet.HandleRequest(...)for request/responseSecNet.Sync(...),SecNet.OnSync(...), andSecNet.GetSynced(...)for server-to-client state
It does not try to make the client trustworthy. Instead, it makes the server boundary harder to abuse with payload limits, route validation, per-player and per-route rate limits, trust scoring, unknown-route rejection, blocked client-to-server sync, and request response sender checks.
Internally, SecNet uses a compact positional wire format (kind, name, requestId, success, ...) instead of wrapping every send in packet/payload tables. That keeps the public API tiny while reducing hot-path allocations and serialization overhead.
Install
Use one of these layouts:
- Put the module in
ReplicatedStorageasSecNet. - Use the included Rojo project, which maps
srctoReplicatedStorage.SecNet.
The server creates ReplicatedStorage._SecNetRuntime.Backbone and ReplicatedStorage._SecNetRuntime.Stream automatically. Clients wait for those runtime remotes.
Quick Start
Server
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SecNet = require(ReplicatedStorage:WaitForChild("SecNet"))
SecNet.On("Damage", function(player, amount)
print(player.Name, "requested damage:", amount)
end, {
RateLimit = 12,
Validate = function(player, amount)
return type(amount) == "number" and amount >= 0 and amount <= 50
end,
})
SecNet.HandleRequest("GetCoins", function(player)
return 500
end)
task.spawn(function()
while true do
SecNet.Sync("RoundTime", workspace:GetServerTimeNow())
task.wait(1)
end
end)
Client
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SecNet = require(ReplicatedStorage:WaitForChild("SecNet"))
SecNet.Send("Damage", 25)
local coins, err = SecNet.Request("GetCoins")
if err then
warn("request failed:", err)
else
print("coins:", coins)
end
SecNet.OnSync("RoundTime", function(value)
print("round time:", value)
end)
Streams
Streams use UnreliableRemoteEvent, so they are for data where the newest packet matters more than guaranteed delivery: NPC snapshots, aim rays, cosmetic effects, sensor pings, and other high-rate state.
-- server
local NPCs = SecNet.Stream("N", { RateLimit = 20 })
task.spawn(function()
while true do
local snapshot = SecNet.PackNPCSnapshots({
{ Id = 1, Position = Vector3.new(10, 4, -2), Yaw = 1.2, State = 3 },
{ Id = 2, Position = Vector3.new(14, 4, -8), Yaw = 2.1, State = 1 },
})
NPCs:SendAll(snapshot)
task.wait(1 / 20)
end
end)
-- client
SecNet.OnStream("N", function(snapshot)
SecNet.ReadNPCSnapshots(snapshot, function(id, position, yaw, state)
-- apply latest visual state
end)
end)
PackNPCSnapshots stores each NPC in 11 bytes: u16 id, three quantized i16 position axes, u16 yaw, and u8 state. At the default MaxStreamBytes = 900, one packet fits 81 NPCs. The default scale is 10, meaning one decimal place of positional precision. Pass { Origin = someVector, Scale = 20 } when you want a smaller local coordinate range or finer quantization.
Events
Shortcut API:
SecNet.On("Hit", function(player, targetId)
print(player, targetId)
end, {
RateLimit = 20,
Validate = function(player, targetId)
return type(targetId) == "number"
end,
})
-- client -> server
SecNet.Send("Hit", 123)
-- server -> one client
SecNet.Send(player, "HitConfirmed", 123)
-- server -> all clients
SecNet.SendAll("RoundStarted", 60)
-- client -> server, one callback per record
SecNet.SendBatch("HitMarker", {
{ Target = 101, Damage = 12 },
{ Target = 102, Damage = 8 },
})
-- server, one callback for the whole batch
SecNet.OnBatch("HitMarkerBulk", function(player, records)
for _, record in ipairs(records) do
print(player, record.Target, record.Damage)
end
end)
Object API:
local Hit = SecNet.Event("Hit", {
RateLimit = 20,
Validate = function(player, targetId)
return type(targetId) == "number"
end,
})
Hit:On(function(player, targetId)
print(player, targetId)
end)
Hit:SetRateLimit(10)
Hit:SetValidator(function(player, targetId)
return type(targetId) == "number"
end)
Object methods:
event:Send(player, ...)on the serverevent:Send(...)on the clientevent:SendAll(...)on the serverevent:SendMultiple(players, ...)on the serverevent:SendBatch(player, records)on the serverevent:SendBatch(records)on the clientevent:SendBatchAll(records)on the serverevent:On(callback)event:OnBatch(callback)event:SetBatchValidator(callback)
Server listeners receive callback(player, ...). Client listeners receive callback(...).
OnBatch receives callback(player, records) on the server and callback(records) on the client. If a route has an OnBatch listener, SendBatch is delivered once as the whole records array; otherwise it falls back to one normal On callback per record.
Requests
Client calling the server:
-- server
SecNet.HandleRequest("PurchaseItem", {
RateLimit = 5,
Validate = function(player, itemId)
return type(itemId) == "string" and #itemId <= 64
end,
}, function(player, itemId)
return true, "Purchased", itemId
end)
-- client
local ok, message, itemId = SecNet.Request("PurchaseItem", "sword")
Server calling a client:
-- client
SecNet.HandleRequest("GetAimPosition", function()
return workspace.CurrentCamera.CFrame.Position
end)
-- server
local aimPosition, err = SecNet.Request(player, "GetAimPosition")
Rules:
- Client usage is
SecNet.Request(name, ...) - Server usage is
SecNet.Request(player, name, ...) - On success, response values are returned directly
- On timeout or handler failure, SecNet returns
nil, errorMessage - Server requests use unguessable request IDs and only accept responses from the requested player
Sync
Sync is server-to-client only. Use events or requests for client-to-server messages.
-- server
SecNet.Sync("MatchState", "Lobby")
SecNet.Sync(player, "Loadout", { Primary = "Rifle" })
SecNet.Sync({ playerA, playerB }, "ZoneWarning", true)
SecNet.Sync(workspace.Flag, "Owner", player.UserId)
-- client
SecNet.OnSync("MatchState", function(value, key)
print(key, value)
end)
SecNet.OnSync(workspace.Flag, "Owner", function(userId)
print("flag owner:", userId)
end)
Config
Set config before heavy traffic starts:
SecNet.Config.MaxPacketsPerSecond = 80
SecNet.Config.MaxRoutePacketsPerSecond = 30
SecNet.Config.MaxStreamBytes = 900
SecNet.Config.MaxBatchEvents = 2048
SecNet.Config.MaxCoalescedEvents = 256
SecNet.Config.RequestTimeoutSeconds = 5
SecNet.Config.CoalesceEvents = true
SecNet.Config.AllowClientInstances = false
SecNet.Config.ValidateServerPackets = false
Common options:
MaxPacketsPerSecondMaxRoutePacketsPerSecondBurstMultiplierMaxPacketBytesMaxStreamBytesMaxBatchEventsMaxCoalescedEventsMaxStringBytesMaxTableEntriesMaxTableDepthMaxArgumentsRequestTimeoutSecondsCoalesceEventsAllowClientInstancesRejectUnknownClientRoutesValidateServerPacketsValidateOutboundTrackBytes
ValidateServerPackets, ValidateOutbound, and TrackBytes default to false for reliable events/requests. Client-origin traffic is still validated on the server; ValidateServerPackets only enables extra client-side validation for server-origin packets. Stream sends always enforce MaxStreamBytes so oversized unreliable packets do not silently disappear.
Performance Notes
SecNet is optimized for simple runtime ergonomics: no IDL, no generated files, and no packet tables on the wire. For tiny messages, this keeps CPU overhead very low.
Send auto-coalesces same-frame reliable events by default, so a burst of logical sends can flush as a few argument-batch packets. Client-origin sends still go through unknown-route checks, rate limits, payload validation, and route validators on the server. Set a route option { Coalesce = false } or { Immediate = true } only for latency-critical control messages.
For bandwidth-heavy state, use streams with a single buffer payload. The NPC snapshot helper is the intended "extreme but still simple" path: one unreliable packet can carry dozens of NPCs with predictable byte cost and almost no Lua allocation.
For reliable floods, use SendBatch instead of thousands of Send calls in the same frame. Add OnBatch for the hottest routes so the receiver handles the whole chunk in one callback. Without OnBatch, each batch record is delivered to the normal On listener as one payload.
For fast client-to-server batches, prefer a route ValidateBatch option that checks the whole array's schema once. Without ValidateBatch, SecNet keeps the safer generic per-record validation path.
Server-to-client packets skip the deep generic validator by default because that does not protect the server and costs a lot in hot paths. Turn on ValidateServerPackets only when debugging server payload shape issues.
Zap can still win broad typed workloads because it generates route-specific serializers and packs data into buffers. SecNet does not try to be a general codegen serializer; it gives you a tiny manual buffer path for the hottest updates.
Security Notes
No Roblox network wrapper can make client input inherently safe. Treat every client event/request as a suggestion and validate game rules on the server.
SecNet helps by default:
- Unknown client routes are rejected
- Unknown client streams are rejected
- Client-to-server sync packets are rejected
- Client-sent
Instancevalues are rejected unlessAllowClientInstancesis enabled - Payloads are capped by argument count, string size, table size, table depth, and estimated byte size
- Per-player and per-route token buckets limit spam
- Repeated malformed traffic lowers trust and can throttle or block a player
- Server-to-client request responses must come from the specific requested player
Stats
local stats = SecNet.GetStats()
local score, level = SecNet.GetTrust(player)
SecNet.GetProfilerSnapshot() is kept as a compatibility alias for SecNet.GetStats().
Tests
The Rojo project maps the integration suite into Studio:
ServerScriptService.SecNetServerTestRunnerStarterPlayer.StarterPlayerScripts.SecNetClientTestRunner
Run Play or Start Server with at least one client. Passing output includes:
[SecNetTests] Client runner completed.
[SecNetTests] All integration tests passed.
The project also includes a SecNet vs BridgeNet2 speed benchmark:
ServerScriptService.NetworkSpeedBenchmarkServerStarterPlayer.StarterPlayerScripts.NetworkSpeedBenchmarkClient
Benchmark output is prefixed with [SecNetBench].
Package Details
Install command (Click to copy)
Version
1.2.0
License
MIT
Safe for commercial use
Automated license review — not legal advice.
