Official sources:
- https://create.roblox.com/docs/en-us/studio/developer-console
- https://create.roblox.com/docs/en-us/performance-optimization/identify
- https://create.roblox.com/docs/en-us/performance-optimization/microprofiler
- https://create.roblox.com/docs/en-us/performance-optimization/scene-analysis
- https://create.roblox.com/docs/en-us/studio/optimization/memory-usage
- https://create.roblox.com/docs/en-us/studio/optimization/scriptprofiler
Start with a reproducible failure and identify whether it occurs on the client, server, or both. Choose a debugging or profiling tool based on that evidence.
The debugging mindset
- Reproduce it. If you can't reproduce it, you can't fix it.
- Isolate it. Remove systems until the bug disappears; the last thing removed is the cause.
- Measure it. Use tools instead of guessing.
- Fix one thing at a time. Verify the fix and add a regression test if possible.
- Log defensively. Good logs make future debugging faster.
Logging discipline
Use print, warn, and error deliberately:
printfor normal diagnostics.warnfor recoverable problems you should notice.errorfor programming errors that should stop execution.
Include context in log messages:
warn(string.format("[DataStore] Save failed for %d: %s", userId, tostring(err)))
Avoid logging secrets, player data, or PII.
pcall and assertions
Wrap fallible calls, especially cloud services, HTTP, DataStores, Marketplace:
local ok, result = pcall(function()
return someService:DoSomething()
end)
if not ok then
warn("DoSomething failed:", result)
end
Use assert for internal invariants that should never fail:
assert(config.MaxSpeed > 0, "MaxSpeed must be positive")
Developer Console
Open with F9 in-game or in Studio play mode.
Tabs:
- Log: client/server output, errors, warnings.
- Memory: categorized memory usage.
- Network: HTTP and service requests.
- Server Stats: heartbeat, ping, data ping.
- Script Profiler: record script CPU usage.
- MicroProfiler: capture server dumps.
Toggle Client/Server views to see which side emitted output.
Unit testing with TestEZ
The standard Roblox testing framework is TestEZ. It supports nested describe/it blocks, lifecycle hooks, async tests, and a rich matcher API.
TestEZ example:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TestEZ = require(ReplicatedStorage.DevPackages.TestEZ)
local MathUtils = require(ReplicatedStorage.MathUtils)
describe("MathUtils", function()
it("clamps values", function()
expect(MathUtils.clamp(5, 0, 10)).to.equal(5)
expect(MathUtils.clamp(-1, 0, 10)).to.equal(0)
expect(MathUtils.clamp(11, 0, 10)).to.equal(10)
end)
end)
TestEZ.TestBootstrap:run({ game.ReplicatedStorage.Tests })
For legacy in-Studio tests, TestService:Error(msg) can mark failures visibly in Studio output, but most projects should prefer TestEZ. The scripts folder contains a minimal TestRunner.lua shim only for environments where TestEZ is unavailable.
Luau type checking
Use the Luau language server (luau-lsp) or the Luau type checker in Studio to catch errors before runtime.
Best practices:
- Add type annotations to module exports and pure functions.
- Run
luau-lsp analyzein CI or before committing. - Enable
--!strictfor new modules. - Do not over-type untyped engine APIs; prefer casts at boundaries.
Studio Debugger
Use Studio's built-in debugger for step-through debugging:
- Set breakpoints by clicking the gutter next to a line number.
- Run in Play mode and use the debugger controls to step over, into, or out.
- Inspect the call stack, local variables, and upvalues in the debugger panel.
- The debugger works in both client and server contexts when Studio runs both.
xpcall and debug.traceback
Use xpcall with debug.traceback to capture full stack traces from failures:
local ok, err = xpcall(function()
riskyOperation()
end, debug.traceback)
if not ok then
warn(err)
end
This is especially useful at top-level entry points, scheduled callbacks, and connection handlers where pcall alone discards the stack.
Connection cleanup
Leaked RBXScriptConnection objects are a common source of memory leaks and stale state. Prefer explicit cleanup patterns:
- Use a Maid/Janitor-style collector:
local Maid = require(path.to.Maid)
local maid = Maid.new()
maid:GiveTask(workspace.ChildAdded:Connect(onChild))
maid:GiveTask(RunService.Heartbeat:Connect(onStep))
maid:Destroy() -- disconnects everything
- Use
:Once()for one-shot event handlers. - Use
Instance.Destroyingto trigger cleanup when an instance is removed:
obj.Destroying:Connect(function()
cleanup()
end)
- Always pair
:Connect()with a matching:Disconnect()or destruction.
Prefer task over spawn/delay
The task library is more predictable than legacy spawn, delay, and wait:
-- good
task.wait(1)
task.delay(1, callback)
task.spawn(coroutineOrFn)
-- avoid
spawn(callback)
delay(1, callback)
wait(1)
task.defer is useful for yielding to the next resumption cycle without opening a new thread.
Log rate limiting and secrets hygiene
- Never log secrets, keys, user identifiers, or personal information.
- Sanitize values before logging:
local function sanitize(value)
if typeof(value) == "Instance" and value:IsA("Player") then
return "Player(" .. tostring(value.UserId) .. ")"
end
return value
end
- Rate-limit noisy logs to avoid flooding the console and network:
local lastLog = 0
local function throttledLog(message)
local now = os.clock()
if now - lastLog > 5 then
lastLog = now
warn(message)
end
end
Network ownership
Incorrect network ownership causes jittery physics and replication lag. Visualize ownership and assign it explicitly:
-- Server
part:SetNetworkOwner(player)
-- Client-side visualization (debug only)
local ownershipLabel -- create BillboardGui or use DebugDraw
Use BasePart:GetNetworkOwner() to inspect ownership. Vehicles and held items should usually be owned by the controlling player.
DataStore retry pattern
For retry-safe reads, use bounded retries with exponential backoff and jitter. Do not use this generic loop to replay writes: a failed response can hide a committed write, and independently retried requests can execute out of order. See roblox-datastores for write reconciliation and per-key ordering.
local function readWithRetry(fn, maxAttempts)
maxAttempts = maxAttempts or 5
for attempt = 1, maxAttempts do
local ok, result = pcall(fn)
if ok then
return true, result
elseif attempt == maxAttempts then
return false, result
else
warn("DataStore attempt " .. attempt .. " failed; retrying...")
task.wait(2 ^ attempt * 0.1 + math.random() * 0.5)
end
end
end
Always call DataStores from the server and validate serialization before saving.
Memory snapshot comparison
Use the Developer Console Memory tab or Scene Analysis to compare snapshots:
- Capture a baseline snapshot in a stable state.
- Play through the action that may leak (spawn/despawn enemies, open/close UI).
- Return to the stable state and capture a second snapshot.
- Compare PlaceMemory, Luau heap, and instance counts.
- Investigate categories that did not return to baseline; look for unparented instances in Scene Analysis.
MicroProfiler
Open with Ctrl+F6 (⌘+F6) in Studio or the desktop client.
Use it to:
- Find frame-time spikes.
- Identify whether a bottleneck is script compute, physics, or rendering.
- Capture server dumps from the Developer Console.
- Add custom labels with
debug.profilebegin/debug.profileend.
Key colors:
- Orange: worker thread (scripts, physics, animations) bottleneck.
- Blue: render thread bottleneck.
- Red: GPU wait / render complexity.
Scene Analysis
Available in Studio under Window → Performance Summary → Scene Analysis.
Views:
- Script memory: per-script Luau heap.
- Unparented instances: potential memory leaks held by scripts.
- Instance composition: counts by category.
- Audio/Animation memory: asset memory usage.
- Triangle composition: draw call breakdown.
Scene Analysis is a Studio UI tool; there is no public SceneAnalysisService API.
Script Profiler
Records CPU time per script. Use it when MicroProfiler points to scripts but you need to know which script.
Common bug categories
| Symptom | Likely causes |
|---|---|
| Script silently fails | Missing pcall, error swallowed, wrong script context |
| Data not saving | DataStore called from client, non-serializable value, no pcall |
| Remote not working | Wrong side, handler not connected, argument mismatch |
| Lag spikes | Pathfinding every frame, too many particles, unbatched loops |
| Physics jitter | Wrong network ownership, assembly splits, conflicting constraints |
| NPCs stuck | Blocked path not recomputed, bad agent params, streaming issues |
| UI doesn't update | Property not replicated, wrong parent, layout order |
| Memory grows forever | Leaked connections, unparented instances, cached assets |
Network debugging
- Check Network tab in Developer Console for HTTP/DataStore failures.
- Use
Shift+F3in-game for network debug stats. - Distinguish network ping (round-trip time) from data ping (replication queue).
- Use Studio's beta Network Simulator for separate inbound/outbound latency, jitter, and packet loss. Enable New Device Simulator in Beta Features, restart Studio, then open Test → Device Simulator → Network. Select Apply to activate staged changes. Ideal Fiber still adds 8 ms each way; use all-zero custom values for a zero-added-delay baseline. See network simulation.
Load time debugging
Measure load time:
local start = os.clock()
game.Loaded:Connect(function()
print("Loaded in", os.clock() - start)
end)
Enable Print Join Size Breakdown in Studio Settings → Network to see the largest replicated instances.
Scripts
scripts/TestRunner.lua: a minimal TestEZ fallback with nested suites, lifecycle hooks, matchers, async support, and TestService integration.scripts/Logger.lua: a structured logger with level filtering and guarded formatting.scripts/DebugDraw.lua: utility for drawing rays, points, and boxes in 3D for visual debugging.
How to proceed
- Reproduce the issue reliably.
- Check logs and errors in the Developer Console.
- Determine client vs server scope.
- Use MicroProfiler/Script Profiler for performance issues.
- Use Scene Analysis for memory leaks and scene composition.
- Add targeted logging or tests to confirm the fix.
- Verify on low-end devices and with network simulation when relevant.
- Compare memory snapshots before and after suspected leaks.
Reference index
- common-bugs-and-fixes.md: Trace a symptom to likely causes and a focused fix.
- debugging-tools.md: Inspect logs, breakpoints, stack traces, or runtime state.
- network-simulation.md: Reproduce latency, jitter, or packet-loss problems in Studio.
- performance-profiling.md: Find CPU, GPU, memory, or load-time bottlenecks.
- testing-patterns.md: Write unit tests, mock dependencies, or test client-server flows.