ワンクリックで
roblox-sharp-edges
12 production footguns ranked by severity. Data loss, exploits, memory leaks, mobile perf.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
12 production footguns ranked by severity. Data loss, exploits, memory leaks, mobile perf.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Service hierarchy, 7 foundational patterns, cross-platform input. Client-server architecture, module patterns, framework options.
Luau language fundamentals, type system, OOP, deprecation table, error patterns.
Roblox AnalyticsService: custom events, economy tracking, funnels, rate limits, event taxonomy.
Animations, particles, tweens, ContentProvider, visual effects.
Code review with security, performance, and monetization lenses for Roblox projects
DataStores, ProfileStore, session locking, data persistence patterns.
| name | roblox-sharp-edges |
| description | 12 production footguns ranked by severity. Data loss, exploits, memory leaks, mobile perf. |
| last_reviewed | "2026-05-22T00:00:00.000Z" |
Every entry here represents a real production footgun that has caused data loss, exploits, crashes, or hours of debugging in Roblox games.
Severity Levels:
- Critical - Data loss, security breach, or revenue loss. Fix before shipping.
- High - Server instability, degraded experience, or exploit surface. Fix in current sprint.
- Medium - Correctness bugs, performance issues, or dev confusion. Fix before scale.
- Low - Code quality, maintainability, or minor timing issues. Fix when convenient.
See roblox-data → Session Locking and ProfileStore for full details.
When a player server-hops, the old server may still be saving while the new server loads stale data. ProfileStore handles session locking automatically - only one server owns a player's data at a time. Never use raw DataStoreService for player data without session locking.
See roblox-networking → Security Hardening for full details.
Currency and all authoritative game state must live exclusively on the server. Never accept currency amounts from the client. The server computes all transactions internally and pushes display-only updates to the client. This is the single most common exploit in Roblox games.
See roblox-monetization → ProcessReceipt for full details.
MarketplaceService.ProcessReceipt must return PurchaseGranted ONLY after the item is granted AND saved. If you return PurchaseGranted before granting, the player loses Robux. If you don't return it, Roblox retries on every join - potentially granting duplicates. Grant first, save second, return third.
Every :Connect() returns an RBXScriptConnection. If you never :Disconnect() it, the connection persists for the script's lifetime - even after the object is destroyed. In per-player systems, memory grows linearly with every player who has ever joined.
Use the vendored Trove module (vendor/rbxutil/trove/) to group connections per-player and clean them all on PlayerRemoving:
local Players = game:GetService("Players")
local Trove = require(game.ReplicatedStorage.Packages.Trove)
local playerTroves: { [Player]: typeof(Trove.new()) } = {}
local function onPlayerAdded(player: Player)
local trove = Trove.new()
playerTroves[player] = trove
trove:Connect(player.CharacterAdded, function(character)
local humanoid = character:WaitForChild("Humanoid")
trove:Connect(humanoid.Died, function()
task.wait(3)
player:LoadCharacter()
end)
end)
end
local function onPlayerRemoving(player: Player)
local trove = playerTroves[player]
if trove then
trove:Clean()
playerTroves[player] = nil
end
end
Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(onPlayerRemoving)
RemoteEvents have no built-in rate limiting. Exploiters can fire thousands of times per second, flooding the server with DataStore calls, instance creation, or raycasts.
Implement per-player, per-remote rate limiting on the server. See roblox-networking → Rate Limiting for production patterns.
Minimal inline example:
local lastFire: { [Player]: number } = {}
local COOLDOWN = 0.1
AttackRemote.OnServerEvent:Connect(function(player: Player, targetId: number)
local now = os.clock()
if lastFire[player] and now - lastFire[player] < COOLDOWN then return end
lastFire[player] = now
-- process attack
end)
See roblox-data → Best Practices (BindToClose Handler) for full details.
game:BindToClose() gives at most 30 seconds. If using ProfileStore, this is automatic. If using raw DataStore, save all players in parallel with task.spawn - sequential saves with 50 players will timeout.
Mobile devices struggle above ~10,000 visible parts. Enable StreamingEnabled and configure StreamingMinRadius/StreamingTargetRadius. Use ModelStreamingMode to mark distant models as Opportunistic and gameplay-critical models as Persistent.
See roblox-runtime → StreamingEnabled for configuration details.
require() executes the module body synchronously. If it yields (WaitForChild, task.wait, HTTP), every script requiring that module blocks. Two modules requiring each other with yields = deadlock.
Never yield in a module body. Use Init/Start lifecycle:
local CombatSystem = {}
function CombatSystem:Init()
-- WaitForChild is safe here (called by bootstrap, not during require)
self._remotes = game.ReplicatedStorage:WaitForChild("Remotes", 10)
end
function CombatSystem:Start()
-- Connect events after all modules are Init'd
end
return CombatSystem
Bootstrap script calls :Init() on all modules, then :Start() on all modules.
# is only reliable for sequence tables (consecutive integer keys, no nil gaps). Setting tbl[3] = nil creates a hole; #tbl may return any valid boundary.
nil. Use table.remove() to shift elements.for _, v in tbl do) instead of for i = 1, #tbl.See roblox-luau-mastery → Task Library for full details.
Replace wait() → task.wait(), spawn() → task.spawn(), delay() → task.delay(). Legacy functions have minimum yield issues, unpredictable timing, and swallow errors.
WaitForChild(name) without a timeout yields forever if the child never appears. Common with renamed instances, StreamingEnabled, or race conditions.
Always pass a timeout. Handle nil return:
local folder = ReplicatedStorage:WaitForChild("Weapons", 10)
if not folder then
warn("[Init] Weapons folder not found after 10s")
return
end
Luau uses Lua string patterns, not regex. \d doesn't work - use %d. Escape with % not \. No alternation (|), no non-greedy *? (use - instead), no lookahead.
%d not \d%w not \w%s not \s%. not \..- not .*?%: %%Luau has no hoisting. A local function is invisible to code above its declaration. AI assistants frequently place helper functions below the functions that call them, causing nil-value runtime errors.
Callees above callers. Always. If functionA() calls helperB(), then helperB must be declared first.
-- BAD: helperB is nil when functionA runs
local function functionA()
helperB() -- ERROR: attempt to call a nil value
end
local function helperB()
print("I'm a helper")
end
-- GOOD: helper declared first
local function helperB()
print("I'm a helper")
end
local function functionA()
helperB() -- works
end
Use forward declaration:
local functionB -- forward declare
local function functionA()
functionB()
end
function functionB() -- note: no 'local' (already declared above)
functionA()
end
CRITICAL (fix before shipping):
SE-1 DataStore session locking → Use ProfileStore
SE-2 Client-side currency → Server-authoritative only
SE-3 ProcessReceipt order → Grant THEN PurchaseGranted
HIGH (fix in current sprint):
SE-4 Undisconnected events → Trove pattern (vendored)
SE-5 RemoteEvent flooding → Per-player rate limiter
SE-6 BindToClose 30s timeout → Parallel saves with task.spawn
MEDIUM (fix before scale):
SE-7 Mobile part count → StreamingEnabled + <10K parts
SE-8 Yielding in module require → Init/Start lifecycle pattern
SE-9 Table # with nil gaps → table.remove or explicit length
SE-11 Infinite yield WaitForChild → Always pass timeout parameter
SE-13 Local function order → Callees above callers (no hoisting)
LOW (fix when convenient):
SE-10 Deprecated wait/spawn/delay → task.wait/spawn/delay
SE-12 String patterns vs regex → %d not \d, % not \