The Roblox Frame Lifecycle
One Roblox frame on the server runs in a strict order: PreSimulation, physics, PostSimulation, Heartbeat, replication. The client adds a few more phases on top, most importantly the render-stepped phase that fires right before drawing. Knowing what runs when — and where your script actually sits in that sequence — is how you write predictable game code.
The server frame at 60Hz
The Roblox server runs a fixed-step loop at 60 ticks per second. Every tick is roughly 16.67ms, and the engine tries to do every per-frame thing within that budget. If something stalls, the next tick fires a bit late but the server doesn't drop the work — it catches up.
The script-facing signals are RunService.PreSimulation, RunService.PostSimulation, and RunService.Heartbeat. The physics step and replication phase don't emit signals — they're the engine doing its own work between your hooks.
When to use each signal
- PreSimulation — fires before physics runs this frame. Use this when you want to apply forces, set velocities, or queue input that physics should react to this frame. The classic example: applying a jump impulse.
- PostSimulation — fires after physics finishes. Use this when you need the final positions of objects for the frame — reading where everything ended up. Useful for camera follow, collision response, "did this hit something" queries.
- Heartbeat — fires once per frame, after PostSimulation, before replication. The general-purpose "every frame" signal. Most game logic that needs frame-rate updates lives here.
local RunService = game:GetService("RunService")
RunService.PreSimulation:Connect(function(dt)
-- Apply forces, read inputs accumulated since last frame
end)
RunService.PostSimulation:Connect(function(dt)
-- Physics is done; positions are final for this frame
end)
RunService.Heartbeat:Connect(function(dt)
-- General per-frame logic
end)The dt argument every handler receives is the time since the previous fire, in seconds. On a healthy server this is close to 1/60 (~0.0167); on an overloaded one it grows. Use dt for anything frame-rate- independent, like rotating a part at a fixed angular velocity:
local part = workspace.Rotator
RunService.Heartbeat:Connect(function(dt)
part.CFrame = part.CFrame * CFrame.Angles(0, math.rad(45) * dt, 0)
-- Rotates 45° per second regardless of frame rate
end)The client frame
The client runs at variable frame rate — typically 30–144 fps depending on hardware and settings. It has all three server-side signals, plus one more:
- RenderStepped (aka
PreRender) — fires just before the client's render pass. Client-only. This is where you update the camera, animate UI smoothly, or move interpolated objects to look perfect for this frame.
-- LocalScript
local RunService = game:GetService("RunService")
local camera = workspace.CurrentCamera
RunService.RenderStepped:Connect(function(dt)
-- Lerp the camera to its target each render frame
local target = computeCameraTarget()
camera.CFrame = camera.CFrame:Lerp(target, math.min(1, 10 * dt))
end)Server-to-client replication
Once the server's frame finishes, it sends a packet of changes to every connected client. The packet contains everything that changed since the last frame: new instances, modified properties, fired events, destroyed objects.
Replication is not free, and it's the main reason server-side property writes have a cost. Setting part.Position on the server doesn't just change a number locally — it queues a replication update for every client that can see that part. A loop that moves 1000 parts per frame is a lot of network traffic, even if the loop itself is cheap.
The corollary: scripts that need to set many properties rapidly are often better off doing the work on the client, where there's no replication cost. Visual effects, UI animations, particle bursts, camera shake — all client work.
Client-to-server replication
Going the other direction, the client doesn't replicate property changes back to the server. (It can't — FilteringEnabled prevents direct mutation of anything but its own character.) Anything the client wants the server to know about goes through a RemoteEvent or RemoteFunction, which the server processes on its next frame.
-- LocalScript: ask the server to do something
game.ReplicatedStorage.Shoot:FireServer(target.Position)
-- ServerScript: handle it
game.ReplicatedStorage.Shoot.OnServerEvent:Connect(function(player, targetPos)
-- This fires during the server's heartbeat phase
-- on the frame where the packet arrived
end)Network round-trip from input to server-side reaction is typically 50–150ms depending on the player's connection. That latency budget is what good network code tries to mask — fire visual effects locally on the click, let the server confirm/validate, gracefully roll back if the server rejects.
Property updates and Stepped batching
One subtle property of the engine: server property changes are batched within a frame for replication, but reads within the same frame see the latest written value immediately. That means:
-- All in one server frame
part.Position = Vector3.new(10, 0, 0)
print(part.Position) -- Vector3.new(10, 0, 0)
part.Position = Vector3.new(20, 0, 0)
print(part.Position) -- Vector3.new(20, 0, 0)
-- Only the final value (20, 0, 0) replicates to clientsThe client never sees the intermediate (10, 0, 0) state — only the value at the moment replication flushed. So a loop that "animates" an object by setting its position 60 times per frame on the server is wasted work; the client sees only the last position. Animate from the client, or use TweenService.
Frame budget in practice
On the server, your scripts have to share the ~16ms frame budget with physics, replication, and the engine's own bookkeeping. Realistically, that means script work shouldn't take more than 4–6ms per frame to maintain a stable 60Hz. The script performance pane in Studio shows per-frame breakdowns; if you see a script consistently consuming >5ms, that's the one to optimise.
On the client, frame budget is fps-dependent. At 60fps you have 16ms; at 144fps you have 6.9ms. Heavy per-frame client logic should be measured at the lowest fps you want to support, not the highest.
Wrap-up
A Roblox frame is more structured than it looks from inside a single script. Knowing the order — and specifically which signal fires when — lets you write code that always sees the world in the state you wanted, and lets you put expensive work on the right side of the network boundary. For deeper companion reading, see our task scheduler article (which covers how coroutines plug into these phases) and the signal internals article (which covers how event handlers themselves are dispatched).
Ather is the lead developer behind Atherhub. He's been writing Luau and Roblox tooling for the better part of a decade, with a focus on the messy interface between game-script internals and the platforms that host them. Have feedback on this article? Drop it in the Discord.