SuperAPI AI Directory

roblox-dev

Coding & development

View repository

Updated 10-15 08:44

About this skill

Roblox Engine

How the Roblox platform behaves at runtime. Sources: create.roblox.com/docs for APIs and concepts; devforum.roblox.com for engineer clarifications and RFCs.

For deep dives see:


Integration Policy

This skill describes preferred patterns; it does not authorize rewriting existing code. Apply rules to new code and code the user explicitly asks to refactor. Match the codebase's existing style (casing, indentation, quote style, module shape) before applying skill defaults. Don't introduce tooling the project doesn't already use; surface conflicts once and default to the existing pattern.

Full rules: ../../shared/integration-policy.md.


Principles

  • Server is authoritative. The client lies. Anything that affects game state (currency, inventory, damage, position-to-trust) lives on the server. The client signals intent; the server validates and applies.
  • State ownership is exclusive. Every piece of state has one owner — server, the controlling client, or replication. Two writers means a race.
  • Replication is async. Anything created server-side appears on clients on a future frame, not the same one. Anything created client-side does not replicate to the server.
  • Yielding is observable. A yielding call inside an event handler runs other handlers in between. Order is not what you wrote.
  • Verify before quoting an API. Roblox's API surface is huge and shifts. State uncertainty rather than hallucinate property names.

Services — GetService Everything

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
  • Use game:GetService("Name") always. Not game.ServiceName.
    • Some services aren't available at script runtime by default — GetService creates them; dot-indexing throws.
    • Some services don't share their class name with their accessor (e.g. RunService is internally "Run Service").
  • Prefer Workspace via GetService over the lowercase workspace global. Both work; the global predates GetService and exists for compatibility. Sticking to GetService keeps every service access one consistent shape and surfaces typos as undefined-variable errors.
  • Put GetService calls at the top of the file, alphabetical, no others scattered later.

DataModel & Instances

  • The DataModel is the tree rooted at game. Services are top-level children. Everything else is an Instance.
  • An Instance is alive until its Parent = nil AND no script holds a reference. Setting Parent = nil is not destruction. Use :Destroy() to disconnect events, mark it as destroyed, and set Parent to nil.
  • :Destroy() is irreversible. A destroyed Instance can't be re-parented; further property access throws.
  • :Clone() requires Archivable = true (the default). Cloning a model includes its descendants; references between siblings are NOT rebound.

Querying Descendants

  • :GetChildren() — direct children only.

  • :GetDescendants() — full subtree, unfiltered. Cheap for small trees, costly for large ones (e.g. a streamed Workspace).

  • :FindFirstChild(name) / :FindFirstChild(name, true) — second arg = recursive.

  • :FindFirstChildOfClass(class) / :FindFirstChildWhichIsA(class) — class-specific direct child.

  • :QueryDescendants(selector: string) → { Instance }. Engine-side filtered descendant query with a CSS-like selector grammar (ClassName, .Tag, #Name, [prop = val], [$attr], [$attr = val], combinators > >> ,, pseudo-classes :not(...) :has(...)). Prefer this over GetDescendants + Luau-side filter for large trees. Thread-unsafe. Full grammar + examples in references/query-descendants.md.

    Workspace:QueryDescendants("MeshPart.SwordPart")              -- MeshParts tagged SwordPart
    Workspace:QueryDescendants("Model >> [$OnFire = true]")       -- attribute-matching descendants of any Model
    Workspace:QueryDescendants(":not(SpotLight, PointLight)")     -- excluding lights
    

WaitForChild Rules

  • On the client, children of replicated containers (e.g. ReplicatedStorage, Workspace) may not exist on the first frame. Use :WaitForChild("Name") for anything you didn't put there yourself.
  • On the server, every Instance you placed in Studio exists at script start; :FindFirstChild is enough.
  • Never call WaitForChild without a timeout in production code — it yields forever if the child never arrives. :WaitForChild("Name", 5) returns nil after 5 seconds.

Script Types & Locations

Three script types:

  • Script — runs on server or client depending on Script.RunContext and location.
    • RunContext = Legacy (default) → server-only, only runs in server containers (ServerScriptService, Workspace, etc.).
    • RunContext = Server → server, can run from ReplicatedStorage too (but don't — clients can read source).
    • RunContext = Client → client, runs from ReplicatedStorage. Recommended for new client scripts (clearer than LocalScript).
  • LocalScript — client-only. No RunContext. Runs only in client containers: StarterPlayerScripts, StarterCharacterScripts, StarterGui, StarterPack, ReplicatedFirst.
  • ModuleScript — require()-able from either side. Runs in the requirer's context.

Recommended layout

Where What goes here RunContext
ServerScriptService Server scripts + server-only ModuleScripts Server
ServerStorage Server-only data (assets, tables) — clients never see contents n/a
ReplicatedStorage Shared ModuleScripts (server + client) and client scripts Client for client scripts
ReplicatedFirst Minimal client loader (e.g. loading screen) Client
StarterPlayerScripts / StarterCharacterScripts / StarterGui / StarterPack LocalScripts that run per-player n/a
Workspace World content. Server-side Scripts for object behaviors. Server if you want explicit

Common practice: keep most code as ModuleScripts in ReplicatedStorage (shared) or ServerStorage/server-only spots, with one server entry-point Script in ServerScriptService that requires the modules, and one client entry-point in ReplicatedStorage (RunContext = Client).

Rules

  • Don't put server logic in ReplicatedStorage. Clients can read source; assume anything there is leaked.
  • Set RunContext explicitly on scripts inside models or packages — removes ambiguity when the model is reparented or shared.
  • Use RunService:IsServer() / :IsClient() for runtime branching in shared ModuleScripts. Don't infer from script.Parent.
  • LocalScripts in Workspace don't run. Client-runnable containers only. Common bug.
  • Server scripts in Workspace run, but RunContext = Server in ServerScriptService is clearer and less surprising when geometry moves.

Source: create.roblox.com/docs/scripting/locations.


Replication

  • Server → client is automatic for replicated containers (Workspace, ReplicatedStorage, Players[X].PlayerGui, StarterGui-cloned-to-PlayerGui). Property changes and parenting changes replicate.
  • Client → server is NOT. Anything a client creates or modifies stays local — the server never sees it. Exception: Players[X].Character (filtering era + character ownership), input events, and explicit RemoteEvent calls.
  • Replication is per-property, not all-or-nothing. Some containers exist on both sides but their contents are client-local (Player.PlayerScripts, Player.PlayerGui after initial replication of StarterGui).
  • Set properties before parenting. Once an Instance is in a replicated container, every subsequent property change replicates as its own update — clients can observe an intermediate state. Build the Instance fully, then set Parent.
  • Signal behavior matters. Workspace.SignalBehavior = Enum.SignalBehavior.Deferred (the modern default in new places) batches signal firings until the next resumption point — code that assumes synchronous-fire ChildAdded is brittle. Test with deferred signals on.
  • See references/replication-model.md for the full table.

RemoteEvents & RemoteFunctions

-- Server
local TakeDamage = ReplicatedStorage:WaitForChild("TakeDamage") :: RemoteEvent
TakeDamage.OnServerEvent:Connect(function(player, amount: unknown)
    if typeof(amount) ~= "number" or amount < 0 or amount > 100 then return end
    -- validated; apply
end)

-- Client
TakeDamage:FireServer(25)
  • OnServerEvent first arg is always the firing Player. It's authentic — Roblox provides it. Don't trust anything else.
  • Type every other parameter as unknown and validate at the boundary. Typing as number makes the type checker accept exploiter input. (See luau-expert skill for the full pattern.)
  • RemoteFunction is :InvokeServer() / :InvokeClient() — synchronous (yields). Avoid client→server InvokeClient when you can; a malicious client can hang the call. Prefer RemoteEvent + reply event.
  • Rate-limit every Remote. Track per-player call counts; reject obvious spam. Default: bounded queue, drop oldest or reject when full.
  • One Remote per logical action. Don't multiplex with a kind field — types stay clearer and validation simpler.

Character Lifecycle

local function onCharacterAdded(character: Model)
    local humanoid = character:WaitForChild("Humanoid") :: Humanoid
    -- ...
end

Players.PlayerAdded:Connect(function(player)
    if player.Character then onCharacterAdded(player.Character) end
    player.CharacterAdded:Connect(onCharacterAdded)
end)
  • A player's first character may already exist when PlayerAdded fires. Check player.Character and connect to future CharacterAdded.
  • Each respawn is a new Model. References to the old character become stale. Reconnect every per-character signal in CharacterAdded; use character.AncestryChanged or humanoid.Died to clean up the previous set.
  • Humanoid.Died fires once per character. After death the model lingers (default ~5s, controlled by Players.RespawnTime) before automatic respawn destroys it.
  • Player.CharacterRemoving fires before the model leaves Workspace — last chance to read state from it.

Streaming

Modern places enable Workspace.StreamingEnabled. Parts of the world load and unload around each player.

  • The client may not see a part the server sees. Never assume client geometry exists — WaitForChild (with timeout) or nil-check.
  • Per-Model streaming is controlled via Model.ModelStreamingMode:
    • Default — model streams normally, descendants stream independently.
    • Atomic — the whole model streams in/out as a single unit (no partial loads).
    • Persistent — the model is always loaded for every client.
    • PersistentPerPlayer — always loaded for players added via the relevant Add/Remove API; off by default for others.
  • Use Atomic for objects whose descendants must arrive together (vehicle assemblies, NPC rigs). Use Persistent sparingly — it costs memory on every client.
  • Replication of property changes still occurs for streamed-out parts; the client just doesn't render them. Don't drive BasePart.CFrame updates on streamed-out objects — wasted bandwidth.
  • See references/streaming-enabled.md for chunk semantics and ModelStreamingMode interactions.

Physics Ownership

  • Each unanchored BasePart has a network owner. Default: server. When a player's character touches/steps on/holds a part, ownership transfers to that player's client.
  • The owner simulates physics; the result replicates back. A client-owned part is not server-authoritative for position — never trust its CFrame for game-state decisions.
  • Force ownership server-side: part:SetNetworkOwner(player) — pass nil to give it back to the server. :GetNetworkOwner() reads it.
  • Anchored parts have no owner. SetNetworkOwner errors on an anchored part. Check with BasePart:CanSetNetworkOwnership() first if you don't control the input.
  • Auto-transfer is the default. Once you call :SetNetworkOwner(player) or :SetNetworkOwner(nil), auto-transfer is off for that part — ownership stays where you set it. Call :SetNetworkOwnershipAuto() to put it back on auto.
  • For anything the player must not be able to teleport (chests, NPCs, projectiles, anti-cheat-relevant parts) — SetNetworkOwner(nil) once, never SetNetworkOwnershipAuto().

Cloud Services (Persistence & Cross-Server)

Roblox offers several storage options. Pick by access pattern, not by reflex.

Service Persistence Scope Use for
Standard data stores (DataStoreService:GetDataStore) Permanent Cross-server User progress, inventory, save data. Numbers/strings/booleans/tables. NoSQL-like.
Ordered data stores (:GetOrderedDataStore) Permanent Cross-server All-time leaderboards (numbers only, sortable).
Memory stores (MemoryStoreService) ≤ 45 days Cross-server Matchmaking, daily/monthly leaderboards, ephemeral cross-server queues. Faster than data stores.
Configs (Creator Hub) Permanent Cross-server Feature flags, tunable values. Read-only from in-game.
Secrets stores Permanent Cross-server API keys, third-party tokens. Read-only from in-game.
In-memory Luau table Session Single server Temporary state (timers, status effects, current health). Free, instant.

Cloud services are server-only. Clients can't read DataStores, MemoryStores, etc. Route any client need through a RemoteEvent.

Data-store core rules

  • Always pcall. Reads/writes throw on network failure, throttling, and quota exhaustion.
  • UpdateAsync for concurrent state (not SetAsync) — gives you the previous value and merges atomically. Return nil from the transform to abort without burning quota.
  • Session-lock pattern: read once on PlayerAdded, mutate in-memory for the session, save on PlayerRemoving + BindToClose. Don't read on every change.
  • game:BindToClose has a ~30-second total budget across all players. Save in parallel via task.spawn.
  • Rate limits are per-key + per-game, refilled over time. There's also a hard ~6-second-per-key rule.

Memory-store core rules

  • Ephemeral: data expires after up to 45 days. Don't put save data here.
  • Useful primitives: queues, sorted maps, hash maps. Higher write throughput than data stores.
  • Server-only API.

See references/cloud-services.md for the full picker (when to use which) and references/datastore-rules.md for data-store rate limits, retry/backoff, and session-locking patterns.

Source: create.roblox.com/docs/cloud-services/data-stores-vs-memory-stores.


Task Scheduling

The task library replaces classic wait/spawn/delay. Use it.

Call When
task.spawn(f, ...) Run f on a new thread immediately (resumes after current resumption point). Use to detach a yielding body from an event handler.
task.defer(f, ...) Run f after the current resumption cycle finishes — later than spawn. Use to coalesce work without yielding.
task.delay(t, f, ...) Run f after t seconds on a new thread. Replaces the legacy delay.
task.wait(t) Yield current thread for t seconds (or one frame if omitted). Replaces wait.
task.cancel(thread) Kill a thread started by task.spawn/task.delay. Pair with explicit cleanup.
task.synchronize() / task.desynchronize() Parallel-Luau actor coordination. See the Parallel Luau section below.
  • Don't use the legacy globals wait, spawn, delay. They throttle (wait clamps to ~30 Hz) and leak in patterns the task versions don't.
  • task.spawn inside an event handler lets fast-firing events run concurrently. Add your own queue/lock if the body mutates shared state.
  • task.defer before task.spawn when you want the work to happen after the current event-handling pass — useful inside :Connect bodies that mutate the world and trigger more signals.
  • Track threads from task.spawn/task.delay if they need cancellation; nothing auto-cancels on Instance destruction.

Buffer Library (Binary Data)

buffer is Luau's fixed-size mutable byte block. Source: buffer.yaml in creator-docs.

Use it for:

  • Compact RemoteEvent payloads (a packed buffer is smaller than a Luau table over the wire).
  • Custom binary serialization for DataStore values you want to keep small.
  • Reading/writing existing binary formats.
  • Replacing string.pack / string.unpack use cases.
local b = buffer.create(16)        -- 16-byte buffer, zero-initialized
buffer.writeu32(b, 0, 0xCAFEBABE)  -- 4 bytes at offset 0
buffer.writef32(b, 4, 1.5)         -- 4 bytes at offset 4
buffer.writestring(b, 8, "abc")    -- 3 bytes at offset 8

local magic = buffer.readu32(b, 0)
local payload = buffer.readstring(b, 8, 3)

Key rules:

  • All offsets are byte offsets from 0. Reading/writing past the buffer raises an error.
  • Little-endian for every numeric read/write.
  • Max buffer size: 1 GiB (buffer.create rejects larger; allocations near that may fail anyway).
  • Sent across Roblox APIs (RemoteEvents, etc.) by copy. Identity is not preserved on the receiver. Don't share a buffer between Actor scripts (Parallel Luau).
  • Fixed size. No grow/shrink — allocate the right size, or copy into a new one (buffer.copy).

Functions in three groups:

  • Lifecycle: buffer.create(size), buffer.fromstring(s), buffer.tostring(b), buffer.len(b).
  • Read/write fixed-width numerics: readi8/u8/i16/u16/i32/u32/f32/f64 and matching write*.
  • Bit-level + bulk: readbits(b, bitOffset, bitCount) / writebits (0–32 bits per call), readstring(b, offset, count) / writestring(b, offset, value, count?), buffer.copy(target, targetOffset, source, sourceOffset?, count?), buffer.fill(b, offset, value, count?).

Full signature reference in references/buffer-library.md.


Parallel Luau (Multithreading)

By default Luau is single-threaded. Parallel Luau lets independent work run on multiple OS threads via the Actor model. Use it for CPU-bound work that can be split: per-player raycast validation, procedural generation, large per-instance computations.

Actor model

  • Scripts under an Actor Instance are eligible to run in parallel.
  • One thread per actor. Multiple scripts in the same actor still run serially with respect to each other.
  • More actors = better load balancing, even on devices with fewer cores than actors.
  • Don't nest actors as descendants of other actors — a script's owning actor is its closest ancestor.

Phases: serial vs parallel

Code runs serially by default. Switch in/out:

-- Inside an Actor's script
RunService.Heartbeat:ConnectParallel(function()
    -- parallel phase
    local result = computeExpensive()
    task.synchronize()
    -- serial phase
    applyResult(result)
end)
  • task.desynchronize() — switch current thread to parallel. Yields, resumes at next parallel-execution opportunity.
  • task.synchronize() — switch back to serial. Required before mutating most Instances.
  • Signal:ConnectParallel(callback) — schedule a signal callback to run in parallel automatically; no need for desynchronize inside.
  • require() is not allowed in a parallel phase. Require modules in serial first.

Thread safety levels

API members carry a thread-safety tag:

Level Properties Functions
Unsafe No parallel read or write No parallel call
Read Parallel Read OK, write not n/a
Local Safe Same-actor any; cross-actor read-only Same-actor only
Safe Read + write Call from anywhere

Default is Unsafe if untagged. Most read-heavy access (geometry queries, raycasts) is safe; most mutations are not. Check the API reference per-member.

Cross-thread communication

  • Actor:SendMessage(topic, ...) + Actor:BindToMessage(topic, fn) / :BindToMessageParallel(topic, fn) — async one-way messaging. Multiple bindings per topic allowed.
  • SharedTable — table-like structure shared across actors with atomic updates. No copy on send. Use for "common world state not stored in DataModel".
  • Direct DataModel writes in serial phases work but force frequent synchronization.

When not to use it

  • Long uninterrupted computations. Even on a parallel thread, a 100ms unyielding loop still blocks that thread for 100ms. Break work up.
  • Cheap per-frame work. Actor overhead dwarfs the savings for trivial tasks.
  • Code that mostly mutates the DataModel. You'll spend most of your time task.synchronize'd, defeating the point.

Full deep-dive: references/parallel-luau.md. Source: create.roblox.com/docs/scripting/multithreading.


Performance

Performance optimization is a cycle: design for it up front, identify hot spots with the MicroProfiler, mitigate, monitor. Below is the high-signal subset; full pitfall list in references/performance-optimization.md.

Frame budget

  • Target: 60 FPS = 16.67 ms per frame for the client. Server has no frame rate but RunService.Heartbeat plays the same role.
  • Anything Luau-side that consistently runs >1 ms per frame is a candidate for optimization.

Top mistakes

  • Heavy work on RunService.Heartbeat (or PreSimulation/PostSimulation/PreRender/PreAnimation). Heartbeat fires every frame — gate by interval (if now - last < 0.1 then return end) or move work to a slower loop.
  • Server-side TweenService. Replicates the tweened property every frame; stutters under latency. Tween on the client unless the result must be authoritative.
  • Connections to Player / character that never disconnect. Player Instances aren't auto-destroyed on leave (see Workspace.PlayerCharacterDestroyBehavior to opt in to auto-destroy). Track and disconnect explicitly, or destroy manually in PlayerRemoving.
  • Tables that grow without bounds. playerInfo[player] = ... on join without playerInfo[player] = nil on leave is a classic server leak.
  • Deep cloning large tables. Use shallow copy; mutate only the path that changed (cross-link luau-expert skill).
  • Large RemoteEvent payloads. Send deltas, not whole inventories. For high-frequency state, pack into a buffer.
  • Excessive instance creation/destruction. Cloning large rigs at runtime replicates the entire descendant tree to clients. Pre-instance pools beat per-event instantiation.
  • Unanchored parts that don't move. Anchored = true removes them from physics simulation entirely.
  • High CollisionFidelity. Precise is expensive in CPU and memory. Use Box/Hull for anything where the player won't notice.
  • Asset duplication. Same mesh/texture uploaded multiple times = same content in memory multiple times. Reuse asset IDs.
  • Animation metadata in published rigs. Animation Editor data on a cloned rig replicates with every clone. Strip it before publish.
  • PreloadAsync over the entire Workspace for a "no pop-in" loading screen. Preload only what the loading screen and starting area need.
  • Native code generation (--!native) for hot, unyielding compute paths is a low-effort win — verify the function actually runs hot first.

MicroProfiler scopes worth knowing

  • RunService.Heartbeat / PreRender / PreSimulation / PostSimulation — your scripts.
  • physicsStepped / worldStep — physics cost.
  • ProcessPackets / Allocate Bandwidth — network cost.
  • updateInvalidatedFastClusters — humanoid/skinned-mesh churn.

Tag your own scopes with debug.profilebegin("Name") / debug.profileend().

Sources: create.roblox.com/docs/performance-optimization, /improve.


Bug Catalog

Duplicate Connections

-- ❌ every CharacterAdded adds another connection; old ones never disconnect
Players.PlayerAdded:Connect(function(player)
    player.CharacterAdded:Connect(function(char)
        char.Humanoid.Died:Connect(function() ... end)   -- leaks per respawn
    end)
end)

Track and disconnect connections per scope. A common pattern: store { RBXScriptConnection } keyed by the lifecycle owner (player, character, instance), iterate and :Disconnect() on cleanup.

Stale References

local character = player.Character
task.wait(5)
character.Humanoid.Health = 0     -- ❌ player may have respawned; old model is destroyed

Re-fetch player.Character after any yield, or capture+verify with if character.Parent == nil then return end.

Yielding in Event Handlers

RemoteEvent.OnServerEvent:Connect(function(player)
    task.wait(1)              -- ❌ all subsequent fires from this player wait behind this one
    grantReward(player)
end)

Either don't yield in handlers, or task.spawn the body so each fire runs independently. Be careful with task.spawn — concurrent state updates need a lock.

Memory Leaks

  • Instance:Destroy() disconnects events and removes from the DataModel, but doesn't break Lua-side references. Set the variable to nil if it's long-lived.
  • Connections to non-Roblox signals (custom signals, libraries) don't auto-clean. Track and disconnect them.
  • Closures capturing large tables in :Connect(...) keep those tables alive as long as the connection exists.

CollectionService Cleanup

-- ❌ tag handler runs once per existing tagged instance, then once per new one
CollectionService:GetTagged("Door"):forEach(setupDoor)
CollectionService:GetInstanceAddedSignal("Door"):Connect(setupDoor)

Always pair with GetInstanceRemovedSignal for cleanup. Track per-instance state in a { [Instance]: State } map, not in closures. Use instance.AncestryChanged (when parent == nil) or the removed signal.

Race: Player Joining Mid-Setup

PlayerAdded fires for players already in the server when your script starts (binding via :Connect only catches future fires) — handle existing players manually:

for _, player in Players:GetPlayers() do onPlayerAdded(player) end
Players.PlayerAdded:Connect(onPlayerAdded)

Architecture Rules

  • Validate every server boundary. RemoteEvent params, DataStore reads, MessagingService payloads, HttpService responses, anything user-influenced.
  • Server-authoritative checks beat client checks. Client-side validation is UX (instant feedback). Server-side validation is security. Never one without the other.
  • One ModuleScript = one concern. Don't aggregate into giant "Manager" modules — stack focused modules with clear ownership. (See luau-expert for module/file structure rules.)
  • One owner per piece of state. A field is server-owned, replicated-only, or client-owned. Never two writers. If multiple actors must update something, route through one module that serializes the writes.
  • Centralize replication. One module per replicated domain (inventory, currency, quest progress) — not Remotes scattered through the codebase. Easier to audit, easier to add validation/rate-limit.
  • Don't trust client-supplied UserIds. A UserId you read off any real Player Instance (the OnServerEvent first arg, Players:GetPlayers(), Players:GetPlayerByUserId) is authentic. A UserId the client sends as a RemoteEvent argument is just a number — validate or ignore.
  • Idempotent server handlers. Replays from network stutter or replays from exploiters are real. Design RemoteEvent handlers so two identical fires produce the same end state.
  • Expensive work goes server-side. Pathfinding, large queries, anything CPU-heavy. Client devices vary wildly; server is uniform.
  • Don't build cross-server state on MessagingService alone. It's best-effort, not guaranteed delivery, and rate-limited per-game. Use it for "interesting events" notifications, not for synchronizing state.
  • Match script container to runtime. Server logic in ServerScriptService/ServerStorage; client logic in StarterPlayerScripts/StarterCharacterScripts/StarterGui; shared modules in ReplicatedStorage. Don't put server-only logic in ReplicatedStorage — clients can read it.

Verification Rules

  • Don't quote API signatures from memory for properties beyond the most stable surface (Instance.Parent, BasePart.CFrame, Humanoid.Health, etc.). Roblox renames, deprecates, and adds. When unsure, point the user at create.roblox.com/docs and state your uncertainty.
  • Behavior beats API. Phrase rules as "the server is authoritative for…" rather than "use Workspace.StreamingMinRadius = 256" — the property may be renamed; the principle won't.
  • Distinguish stable from changing. DataModel hierarchy, services, replication direction, character lifecycle: stable for years. New APIs (e.g. recent Workspace properties, new RunService methods, scripting-related betas): verify before recommending.
  • Don't conflate engine and language. If the question is about typing or --!strict behavior, defer to luau-expert. If it's about Rojo or filesystem layout, defer to roblox-toolchain. If it's about UI libraries, defer to roblox-ui.

Anti-Patterns

❌ Don't ✅ Do
game.Workspace / workspace global game:GetService("Workspace")
game.Players.PlayerAdded:Connect(f) only Iterate :GetPlayers() then :Connect for joins-during-startup
child = parent:FindFirstChild("X") on client :WaitForChild("X", timeout)
Trust client RemoteEvent args by type annotation Type as unknown, validate, narrow
RemoteFunction:InvokeClient RemoteEvent round-trip
One Remote multiplexing many actions via kind field One Remote per action
SetAsync for concurrent state UpdateAsync
DataStore call without pcall Always pcall + retry/backoff
Read DataStore on every change Session-cached; write on PlayerRemoving + BindToClose
Store character ref across yields Re-fetch player.Character after any yield
Connect-without-disconnect across respawns Track + disconnect connections per character
Instance.Parent = nil for cleanup :Destroy()
WaitForChild without timeout :WaitForChild("X", 5) and handle nil
Client-side validation only Always validate on server
Yield inside an event handler that fires often task.spawn the body; serialize via your own queue
Auto network ownership for sensitive parts Explicit SetNetworkOwner(nil)
pairs over a tag set without removed-signal pair Pair GetInstanceAddedSignal with GetInstanceRemovedSignal
Server logic in ReplicatedStorage ServerScriptService / ServerStorage
LocalScript in Workspace Client containers (StarterPlayerScripts, etc.) or Script with RunContext = Client in ReplicatedStorage
wait / spawn / delay legacy globals task.wait / task.spawn / task.defer / task.delay
Heavy work directly on RunService.Heartbeat Throttle / move to slower loop / task.delay
Server-side TweenService for visual tweens Tween on the client; replicate target only
Cloning large rigs at runtime per event Pool / pre-instance
CollisionFidelity = Precise on small parts Box / Hull
Mutating Instances inside a parallel phase task.synchronize() first
Storing API keys in DataStores Secrets stores
Save data in MemoryStore Standard data stores

Examples