Skip to main content

Benchmarks

Numbers first, then everything else.

FireSync with 5,000 all-sync listeners costs 18.5ms per frame. FireDeferred under the same load costs 0.116ms — and that gap widens as listener count climbs. The right fire method for your use case matters. The data below shows exactly when each one wins.

These numbers are the raw output of VeSignal's benchmarker, captured on a live Roblox server with 120 frames sampled per cell.

FireDeferred ratios are misleading

FireDeferred shows ratios like 0.006x and 0.028x that look extraordinary — but they are a timing artifact, not a real performance win. FireDeferred does not execute any listeners. It calls task.defer and returns immediately. The benchmark measures the cost of queuing the work, not running it. All those deferred fires pile up and execute later, outside the measurement window. The actual listener execution cost is the same as FireSync — it just happens on the next cycle.

Use FireDeferred when you need to break re-entrance or intentionally delay execution. Do not use it to "go faster."


The Setup

  • Samples per cell: 120 Heartbeat frames
  • Fires per frame: 100
  • Baseline method: FireSync

Four listener profiles, tested across five listener counts each. All values are wall-clock cost of 100 signal fires per Heartbeat. Run in an empty place for the cleanest results.

  • All-sync — every listener registered with Connect
  • Half async — half registered with ConnectAsync, half with Connect
  • All-async — every listener registered with ConnectAsync
  • Once (reconnect)Once listeners that reconnect immediately after each fire

All-Sync Listeners

When AsyncCount == 0, Fire takes the sync-only fast path and is effectively identical to FireSync. FireDeferred appears cheapest here but see the caveat above — its numbers measure scheduling cost only, not execution.

ListenersFireSyncFireFireAsyncFireDeferredFireSafe
100.05 ms0.047 ms0.177 ms0.133 ms0.131 ms
1000.354 ms0.375 ms1.659 ms0.126 ms1.262 ms
5001.892 ms2.17 ms7.403 ms0.127 ms5.678 ms
1,0003.852 ms3.684 ms14.589 ms0.107 ms10.463 ms
5,00018.539 ms18.974 ms76.019 ms0.116 ms62.485 ms

Throughput (fires/s) at 100 listeners:

FireSyncFireFireAsyncFireDeferredFireSafe
282,634266,40560,259794,69779,265

Fire and FireSync are essentially tied across all counts — the fast path kicks in whenever there are no async connections. FireAsync costs ~4× more due to thread dispatch overhead on every listener.


Half-Async Listeners

Once async listeners are in the mix, Fire must snapshot both Fn and IsAsync per connection and conditionally resume threads. Cost roughly triples vs all-sync.

ListenersFireSyncFireFireAsyncFireDeferredFireSafe
100.05 ms0.125 ms0.175 ms0.113 ms0.206 ms
1000.362 ms1.07 ms1.808 ms0.123 ms1.793 ms
5001.873 ms5.105 ms7.57 ms0.122 ms8.506 ms
1,0003.676 ms10.068 ms14.656 ms0.105 ms16.091 ms
5,00019.548 ms55.725 ms77.676 ms0.11 ms87.212 ms

FireSync ignores IsAsync entirely — it always calls every listener directly. If you need raw throughput and can tolerate blocking async listeners, FireSync is the right choice even in a mixed setup.


All-Async Listeners

When every listener is async, Fire and FireAsync converge — both dispatch every listener to a pooled thread. FireSync remains the cheapest option here because it ignores the async flag and calls every listener directly on the firing coroutine.

ListenersFireSyncFireFireAsyncFireDeferredFireSafe
100.046 ms0.181 ms0.165 ms0.099 ms0.243 ms
1000.371 ms1.596 ms1.466 ms0.11 ms2.411 ms
5001.743 ms7.63 ms7.499 ms0.12 ms13.953 ms
1,0003.744 ms16.346 ms15.859 ms0.122 ms23.979 ms
5,00019.085 ms81.308 ms81.628 ms0.136 ms116.704 ms

FireSafe is most expensive here because it deep-copies table arguments and wraps every listener in pcall — costs stack with listener count.


Once Listeners (Reconnect)

Connection pool reuse dominates here. Each fire disconnects the listener, runs the callback, then the test immediately reconnects. Costs are dramatically lower than persistent listeners because the pool eliminates allocation overhead.

ListenersFireSyncFireFireAsyncFireDeferredFireSafe
100.011 ms0.011 ms0.015 ms0.109 ms0.015 ms
1000.025 ms0.026 ms0.039 ms0.100 ms0.037 ms
5000.088 ms0.089 ms0.151 ms0.102 ms0.136 ms
1,0000.174 ms0.178 ms0.294 ms0.161 ms0.265 ms
5,0001.004 ms0.943 ms1.646 ms0.131 ms1.507 ms

FireDeferred is unusually slow at low counts (9.83× at 10 listeners) — the task.defer overhead dominates when the actual fire work is near-zero. It recovers at 1,000+ listeners where the deferred cost becomes proportionally negligible.


What These Numbers Mean in Practice

10–100 listeners — all methods are fast. Pick based on semantics, not performance.

100–1,000 listeners, all-syncFire and FireSync are equivalent. Avoid FireAsync and FireSafe on hot paths.

100–1,000 listeners, mixed or all-asyncFireSync is 3–4× faster than Fire if you can tolerate blocking async listeners. FireDeferred can offload execution to the next cycle, but the listeners still run — just later.

1,000+ listenersFireDeferred has near-zero firing cost because it only queues work. Useful when you need to unblock the current frame. The listener execution cost still hits on the deferred cycle.

Once-heavy patterns — the connection pool makes reconnect-heavy code far cheaper than it looks. At 1,000 Once listeners, Fire costs 0.178ms — less than half the cost of 1,000 persistent async listeners.


Running the Benchmarker

The benchmarker that produced these results is included as Benchmarker.lua in the src folder.

Setup

  1. Place VeSignal somewhere accessible (e.g. ReplicatedStorage)
  2. Place Benchmarker.lua in ServerScriptService
  3. Add an ObjectValue named SignalReference as a child of the script, with its Value pointing at the VeSignal ModuleScript
  4. Require and run it from a Script:
local VeSignalBenchmark = require(ServerScriptService.Benchmarker)

local Benchmark = VeSignalBenchmark.new()
Benchmark:Run()

Configuration

VeSignalBenchmark.new() accepts an optional config table:

local Benchmark = VeSignalBenchmark.new({
ListenerCounts = { 10, 100, 500, 1000 }, -- which counts to test
SampleFrames = 120, -- Heartbeat frames sampled per cell
WarmupFrames = 30, -- frames discarded before sampling
FiresPerFrame = 100, -- fires issued per frame
BaselineMethod = "FireSync", -- method used for ratio calculations
})

All fields are optional — unset fields fall back to the defaults used for the numbers above.

Reading the Output

Each cell prints as it finishes:

FireSync  | All-sync listeners  |  100 listeners | avg 0.354 ms  min 0.294  max 0.542  σ 0.071 | 282634 fires/s
Fire | All-sync listeners | 100 listeners | avg 0.375 ms min 0.293 max 0.639 σ 0.087 | 266405 fires/s
→ Fire / FireSync ratio: 1.061x [FireSync FASTER]

The σ column is standard deviation. High σ relative to the average means the frame time was inconsistent — usually GC pressure or Roblox scheduler noise during that window. Treat high-σ rows with skepticism and re-run if needed.