ModuleScripts in Roblox Explained (With Examples)
Quick answer: A ModuleScript is a script that runs nothing on its own; when you call require() on it, Luau executes it once per environment (client or server), returns whatever the module returns — usually a table of functions — and caches that result so every later require() gets the same table.
A ModuleScript in Roblox is a special kind of script that does not run by itself. Instead, it waits until another script calls require() on it, executes its code exactly once per environment, and hands back whatever value it returns — almost always a table containing functions and data. This makes ModuleScripts the standard way to organize reusable code, configuration, and shared state instead of copying the same logic into every Script and LocalScript.
How require() actually works
When any script calls require(module), three things happen:
- Luau checks its cache for that module in the current environment.
- If it has not run yet, the ModuleScript's code executes from top to bottom.
- The value the module returns is stored in the cache and given to every future caller.
The caching behavior matters. A module's top-level code runs only once per environment, no matter how many scripts require it:
-- ReplicatedStorage/RewardConfig (a ModuleScript)
print("RewardConfig loading...")
local RewardConfig = {}
RewardConfig.dailyCoins = 250
RewardConfig.levelUpMultiplier = 1.5
function RewardConfig.coinsForLevel(level)
return math.floor(RewardConfig.dailyCoins * level * RewardConfig.levelUpMultiplier)
end
return RewardConfig
If five different server scripts call require() on this module, you will see "RewardConfig loading..." printed exactly one time on the server. Every caller receives the same table, so changes made through one reference are visible everywhere.
Why return a table?
A ModuleScript can technically return any value — a number, a string, even nothing (which gives back nil). In practice, returning a table is the convention because a table can carry multiple functions and values, like a toolbox:
-- ServerScriptService/InventoryUtils
local InventoryUtils = {}
function InventoryUtils.countItems(inventory)
local total = 0
for _, amount in pairs(inventory) do
total += amount
end
return total
end
function InventoryUtils.addItem(inventory, item, amount)
inventory[item] = (inventory[item] or 0) + amount
end
return InventoryUtils
Any server Script can now do:
local InventoryUtils = require(game.ServerScriptService.InventoryUtils)
local backpack = {}
InventoryUtils.addItem(backpack, "Gold", 10)
print(InventoryUtils.countItems(backpack)) -- 10
For deeper object-oriented patterns built on top of returned tables, see setmetatable OOP in Roblox. And if you want to brush up on how those tables work under the hood, Luau tables, arrays, and dictionaries covers the foundation.
Where should I put my ModuleScripts?
Placement controls who can require the module:
- ReplicatedStorage — visible to both client and server. Use it for shared config, pure utility functions, and data definitions.
- ServerStorage or inside ServerScriptService — server-only logic such as database helpers or anti-cheat math.
- Inside a LocalScript hierarchy or StarterPlayerScripts — client-only UI helpers.
One subtlety trips up almost everyone: the cache is per environment. If both a Script and a LocalScript require the same module from ReplicatedStorage, the module's code runs twice — once on the server and once on each joining client. The two environments get two independent copies of the table. Setting Config.score = 100 on the server does not change what clients read; state that must stay in sync belongs in DataStores, attributes, or remote events (remote events guide).
The singleton pattern
Because require() caches the result, a module is naturally a singleton: one shared instance per environment. That makes ModuleScripts perfect for central managers — a single point of truth for game state:
-- ServerScriptService/GameManager
local GameManager = {}
GameManager.playersInRound = {}
GameManager.roundActive = false
function GameManager.addPlayer(player)
GameManager.playersInRound[player] = os.clock()
end
function GameManager.removePlayer(player)
GameManager.playersInRound[player] = nil
end
function GameManager.startRound()
GameManager.roundActive = true
local count = 0
for _ in pairs(GameManager.playersInRound) do
count += 1
end
print(("Round started with %d players"):format(count))
end
return GameManager
Now the round system, the shop system, and the leaderboard can all require the same GameManager and agree on who is in the round — without any direct wiring between them. If your manager needs methods attached to player objects rather than plain functions, combine this with the metatable techniques in setmetatable OOP in Roblox.
Pitfall: circular requires
If module A requires module B at the top level, and B requires A back, one of them will receive an incomplete value because the other has not finished running yet. Luau detects a cycle mid-execution and errors, or silently hands back a half-built table depending on where the cycle lands.
The fix is to move the require inside the function that needs it, deferring it until both modules are fully loaded:
-- Combat (a ModuleScript)
local Combat = {}
function Combat.hit(target)
local Effects = require(script.Parent.Effects) -- required lazily
Effects.sparkle(target.Position)
end
return Combat
Better yet, restructure so neither module depends on the other — pass dependencies as arguments instead of reaching for them.
Other common mistakes
Requiring a module whose top-level code yields (for example, calling wait() or yielding DataStore calls before returning) blocks every script waiting on it. Keep module initialization instant; do slow work later, wrapped in pcall.
Also remember that requiring the same module through two different paths (say, once via a variable pointing at the instance and once via game.ReplicatedStorage...) still hits the same cache entry — the cache is keyed by instance, so this is safe.
Learn Luau by doing
Reading about modules clicks much faster once you refactor something real. At Luablox you can work through interactive lessons at /theory, solve hands-on challenges at /problems, or follow structured Luablox lessons that walk you from first script to full game systems.
Frequently asked questions
What does require() return for a ModuleScript?
It returns whatever value the ModuleScript ends with via a return statement. Most modules return a table of functions and data; if a module returns nothing, require() returns nil.
Does a ModuleScript run more than once?
No — within one environment (server or client) the module code runs only the first time it is required. The result is cached, so every later require() returns the same table. However, the server and each client each maintain their own separate cache, so shared modules effectively run once per machine.
Where should I put a ModuleScript?
Put shared modules in ReplicatedStorage so both client and server can require them, server-only modules in ServerStorage or ServerScriptService, and client-only helpers near your LocalScripts. Placement determines which scripts are allowed to see the module.
Why do I get an error about circular requires?
When two modules require each other at the top level, one of them tries to use a module that has not finished executing yet. Move the require() call inside the function that uses it (lazy require), or restructure the code so the modules do not depend on each other.
Can ModuleScripts hold changing state?
Yes, per environment. Because require() returns a cached table, fields you write persist across all callers in the same environment — the classic singleton pattern. But server-side changes are invisible to clients, since each environment caches its own copy.
Learn how to share code and architect clean, modular Roblox systems using ModuleScripts and require().