This article explains why you should be using signals in your code as well as the benefits signals can provide compared to alternatives like BindableEvents. Note: This article was touched up with an AI content writer, however, everything here has been looked over the ensure that all the information here is up-to-date and accurate.
This article is an more in-depth explanation for a reply I posted a few months back:
With that being said, lets get started!
If you’re building anything remotely complex on Roblox, you’ve already hit the wall. You know the one. Your Inventory system needs to talk to your UI, which then needs to update the DataStore, and somehow, everything ends up broken.
How are you handling that right now?
If your answer involves a spaghetti bowl of callbacks, while wait() do loops, or—God forbid—a pile of BindableEvents sitting in a Folder in the Workspace, stop. Just stop. You need Signals.
We’re going to break down what a Signal class actually is, why BindableEvents are secretly sabotaging your architecture, and how to stop writing code that looks like it was pasted together by a tired intern.
What the heck is a Signal?
Simply put, a signal just stores a list of callbacks and iterates through those callbacks when the signal is fired, forwarding the passed in parameters.
It’s a implementation of the Observer Pattern (I highly recommend checking this website out, it has great information).
But let’s skip the CS degree jargon. Think of it exactly like a built-in Roblox event, like Part.Touched or Player.PlayerAdded. The difference is that you aren’t relying on some physical Instance created by the engine. You’re building the event purely in Luau code.
The “Radio Station” Analogy
Picture your game logic as a Radio Station.
The Signal: The frequency.
The Firing: The DJ blasting a track.
The Connection: The listeners in their cars.
The DJ (your script) doesn’t care who is listening. It just broadcasts. If zero people are listening? Fine. If a thousand are listening? Also fine. That’s the beauty of Event-Driven Architecture.
Why BindableEvents are actually terrible
“But BindableEvents work fine!”
Do they, though? I see beginners lean on them constantly, but they come with three massive architectural headaches that will bite you later.
1. Parameter Mangling
This is the big one. When you fire a BindableEvent, Roblox copies your data and send it to your each of your active connections.
This process deep-copies tables.
If you pass a table and modify it in the receiving script, the original table doesn’t change. This is because, internally, Roblox passes a copy instead of a reference to the original table. If your system relies on a metatable being passed through you’ll notice it gets stripped as you’re passing by copy and not reference.
2. Instance Clutter
You have to create physical folders of instances in ReplicatedStorage just to handle logic? That’s messy. It’s 2025 (or later, depending on when you read this). We shouldn’t be using physical objects to handle abstract logic.
You might be thinking to yourself, “Why not just parent a bindable event to nil and store a reference to it?”. Yeah that works! but you’ll still be missing out on all the other benefits a custom signal implementation can provide.
3. Performance
Creating and destroying Instances is expensive. Allocating a lightweight Lua table (which is all a Signal is) is wicked fast.
The bottom line: Signals let you pass complex data types—metatables, circular references, whatever—without Roblox stripping the data or breaking the memory address.
How to actually use a Signal
There are plenty of solid open-source options out there (stravant’s GoodSignal is my #1 recommendation).
Here is what it looks like in practice. Let’s clean up a messy scenario.
The Scenario: A Player Levels Up
This is a general usage scenario for both signals and BindableEvents.
The “Old” Way (Hard Dependencies):
You try to require the UI script directly inside the Level script. Now they are married. If you delete the UI, the Level script throws an error. It’s fragile.
The “Signal” Way (Decoupled):
-- In your 'LevelSystem' Module
local Signal = require(path.to.Signal)
local LevelSystem = {}
LevelSystem.OnLevelUp = Signal.new() -- Create the signal
function LevelSystem:AddXP(amount)
self.CurrentXP += amount
if self.CurrentXP >= self.MaxXP then
self.Level += 1
-- Fire the signal! We don't care WHO is listening.
self.OnLevelUp:Fire(self.Level)
end
end
return LevelSystem
Now, the rest of your game can just tune in:
-- In a UI Script
local LevelSystem = require(path.to.LevelSystem)
-- Connect to the signal
LevelSystem.OnLevelUp:Connect(function(newLevel)
print("Congratulations! Reached level:", newLevel)
SoundService.LevelUpSound:Play()
end)
-- In a Particle Effect Script
LevelSystem.OnLevelUp:Connect(function(newLevel)
ParticleEmitter:Emit(50)
end)
See that? LevelSystem has no clue the UI or the Particles even exist. That is decoupling. That is clean.
With signals, systems communicate without holding hands. You can delete the UI script entirely, and your Level system won’t error—it just screams its signal into the void, and nobody answers. That makes debugging a breeze.
Wrapping this up
I hope this article helped you guys understand signals more. Lmk if you guys enjoy content like this and, if so, I’ll make more in the future! If there are any specific topics you’d like me to cover in the future, let me know as well.
Was this article of use to you?
- Yes, it helped me understand why people use signals

- No, I already knew everything discussed here

- No, I’m even more confused now

