Lua Scripting

ModuleScript: require runs once, and that is what almost nobody knows

A ModuleScript is how you reuse code in Roblox. The rule that explains half the hard bugs: require runs the module only once and always returns the same reference — and on the client and the server, one for each side.

by Gustavo Cantino9 min readBeginner

Produced with AI from the sources cited below, and checked by automated verification against those same sources. No person read this text before publication. Editorial Policy

Typographic plate with the Lua Scripting category in large type, the beginner level below it, and a field of vertical lime green bars on a dark background.

After writing a few scripts, the moment always arrives when you want to use the same code in two places. Roblox's answer to that is the ModuleScript, and it is simple to use and easy to use wrongly — because it has a behaviour that is not obvious and that explains half the hard module bugs.

The behaviour is this: require runs the module only once. This guide starts with the basics and ends on the consequences of that.

The problem

Without modules, repeated code is copied. Copying works until the first fix: you correct it in one place and forget the other, and from then on the game has two versions of the same rule disagreeing with each other.

The ModuleScript solves that in a way that even crosses the client-server boundary. In the official definition, it lets you reuse code between scripts on both sides of the boundary or on the same side — depending on where the module lives.

And that is exactly where the first decision lives. A module can sit anywhere a script fits, but ReplicatedStorage is the popular place, because keeping the module there is what lets you reuse code between the server and clients.

The word that matters in that sentence is code, not data. Keeping calculation rules in ReplicatedStorage is excellent. Keeping game state there is handing the client a copy it can read — and, worse, letting it think it is writing to yours.

Step by step

1. Create the module. A new ModuleScript is born with the skeleton: an empty table and a return. That code creates an empty Luau table and returns it to any script that requires it.

The returned value can be any type except nil, but most modules return a function, a table, or a table of functions.

2. Prefer returning a table. It is not mandatory — you can return the function directly. The documentation recommends the table anyway, and gives two concrete reasons: it gives an easy-to-understand syntax at the call site, and it makes it easier to add functions later without changing how other scripts call it.

In practice: PickupManager.getPickupBonus("legendary") reads by itself. PickupManager("legendary") requires remembering what that module was.

3. Load it with require. Loading is through the require function, passing the module object.

On the client, there is a detail the documentation marks as an important safety measure: the WaitForChild pattern. A client script can run before the object has replicated, and without the wait it fails intermittently — the worst kind, because it works on your machine.

4. Use the same module from the other side. Kept in ReplicatedStorage, the same require works from ServerScriptService. That is the gain that justifies the folder.

5. Store data in a module, not in a value object. To associate data with an object, the alternatives are attributes or Configuration folders with value objects. They work for small amounts, but the documentation is direct: both become problematic with dozens of objects or values.

The decisive argument is capacity: attributes and value objects do not store tables or functions. A module does. A weapon configuration with damage per body part is a nested table — and that does not fit in an IntValue at all.

The rule the documentation gives for deciding is about repetition: if you want to modify the same data across several copies of the same object, or reuse the same data across different objects, store it in the module. An attribute is good for what belongs to that specific object and only it — the colour of that door, the number of that room. The instant the same value appears on two objects, it has become configuration, and configuration lives in a module.

There is an operational gain that only shows up later, and it is what settles the argument for good. With the data scattered in attributes, changing a weapon's damage means finding every object of that weapon in the game and editing them one by one — including the ones inside models, inside folders, inside other models. With the data in a module, it is one line. And, more importantly, it is a line that shows up in the repository history, with a date and an author, instead of an invisible change to an object property nobody can say when happened.

The artifact

The module below shows the two uses side by side, because the difference between them is the thing that confuses people most at the start.

The first half is configuration: constants and a calculation function. Everyone reads the same thing, nobody writes, and there is nothing to go wrong.

The second half is state: a table the scripts modify. There the require rule stops being a curiosity and becomes the central behaviour — and that is the subject of the next section.

Common mistakes

Three mistakes, and the first is the one that gives this text its title.

"Each script has its own copy." It does not. When you call require on a ModuleScript, it runs once and returns a single item as a reference. Calling require again returns exactly the same reference — so a table you modified stays modified on subsequent calls. The module itself does not run again.

That is excellent when you want it: two server scripts share a counter with no plumbing at all. And it is a trap when you do not know: a module that keeps "the current enemy" ends up with a global current enemy, and two systems that were meant to be independent start interfering with each other through a route that appears nowhere in the code.

The question that settles it case by case: if two different places use this module at the same time, should they see the same value? If yes, store it in the module. If not, the module needs to return a function that creates the thing, not the thing ready-made.

"The client wrote and the server did not see it." This is the second half of the same rule, and it catches people who already understood the first. If you require a ModuleScript from both sides of the boundary, it returns a unique reference for each side.

Meaning: there are two live copies, one on the server and one on each client, and they do not talk. A value written on the client does not appear on the server, and the reverse is also true. Nothing goes wrong, no error is raised — the number simply does not go up on the other side, and the person spends hours looking for the bug in the wrong place.

The practical rule: a module with state that belongs to the server lives in ServerStorage or ServerScriptService. Then the client cannot even require it, and the ambiguity stops existing. ReplicatedStorage holds rules and constants — things it does not matter to duplicate.

The circular require. The documentation warns against having modules require each other in a circle, which produces the module required recursively error.

The error is honest, but the cause is tedious to find, because the circle is rarely two long. It is A requires B, B requires C, C requires A — and none of the three looks wrong alone. The warning sign is a module needing another just to call one small function: almost always that function belongs to a third module that does not exist yet.

The way out is usually extracting, not patching. If A and B need each other, there is something in common between them that wants to be a module C — and once extracted, A and B depend on C without depending on each other. Moving the require inside the function, which is the hack that fixes it on the spot, makes the error disappear and leaves the cycle standing for the next person to find.

Next step

With modules in place, the next pattern the documentation presents solves a real annoyance: custom events let scripts talk, but tracking a reference to every BindableEvent clutters the code. Keeping the events inside a module, and exposing functions to subscribe and fire, takes that mess out of the way.

Before that, a tidying exercise is worth doing. Open your game and look for the same number written in two different scripts — damage, price, speed, cooldown. Every repeated occurrence is a constant that should be in a module, and each one is a future bug waiting for somebody to fix only one side.

And when you move it, move the logic that uses the number with it. A module that only has constants becomes a list nobody remembers to update; a module that has the constant and the function that applies it is the obvious place to go when the rule changes.

Sources

How we correct mistakes

Ready-to-use artifact

A configuration module and a module with state, showing what require shares (ReplicatedStorage)
lua
-- ReplicatedStorage > ModuleScript called "Economy"
--
-- This file shows the TWO uses of a module and the difference between them,
-- which is the thing that confuses people most at the start:
--
--   CONFIGURATION -> read only. Everyone reads the same thing, and that is fine.
--   STATE         -> written to. Everyone writes to the SAME place, and then
--                    you need to know exactly who has access.

local Economy = {}

-- ─────────────────────────────────────────────────────────────────
-- 1. CONFIGURATION — the safe use
-- ─────────────────────────────────────────────────────────────────
-- This replaces attributes and Configuration folders: it holds a TABLE,
-- which a value object cannot, and it is all on one screen.

Economy.DEFAULT_MULTIPLIER = 1.25

Economy.RARITY = {
	common = 10,
	uncommon = 20,
	rare = 50,
	legendary = 100,
}

function Economy.bonusFor(rarity)
	local base = Economy.RARITY[rarity]
	if not base then
		warn("unknown rarity: " .. tostring(rarity))
		return 0
	end
	return base * Economy.DEFAULT_MULTIPLIER
end

-- ─────────────────────────────────────────────────────────────────
-- 2. STATE — the use that demands attention
-- ─────────────────────────────────────────────────────────────────
-- `total` is created ONCE, when the module runs for the first time.
-- Every script on the SAME side that requires it receives the same table,
-- so whoever adds here adds for everyone.
--
-- That is why this is useful: two server scripts share a counter without
-- inventing any plumbing.
--
-- And that is why this is dangerous: if this module lives in
-- ReplicatedStorage, the CLIENT can require it too. The client gets its
-- OWN copy -- not the server's -- so nothing it writes here reaches the
-- server. Anyone expecting otherwise will debug a number that does not
-- go up.

local total = {}

function Economy.credit(userId, amount)
	total[userId] = (total[userId] or 0) + amount
	return total[userId]
end

function Economy.balanceOf(userId)
	return total[userId] or 0
end

return Economy

--[[
HOW TO USE IT, from both sides:

  -- Script in ServerScriptService
  local ReplicatedStorage = game:GetService("ReplicatedStorage")
  local Economy = require(ReplicatedStorage:WaitForChild("Economy"))
  print(Economy.bonusFor("legendary"))

  -- LocalScript
  local ReplicatedStorage = game:GetService("ReplicatedStorage")
  local Economy = require(ReplicatedStorage:WaitForChild("Economy"))
  print(Economy.bonusFor("legendary"))   -- same result: it is read only
  Economy.credit(1, 100)                 -- does NOT reach the server

The WaitForChild is not decoration: on the client, the object may not have
replicated yet when the script runs.

IF THE STATE BELONGS TO THE SERVER, the module should live in ServerStorage or
ServerScriptService. Then the client cannot even require it, and the
ambiguity above stops existing.
]]
In this article

Keep reading