BlinkBlox
Buffer networking for Roblox whose server is safe to point at the open internet.
GitHub · Documentation · Quick start
First, a big thank you to @1Axen for Blink. It has helped me a lot, and the schema language they designed is a genuinely great way to describe networking. It was a shame to see the project move to a from-scratch rewrite, though: I needed bug fixes and new features now, and the version everyone uses was only getting major fixes. So I forked it, and ended up reworking pretty much everything - the runtime, the wire format, the security layer, the plugin - keeping the .blink schema you already have in your project. The language only grew, so your schemas carry over.
TL;DR - how it compares
| BlinkBlox | Blink | zap | ByteNet | Packet | QuickNet | Warp | |
|---|---|---|---|---|---|---|---|
| Server decode, 100 structs ×1000 a frame | 3.58 ms | 21.83 ms | 21.16 ms | 42.49 ms | 52.70 ms | 11.37 ms | 26.91 ms |
| Client send, 1000 booleans (interpreted) | 16.26 ms | 29.34 ms | 65.52 ms | 54.15 ms | 53.99 ms | 16.78 ms | 38.47 ms |
Bytes for a boolean[1000] |
128 B | 1003 B | 1003 B | 1003 B | 1003 B | 128 B | 128 B |
FireAll of 100 structs to 50 players |
0.020 ms, 1 call | 0.711 ms, 50 calls | 0.734 ms, 50 calls | 0.044 ms, 1 call | 0.084 ms, 1 call | 0.306 ms, 50 calls | 2.350 ms, 50 calls |
| Studio FPS at 1000 random structs a frame (RemoteEvents: 15) | 60* | 45 | 44 | 34 | 27 | 57 | 32 |
* Studio caps the frame rate at 60. Intel i7-13700K, on LuneBlox 0.10.9 (the Luau version Roblox runs, with Roblox’s flags); BlinkBlox 0.42.0 (Studio: 0.41.2), Blink 0.18.9, zap 0.6.29, ByteNet 0.4.3, Packet 1.7.0, QuickNet 0.3.5, Warp 1.1.0-pre7; medians of three to five runs. Don’t trust us - run it: luneblox run Rivals in benchmark/. Full methodology: Benchmarks.
Why another networking library?
It isn’t a new one - it’s Blink, continued. The 0.18 line was left with reported bugs open, including a hostile client buffer that could be parsed without bounds (upstream issue #45). BlinkBlox fixed those, then kept going for over twenty releases.
Every library on this forum is fast. BlinkBlox is fast and treats every client as an attacker. Exploiters don’t use your client code - they write packets by hand. Your server shouldn’t care.
The libraries around it leave that gap open: zap has no rate limiting at all (its maintainer’s position in issue #219 is that throttling is the game’s job) and no pcall around each event, so one malformed event throws away the rest of that player’s batch. Packet matches a function’s reply by slot alone and has no schema check between client and server. QuickNet allocates whatever length a packet names before checking it. Blink’s 0.18 line left the unbounded parse open.
How it works
Describe your events once in a .blink schema. The compiler generates plain Luau modules (and TypeScript definitions) for the server and the client.
event Damage {
From: Client,
Type: Reliable,
Call: SingleSync,
Rate: 10,
Data: struct { Target: Instance(Humanoid), Amount: u8(1..100) }
}
-- Server: runs only after the rate limit passed, Target is a Humanoid and Amount is 1..100
Net.Damage.On(function(Player, Hit)
Combat.Apply(Player, Hit.Target, Hit.Amount)
end)
-- Client
Net.Damage.Fire({ Target = Humanoid, Amount = 25 })
No validation code, no type checks, no rate limiter to write. It’s all generated.
What you get
Security
- Per-player, per-event token buckets (
Rate,Burst,option DefaultRate), a per-player inbound byte budget, andConcurrencyfor functions whose listeners yield. - Packet size, event count and Instance count are capped before anything is parsed.
- Every length is checked before the read and the allocation it pays for - a lying length prefix buys the attacker nothing.
- A malformed event ends its packet cleanly: the events before it are still delivered.
- A client and a server built from different schemas refuse each other instead of decoding one event as another.
- Hooks for your anti-cheat:
SetPacketDropHandler,SetDecodeErrorHandlerand a rate-limit handler, each told which player did what. Nobody is kicked automatically - you decide.
Performance and bandwidth
- One buffer per frame, and a
FireAllis a singleFireAllClientsno matter how many players. - Booleans, optionals, enums and tags share one bitfield;
boolean[]packs eight to a byte. - Lengths are sent relative to their range, usually in one byte.
- Quantized floats:
f32<0.01>(-1..1)is 1 byte instead of 4. u24,i24,f24:vector<f24>is 9 bytes instead of 12.CFrame<quat>fits a rotation in 7 bytes.- A per-player outbound budget: events marked
Priority: Loware the first to be left out when a player’s connection is full. - An unreliable event that can’t fit in a packet is refused at compile time, not dropped silently in production.
- Streams for world state, and
Stampfor lag-compensated hit checks the server clamps.
Tooling
- A Studio plugin with syntax highlighting, autocomplete and live diagnostics.
- A CLI with watch mode,
@profilebuilds that keep debug remotes out of release, and--check --jsonfor editors and AI assistants. - TypeScript output for roblox-ts.
- The generated modules pass their own
--!strict. - Converters from zap, ByteNet, Packet, QuickNet and Warp:
blinkblox <definitions> --from <library>writes a draft schema from what your game already defines.
Full benchmarks
In Studio - 1000 events a frame for ten seconds, medians. “ms” is the client CPU time the frame’s fires took; bytes are per event after Roblox’s compression. Warp only queues a value on fire and encodes it later in the frame, so its fire time can’t be measured - its cost shows in the FPS.
| Payload | RemoteEvents | BlinkBlox | Blink | zap | ByteNet | Packet | QuickNet | Warp |
|---|---|---|---|---|---|---|---|---|
| 1000 booleans | 15 FPS, 26.0 ms | 60 FPS, 1.92 ms | 60 FPS, 4.52 ms | 60 FPS, 12.15 ms | 30 FPS, 17.45 ms | 35 FPS, 27.31 ms | 60 FPS, 1.89 ms | 60 FPS |
| 1000 random booleans | 15 FPS, 29.1 ms | 60 FPS, 4.81 ms, 128.4 B | 60 FPS, 9.89 ms, 193.9 B | 46 FPS, 18.08 ms, 193.8 B | 23 FPS, 21.40 ms, 193.3 B | 28 FPS, 31.79 ms, 194.2 B | 60 FPS, 7.12 ms, 128.1 B | 55 FPS, 128.6 B |
| 100 entities | 15 FPS, 72.7 ms | 60 FPS, 1.98 ms | 44 FPS, 2.81 ms | 45 FPS, 7.35 ms | 35 FPS, 15.79 ms | 27 FPS, 19.24 ms | 58 FPS, 6.21 ms | 33 FPS |
| 100 random entities | 15 FPS, 74.5 ms | 60 FPS, 2.38 ms | 45 FPS, 3.16 ms | 44 FPS, 7.77 ms | 34 FPS, 16.55 ms | 27 FPS, 19.93 ms | 57 FPS, 6.74 ms | 32 FPS |
On LuneBlox, the Luau Roblox runs, without Roblox - ms a frame: send interpreted (as most clients run it) / decode natively (as a server runs it), and bytes per event before compression.
| Payload | BlinkBlox | Blink | zap | ByteNet | Packet | QuickNet | Warp |
|---|---|---|---|---|---|---|---|
| 1000 booleans | 16.26 / 6.72, 128 B | 29.34 / 10.66, 1003 B | 65.52 / 11.49, 1003 B | 54.15 / 50.99, 1003 B | 53.99 / 50.95, 1003 B | 16.78 / 7.48, 128 B | 38.47 / 11.47, 128 B |
| 100 entities | 12.09 / 3.58, 602 B | 12.75 / 21.83, 603 B | 33.39 / 21.16, 603 B | 42.69 / 42.49, 603 B | 39.90 / 52.70, 603 B | 21.69 / 11.37, 603 B | 65.72 / 26.91, 602 B |
Game scenarios, natively, fifty players - send ms, decode ms, remote calls a frame.
| Scenario | BlinkBlox | Blink | zap | ByteNet | Packet | QuickNet | Warp |
|---|---|---|---|---|---|---|---|
100 structs to everyone, FireAll |
0.020, 0.012, 1 call | 0.711, 0.026, 50 calls | 0.734, 0.025, 50 calls | 0.044, 0.109, 1 call | 0.084, 0.170, 1 call | 0.306, 0.034, 50 calls | 2.350, 0.113, 50 calls |
8 unreliable inputs a frame, BatchUnreliable |
0.002, 0.001, 1 call | 0.002, 0.003, 8 calls | 0.003, 0.002, 8 calls | 0.003, 0.008, 1 call | no unreliable channel | 0.002, 0.003, 1 call | 0.004, 0.009, 1 call |
| 100 events carrying a Part | 0.005, 0.011 | 0.005, 0.013 | 0.006, 0.009 | 0.031, 0.093 | 0.038, 0.126 | 0.014, 0.027 | 0.031, 0.091 |
| 16 units a frame to 50 players, unreliable | 0.002, 0.001, 1 call | 0.002, 0.004, 1 call | 0.003, 0.004, 1 call | 0.008, 0.009, 1 call | no unreliable channel | 0.014, 0.003, 50 calls | 0.198, 0.006, 50 calls |
| Each player their own unit, unreliable | 0.012, 0.000 | 0.013, 0.001 | 0.016, 0.001 | 0.033, 0.002 | no unreliable channel | 0.025, 0.001 | 0.046, 0.002 |
The one place BlinkBlox isn’t first is decoding Instances, where zap is faster: zap doesn’t check that what arrives is an Instance at all, while BlinkBlox verifies each one’s class before your code sees it.
The fine print. Studio caps at 60 FPS and compiles LocalScripts natively, which most clients don’t - that’s why the LuneBlox numbers exist. Inbound limits are raised for the flood: BlinkBlox’s through the schema, Packet’s and Warp’s hard-coded 8000 bytes patched in the downloaded copy, QuickNet’s per-event rate limit lifted the way its own benchmark does. Every check still runs and is counted. Identical payloads compress to almost nothing, which is why the random benches exist. Warp sends each reliable packet XORed against the last, so the random benches never repeat a frame. The harness and full percentiles are in benchmark/.
Install
rokit add XopoIII/BlinkBlox blinkblox
pesde add xopoiii/blinkblox --dev --target lune
Binaries for every platform and the plugin .rbxm are attached to each release.
MIT licensed. Originally written by Axen;