GenericSignal
GenericSignal is a lightweight signal implementation designed for quick runtimes and user-friendliness. GenericSignal objects have the same capabilities as RBXScriptSignals, but they’re much more performant and include a wide range of utility methods to handle additional, niche use-cases.
RBXScriptSignal is Luau’s built-in signal class, used to manage events like Instance.Changed and Player.CharacterAdded, so it’s likely you’ve used one before, even if you didn’t realize it. In reality, RBXScriptSignals are one of the most frequently used features in all of Luau, yet they’re unoptimized and can’t be created manually. Because of this, most developers resort to BindableEvents, which are even less performant.
There are plenty of wrapper classes that address this issue, such as GoodSignal and FastSignal. However, GenericSignal differs from these other modules with its multiple-thread caching system, which reuses coroutines, significantly improving firing performance. GenericSignal objects are lightweight by nature and consequently use very little memory. This, combined with the implementation of the thread cache, gives you much more flexibility when working with multiple signal objects.
GenericSignal objects also store their connections using linked lists, which is optimal for signal implementations because of their frequent insertions, deletions, and shifts. There are some places where GenericSignal performs worse than other modules, but it is considerably more well-rounded and includes plenty of extra features that make up for its shortcomings.
Immediate & Deferred Firing:
The two primary methods of firing a signal are immediate and deferred. Immediate firing (Fire()) executes connections as quickly as possible using task.spawn(), while deferred firing (FireDeferred()) schedules connections to run at the end of the current resumption cycle (always the same frame) using task.defer(). Neither method halts the execution of the current thread.
GenericSignal provides an option for both methods, but using immediate firing is more efficient because it utilizes the thread cache. However, Roblox is gradually shifting towards using only deferred firing, so it’s not a bad idea to do that yourself, especially if the performance impacts of the switch are minimal.
Global Signals & Functions:
You can register GenericSignal objects globally within the local environment by calling GenericSignal:MakeGlobal(name), enabling you to access them anywhere via GenericSignal.globalSignals[name]. There are also a few functions that utilize this system to perform operations on all global signals.
Utility Methods:
GenericSignal objects have all the same functionality as an RBXScriptSignal, but they also include a few new methods to add some extra functionality. This includes methods for memory management, such as Clear() and Destroy(), and some QOL methods, such as AutoDisconnect() and Clone().
QOL Features:
GenericSignal also includes various quality-of-life features that make using the module much more convenient:
- Method chaining support
- Comprehensive type checking
- Error handling for specific methods
- Metamethods for
__call(),__iter(), and__len()
Creating GenericSignal Objects
Creating GenericSignal Objects:
Creating a GenericSignal object only takes nineteen characters: GenericSignal.new(). The constructor doesn’t take any arguments, so all you have to do is call it.
local GenericSignal = require(scriptLocation.GenericSignal)
local exampleSignal = GenericSignal.new()
API:
new: () -> GenericSignal
Managing GenericSignal Objects
Creating GenericConnection Objects:
The two methods used to create GenericConnection objects are Connect() and Once(), each accepting a function as an argument. The function received is used to create a connection, which is subsequently linked to the signal that the method was called on.
Connect() creates a connection and links it to the signal object. Once() is similar, but the connection it creates will automatically disconnect itself after the signal fires once.
exampleSignal:Connect(function(...)
print("signal fired")
end)
-- called every time `exampleSignal` is fired
exampleSignal:Once(function(...)
print("first signal fire")
end)
-- called only the next time `exampleSignal` is fired, then automatically disconnected
Firing GenericSignal Objects:
Both Fire() and FireDeferred() accept any number of arguments of any type. These arguments are passed to all of the signal’s connections and returned to any yielding Wait() calls.
exampleSignal:Fire(args)
-- executes connections as quickly as possible using the thread cache
exampleSignal:FireDeferred(args)
-- schedules connections to run at the end of the current resumption cycle
Managing GenericConnection Objects:
Both Connect() and Once() second return value will be the connection they created, allowing you to manage individual connections.
GenericConnection objects currently have two methods other than Destroy(): Disconnect() and AutoDisconnect(). Both remove the connection from the signal’s list, but Disconnect() runs immediately when called, while AutoDisconnect() accepts another RBXScriptSignal or GenericSignal and only runs when that event fires.
local signal, connection = exampleSignal:Connect(function()
print("connected")
end)
connection:Disconnect()
-- disconnects the connection immediately
connection:AutoDisconnect(game.Players.PlayerRemoving)
-- disconnects the connection when the `PlayerRemoving` event fires
API:
Connect: (self: GenericSignal, func: (...any) -> ()) -> (GenericSignal, GenericConnection)Once: (self: GenericSignal, func: (...any) -> ()) -> (GenericSignal, GenericConnection)Fire: (self: GenericSignal, ...any) -> GenericSignalFireDeferred: (self: GenericSignal, ...any) -> GenericSignalAutoDisconnect: (self: GenericConnection, disconnectEvent: RBXScriptSignal | GenericSignal) -> ()Disconnect: (self: GenericConnection) -> ()
Managing Global GenericSignal Objects
Registering Global Signals:
You can use MakeGlobal() to register a signal globally within the local environment, and MakeLocal() to unregister it. Only MakeGlobal() takes an argument: the string name, which is used to access the signal via GenericSignal.globalSignals[name].
exampleSignal:MakeGlobal("exampleSignal")
-- adds the signal to the `globalSignals` table (access via GenericSignal.globalSignals["exampleSignal"])
exampleSignal:MakeGlobal("otherName")
-- overrides the current global name
exampleSignal:MakeLocal()
-- removes the signal from the `globalSignals` table
Using Batch Operations on Global Signals:
The two functions for batch operations are fireAll() and destroyAll(). Neither takes any arguments, and they do exactly what it sounds like they do.
GenericSignal.fireAll()
-- fires every global signal
GenericSignal.destroyAll()
-- destroys every global signal
Waiting on Multiple Signals:
The other two functions for global signals are waitAny() and waitAll(). waitAny() is just like Wait(), but it yields until any one of the provided signals fires, then returns that signal’s arguments. waitAll() is similar, but instead it waits for all of the provided signals to fire and returns nothing.
local args = GenericSignal.waitAny()
-- wait for any global signal
local customSignals = {
signal1 = exampleSignal1,
signal2 = exampleSignal2
}
GenericSignal.waitAll(customSignals)
-- wait for specific signals
API:
MakeGlobal: (self: GenericSignal, globalName: string) -> GenericSignalMakeLocal: (self: GenericSignal) -> GenericSignalfireAll: () -> ()destroyAll: () -> ()waitAll: (signals: {[string]: GenericSignal}?) -> ()waitAny: (signals: {[string]: GenericSignal}?) -> ...anysignalsprovides a specific set of GenericSignal objects for the function to use, but is not required and will default to theglobalSignalstable
Other Operations with GenericSignal Objects
Waiting on GenericSignal Objects:
The Wait() method yields the current thread until the signal object it was called on is fired, then returns the arguments that the signal was fired with.
local args = exampleSignal:Wait()
-- yields until `exampleSignal` fires, then returns its arguments
FUN FACT: coroutine.yield() returns the arguments of whatever task.spawn() call resumes the coroutine.
Cloning GenericSignal Objects:
Cloning is simple; all you do is call the method Clone(). This creates and returns a shallow copy of the GenericSignal object using the same GenericConnection objects.
local clonedSignal = exampleSignal:Clone()
-- returns a shallow copy of `exampleSignal`
Clearing & Destroying GenericSignal Objects:
Clear() removes all connections from the signal but keeps the signal object intact. Destroy() removes all connections, removes the signal from globalSignals if applicable, and cleans up the OOP table. Neither method accepts any arguments.
exampleSignal:Clear()
-- removes all connections
exampleSignal:Destroy()
-- fully destroys the signal and cleans up memory
API:
Wait: (self: GenericSignal) -> ...anyClone: (self: GenericSignal) -> GenericSignalClear: (self: GenericSignal) -> GenericSignalDestroy: (self: GenericSignal) -> ()
Changelog:
Version 1.0 (release)
Initial release of the ModuleScript
Version Date: 10/11/25
Version Store Page: GenericSignal 1.0
Version File: GenericSignal Source
Most information on the ModuleScript can be found in this post, but if you have any questions, feel free to leave a reply, and I will usually respond within a day or two. Feedback and suggestions are also appreciated, as there is a lot more I plan to do with this module.








