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:

LegacyReplacement
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:

-- 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)

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

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.

Related pages