Skip to content

Introduction ​

What are Signals? ​

A Signal is an implementation of the Observer Pattern. They're used to manage communication between different scripts, promoting loose coupling and readability, resulting in a more modular and maintainable codebase.

Roblox Signals vs. Web Signals

Not to be confused with modern web development terminology, Signals here refer to Event Emitters, not reactive state primitives!

Why use NamedSignal? ​

Rather than go all out on a single selling point, NamedSignal balances performance, developer experience, and correctness.

Most notably, we support named parameters (hence the name, NamedSignal), which lends you more useful information in lambdas than the type alone.

We mirror the standard signal API, along with additional features seen in some other libraries like Connection:Reconnect(), so switching to NamedSignal is as simple as swapping modules out!

Comparisons ​

See how NamedSignal compares to alternatives!

vs. BindableEvent

The BindableEvent with the RBXScriptSignal and RBXScriptConnection it provides access to form the Roblox engine's implementation of the event emitter pattern.

Bindables established the convention that many signal libraries continue to follow and take inspiration from, however they suffer from shortfalls regarding data marshalling, and lack modern amenities such as type-checking.

FeatureNamedSignalBindableEvent
Argument Limitations✅ No Limitations❌ C++ Marshalling
Values are passed directly and untouched.
Passes arguments directly, preserving tables, references, metatables, and other values.
Engine deep-copies and serializes values.
Mixed tables break; arbitrary keys are unsupported; cyclic tables cause an error; metatables, special objects, identities are lost, and other limitations apply
Type-checking✅ Full Support❌ No Support
Type-checking for signal arguments in all relevant contexts, and parameter names[1] in function auto-fill.Lacks any interface to define types or parameter names.
Object Lifecycle📏 Simple🍝 Messy
Single-line creation with Signal.new().
Simple clean-up with convenient methods, or just 'forget and GC'.
Boilerplate Instance.new() creation for every event.
May cause memory leaks if not explicitly cleaned-up, which requires tracking connections, or destruction of the object.
Performance🪶 Low overhead Luau🪨 Heavy engine overhead
Luau implementation with several optimizations and low overhead.C++ engine marshalling deep-copies and serializes values, degrading performance with larger payloads.

[1] Parameter names are available when using the standard Signal type, which uses function signatures (...) -> () to enable parameter naming.

vs. GoodSignal/RbxUtil Signal

Stravant's GoodSignal is the de facto standard of Roblox signal libraries, with sleitnick's RbxUtil fork being a direct extension of it.

As sleitnick's fork is extremely similar, just with some added methods and types, we'll compare with only RbxUtil Signal for simplicity.

FeatureNamedSignalRbxUtil Signal
Type-checkingNamed Parameters and Wrap InferenceStandard Generic Types
Uses type functions to facilitate enhanced type information, supporting parameter names and Signal.wrap() inference.
Reduces the need to refer to documentation to identify parameters.
Has type-checking but lacks parameter names.
Necessitates referral to documentation when types alone are insufficient.
Thread Edge Cases✅ Safely caught and handled⚠️ No coroutine.status check
Cached threads are checked before reuse, ensuring listeners are always dispatched correctly.Doesn't check thread status before reuse, may cause dispatch to fail if a thread is closed externally.
Performance[1]
Always O(1) disconnectsO(n) worst-case disconnects
Uses a doubly-linked list structure to maintain order, while allowing for quick constant-time disconnects in all cases.Uses a singly-linked list structure that does not track the previous node, requiring iteration which degrades performance.
Multi Thread ReuseSingle Thread Reuse
Reuses all created threads to reduce overhead and churn for asynchronous listeners.Reuses a single thread, faster than naive (no cache) for synchronous listeners, but slow churn for async.
Minimized Resumption (Per Async)Naive Resumption (Per Listener)
Resumes threads only when invocation becomes asynchronous (i.e. a listener yields), greatly improving performance for large non-yielding signals.Always resumes a thread per listener, incurring the overhead for each one, which results in poor scaling for multiple connections.

[1] Additional implementation details and benchmarks are covered in the dedicated section.

vs. FastSignal

FeatureNamedSignalFastSignal
Type-checkingNamed Parameters and Wrap InferenceStandard Generic Types
Uses type functions to facilitate enhanced type information, supporting parameter names and Signal.wrap() inference.
Reduces the need to refer to documentation to identify parameters.
Has type-checking but lacks parameter names.
Necessitates referral to documentation when types alone are insufficient.
Thread Edge Cases✅ Safely caught and handled⚠️ No coroutine.status check
Cached threads are checked before reuse, ensuring listeners are always dispatched correctly.Doesn't check thread status before reuse, may cause dispatch to fail if a thread is closed externally.
Performance[1]
Multi Thread ReuseSingle Thread Reuse
Reuses all created threads to reduce overhead and churn for asynchronous listeners.Reuses a single thread, faster than naive (no cache) for synchronous listeners, but slow churn for async.
Minimized Resumption (Per Async)Naive Resumption (Per Listener)
Resumes threads only when invocation becomes asynchronous (i.e. a listener yields), greatly improving performance for large non-yielding signals.Always resumes a thread per listener, incurring the overhead for each one, which results in poor scaling for multiple connections.

[1] Additional implementation details and benchmarks are covered in the dedicated section.

vs. Signal+

FeatureNamedSignalSignal+
Type-checkingNamed Parameters and Wrap InferenceStandard Generic Types
Uses type functions to facilitate enhanced type information, supporting parameter names and Signal.wrap() inference.
Reduces the need to refer to documentation to identify parameters.
Has type-checking but lacks parameter names.
Necessitates referral to documentation when types alone are insufficient.
Thread Edge Cases✅ Safely caught and handled⚠️ No coroutine.status check
Cached threads are checked before reuse, ensuring listeners are always dispatched correctly.Doesn't check thread status before reuse, may cause dispatch to fail if a thread is closed externally.
Performance[1]
Minimized Resumption (Per Async)Naive Resumption (Per Listener)
Resumes threads only when invocation becomes asynchronous (i.e. a listener yields), greatly improving performance for large non-yielding signals.Always resumes a thread per listener, incurring the overhead for each one, which results in poor scaling for multiple connections.

[1] Additional implementation details and benchmarks are covered in the dedicated section.

vs. LemonSignal

LemonSignal is a fairly competent Signal library, featuring the standard and extended signal API of most other libraries.

FeatureNamedSignalLemonSignal
Type-checkingNamed Parameters and Wrap InferenceStandard Generic Types
Uses type functions to facilitate enhanced type information, supporting parameter names and Signal.wrap() inference.
Reduces the need to refer to documentation to identify parameters.
Has type-checking but lacks parameter names.
Necessitates referral to documentation when types alone are insufficient.
Performance[1]
Minimized Resumption (Per Async)Naive Resumption (Per Listener)
Resumes threads only when invocation becomes asynchronous (i.e. a listener yields), greatly improving performance for large non-yielding signals.Always resumes a thread per listener, incurring the overhead for each one, which results in poor scaling for multiple connections.

[1] Additional implementation details and benchmarks are covered in the dedicated section.