Middleware & Reliability

Middleware & Reliability

Every channel accepts an optional ChannelOptions table controlling how a packet is delivered and what happens to its data before your callback runs.

type ChannelOptions = {
	reliability : ReliabilityType?,
	middleware  : {MiddlewareFn}?,
}
FieldTypeDefaultDescription
reliabilityReliabilityType"reliable"Delivery guarantee, forwarded to the underlying ByteNet packet's reliabilityType.
middleware{MiddlewareFn}{}Ordered list of gatekeeper functions run on every incoming on() callback.

Reliability

type ReliabilityType = "reliable" | "unreliable" | "unreliableOrdered"
ValueGuarantees delivery?Guarantees order?Typical use case
reliableYesYesDamage events, inventory changes, chat — anything that must never be dropped.
unreliableNoNoHigh-frequency data where the newest value matters more than every value, e.g. per-frame position updates.
unreliableOrdered⚠️ Not actually supported⚠️ Not actually supportedSee warning below.
🚫

Known limitation: the underlying ByteNet 0.4.6 packet type only recognizes reliabilityType: ("reliable" | "unreliable")? — there is no third option. KravasNet's ReliabilityType still declares "unreliableOrdered" for forward-compatibility, but passing it today will not behave as "ordered unreliable" delivery — at best ByteNet falls back to whatever its reliabilityType == "reliable" check resolves to (i.e. it silently behaves as "unreliable", since anything that isn't exactly "reliable" takes the unreliable code path). Do not rely on "unreliableOrdered" for anything that needs in-order delivery until ByteNet itself adds real support for it. Stick to "reliable" or "unreliable".

CombatNet:add("Position", { pos = KravasNet.vec3 }, { reliability = "unreliable" })

If reliability is omitted, KravasNet defaults to "reliable", matching ByteNet's own safest default.

Middleware

type MiddlewareFn = (data: any, player: Player?) -> (shouldContinue: boolean, data: any?)

Middleware functions are gatekeepers attached to a channel's on() listener. They run in the order supplied, once per received packet, before your callback executes.

Return valueMeaning
false (first value)Packet is dropped. The user callback is never invoked for this packet.
true (first value)Packet continues to the next middleware (or to the callback if it was the last one).
second value ~= nilReplaces data for the remainder of the chain and for the final callback.
second value == nildata is left unchanged.

Execution flow

local function runMiddleware(fns, data, player)
	for _, fn in ipairs(fns) do
		local ok, result = fn(data, player)
		if not ok then
			return false, nil
		end
		if result ~= nil then
			data = result
		end
	end
	return true, data
end

This is the exact internal implementation: middleware is short-circuiting — the first function that returns false stops the chain immediately, and the callback is skipped entirely for that packet.

Example: server-side validation

local function clampDamage(data, player)
	if type(data.amount) ~= "number" then
		return false -- drop malformed packets
	end
	data.amount = math.clamp(data.amount, 0, 100)
	return true, data
end
 
CombatNet:add("Damage", { amount = KravasNet.float32 }, {
	middleware = { clampDamage },
})

Example: chaining multiple middlewares

local function mustBeAlive(data, player)
	return player.Character ~= nil and player.Character:FindFirstChild("Humanoid") ~= nil
end
 
local function logPacket(data, player)
	print(`[Combat] {player.Name} sent`, data)
	return true -- data unchanged
end
 
CombatNet:add("Damage", { amount = KravasNet.float32 }, {
	middleware = { mustBeAlive, logPacket, clampDamage },
})
⚠️

Middleware runs on both server and client on() calls, since runMiddleware is shared code. On the client, the player argument passed into your middleware functions will be nil — the client-side on callback signature only forwards data.


Next: see the full list of supported Data Types, or jump to Best Practices.