task.wait() vs wait() in Roblox: Which Should You Use?
Quick answer: Always use task.wait(). The legacy wait(), spawn(), and delay() are deprecated: they throttle resumes under sustained load and are less precise than the task library's frame-boundary scheduling.
Use task.wait() instead of wait(). Roblox deprecated the legacy globals wait, spawn, and delay because they run on an old scheduler that throttles resumes after sustained use, while the newer task library (task.wait, task.spawn, task.delay, task.defer) resumes threads precisely at the next frame boundary with better scheduling priority. Existing scripts keep working, but every new script you write should use the task library.
Why is wait() deprecated in Roblox?
The old yield functions date back to Roblox's earliest scripting model. When many scripts call them at once, the legacy scheduler degrades: after sustained heavy use, wait() becomes throttled and may resume your thread far later than the duration you asked for — historically as coarse as roughly one resume per 30 seconds under extreme load, instead of once per frame. That behavior made timing unpredictable and caused hard-to-debug "my loop runs slow" reports.
Roblox introduced the task library in 2021 to replace these functions. The legacy names were never removed (millions of games depend on them), but they are officially deprecated, and the documentation directs you to their task library equivalents:
| Legacy | Replacement |
|---|---|
wait(n) | task.wait(n) |
spawn(fn) | task.spawn(fn) |
delay(n, fn) | task.delay(n, fn) |
defer(fn) | task.defer(fn) |
What does task.wait() actually do?
task.wait(duration?) yields the current thread until the next Heartbeat step after the requested duration elapses. Key properties:
- It resumes at a frame boundary, so your code runs once per frame rather than being queued behind the legacy scheduler's throttled queue.
- With no argument it waits a single frame, making it the standard delay for loops.
- The actual wait time is always at least the requested duration; frames take time too, so treat any returned value as "how long I really waited" rather than expecting exact timing.
-- Standard frame-paced loop
while true do
updateHud()
task.wait()
end
-- Wait at least two seconds
task.wait(2)
print("two seconds passed")
How do task.spawn, task.defer, and task.delay differ?
All three start a new thread immediately; what differs is when the function's first execution happens relative to the current resumption cycle.
task.spawn(function()
print("runs immediately, right now")
end)
task.defer(function()
print("runs at the end of this resumption cycle")
end)
task.delay(3, function()
print("runs after at least 3 seconds")
end)
- task.spawn calls the function straight away, like calling it directly but in its own thread so errors and yields do not affect the caller.
- task.defer schedules the function to run at a deferred point in the same frame — useful when you want other code in the current cycle to finish first, for example letting a part finish initializing before reacting to it.
- task.delay is the safe replacement for
delay(): schedule something to happen later without occupying a running thread in the meantime.
The library also provides task.cancel(thread), which stops a thread started by the task functions — handy for cancelling a scheduled task.delay before it fires.
Side-by-side migration example
Here is a typical old-style script next to its modern equivalent.
Legacy version:
spawn(function()
while true do
spawnRespawnCheck()
wait(5)
end
end)
delay(10, function()
announceServerStart()
end)
Modern version:
local scheduled = task.delay(10, announceServerStart)
task.spawn(function()
while true do
spawnRespawnCheck()
if shuttingDown then
task.cancel(scheduled)
break
end
task.wait(5)
end
end)
Same behavior, but the modern version gets consistent frame-boundary resumes instead of throttled legacy ones, and gains the ability to cancel pending work with task.cancel.
Can RunService replace task.wait()?
For code that must run every single frame, connecting to a RunService event is often cleaner than looping with task.wait(), because the connection can be disconnected precisely and survives no matter how long each frame takes.
local RunService = game:GetService("RunService")
local connection = RunService.Heartbeat:Connect(function(deltaTime)
updateCameraShake(deltaTime)
end)
-- Later:
connection:Disconnect()
A good rule of thumb: use a Heartbeat connection when the work is inherently per-frame and needs explicit lifecycle control, and use a while true do ... task.wait() loop when the work has its own natural pause between iterations. Both are idiomatic; mixing them incorrectly (for example, yielding inside a render-stepped callback) is not.
Does task.wait() return anything?
Yes — like the legacy wait(), task.wait() returns the actual elapsed time, which will be slightly more than what you requested. You can use it to build framerate-independent logic:
while true do
local elapsed = task.wait(0.25)
position += velocity * elapsed -- scale by real time, not assumed time
end
Do not rely on exact numbers such as "task.wait returns exactly 1/60" — the value depends on the device's frame rate and server heartbeat rate. Measure it if your logic depends on it.
Common mistakes when switching to the task library
- Calling
task.wait()from a context where yielding is not allowed (for example, inside some event callbacks that expect synchronous behavior). Yielding rules did not change with the task library. - Assuming
task.spawnpropagates errors to the caller. It runs in a separate thread, so wrap risky work inpcallif failure handling matters. - Replacing
wait(0)withtask.wait(0): pass no argument or a small positive number instead, since zero-duration waits have no meaningful frame semantics. - Keeping both styles in one file. Pick the task library everywhere so future readers do not wonder whether a throttled
waitwas intentional.
If you are wiring up client-server communication while modernizing your loops, see our RemoteEvents guide; if you animate parts, tweening with TweenService usually replaces manual interpolation loops entirely.
Learn Luau by doing
Reading about schedulers only goes so far — timing bugs make sense once you have written a loop that misbehaves. Practice the task library alongside the rest of Luau in the theory reference, drill real problems in the problem set, and follow structured Luablox lessons that put each concept into a live Roblox project.
Frequently asked questions
Is wait() still usable in Roblox?
Yes, wait(), spawn(), and delay() still work because removing them would break existing games, but they are deprecated. They run on a legacy scheduler that throttles resumes under load, so new scripts should use task.wait(), task.spawn(), and task.delay() instead.
What does task.wait() do differently from wait()?
task.wait() resumes your thread at the next frame boundary after the requested duration using the modern scheduler, giving consistent timing and better priority. The deprecated wait() goes through a legacy queue that can be heavily throttled after sustained use.
When should I use task.defer() instead of task.spawn()?
Use task.spawn() when you want the function to run immediately in its own thread. Use task.defer() when the function should run at a deferred point in the same resumption cycle, for example after the current initialization or event handling finishes.
Should I use RunService.Heartbeat or a while loop with task.wait()?
Use a Heartbeat connection for per-frame work that needs precise lifecycle control via Disconnect(). Use a while true do loop with task.wait() when the work has a natural interval longer than one frame. Both are valid; choose based on whether the loop pauses itself.
Does task.wait() return the exact time waited?
No. It returns the actual elapsed time, which is always at least the duration you requested because waits resolve at frame boundaries. Frame rate varies by device, so scale time-sensitive logic by the returned value rather than assuming a fixed step.
Learn scope, local closures, varargs, and multiple returns in Luau to write reusable Roblox code.