Lua Coroutines: Cooperative Pause and Resume Without Threads
Lua coroutines provide cooperative pause and resume in a single thread. Learn how coroutine.create, resume, yield and status work together with a producer example, plus limits and common mistakes to avoid.
28 Jun 2026, 02:00 UTC

Use coroutines when you need pauseable work in a single Lua thread
The useful answer is this: Lua coroutines give you cooperative multitasking without OS threads. A coroutine is a first-class value that remembers its local variables, stack position and instruction pointer between pauses. You create it, resume it from the main thread, and let it yield back values. It is ideal for generators, state machines, and producer-consumer pipelines where blocking the main loop is not an option.
Coroutines are cooperative, not preemptive. They only pause when code explicitly calls coroutine.yield. Long running loops must yield themselves or they will block everything else in the same Lua state.
How create, resume, yield and status work together
coroutine.create(fn) returns a thread object. The function is not run yet. coroutine.resume(co, ...) starts or continues the thread and passes arguments into it. coroutine.yield(...) suspends the thread and returns values to the caller of resume. coroutine.status(co) reports "running", "suspended", "normal" or "dead".
Error handling is explicit. If the coroutine errors, coroutine.resume returns false as the first result and the error message as the second. The error does not propagate automatically to the caller.
Threads share the same global environment but have independent stacks. Locals are preserved across yields, which makes isolation simple without copying state.
Worked example: producer coroutine with explicit resume
The pattern below shows a producer that yields numbers and a main loop that consumes them. It demonstrates creation, resume with arguments, yield with multiple values, and status checks.
-- run with lua 5.1+ or LuaJIT in a terminal with write permission
local function producer(limit)
local i = 1
while i <= limit do
-- yield current value back to caller, resume will receive i and next arg
local ack = coroutine.yield(i)
-- ack is the value passed into resume on the next iteration
i = i + 1
end
return "done"
end
local co = coroutine.create(producer)
-- first resume starts the coroutine, no arguments needed
local ok, first = coroutine.resume(co, 5)
-- first is the first yielded value
while coroutine.status(co) ~= "dead" do
local ok, value = coroutine.resume(co, "ack")
if not ok then
-- error path: resume returns false + message
error("coroutine error: " .. tostring(value))
end
-- value is what producer yielded, or final return when dead
if coroutine.status(co) == "dead" then
-- final return value is in value
end
end
Where to run: save as producer.lua and run with lua producer.lua. Required permissions are normal file read/execute for the interpreter. Risks are minimal; the code only reads and yields. Do not call coroutine.resume from inside the coroutine itself, which would create recursive resumption and stack growth.
Check the result practically. After each yield, call coroutine.status(co). It should be "suspended" while the producer is waiting, and "dead" after the loop ends. If you resume a dead coroutine, resume returns false with the message "cannot resume dead coroutine".
Limits you must design around
Cooperative only, single threaded
Lua has one execution thread per state. Coroutines do not run in parallel. If a coroutine performs a blocking I/O call or a tight loop without yielding, the whole program stalls. Design long work as chunks with explicit yields.
Shared globals, isolated stacks
Globals are shared across all coroutines. Mutable global tables can be changed by any thread. Locals and upvalues are private to the coroutine and survive across yields. This is safe for state but not for accidental global mutation.
Return values and arguments are positional
coroutine.yield can return multiple values. coroutine.resume receives them as the first results after the status boolean. When you resume again, the values you pass become the return values of the previous yield call. Forgetting this mapping is a common source of off-by-one values.
Error propagation is manual
An error inside a coroutine does not unwind the main thread. resume returns false, error message. You must check the first return value of every resume and handle it, otherwise failures are silent.
Common mistakes
- Forgetting to resume. A created coroutine never runs until resume is called. Status stays "suspended".
- Mismanaging yielded values. Yielding multiple values is fine, but resuming with no arguments means the yield expression receives nil. Always match the number of expected results.
- Recursive resumption. Calling coroutine.resume from inside a coroutine leads to nested execution and can overflow the C stack. Use a scheduler coroutine to drive others, not self-resume.
- Assuming preemption. Coroutines will not yield on their own. Put yield points in loops, parsers, or network reads.
- Resuming a dead coroutine. After completion status is "dead". Further resume attempts error. Check status before resuming in a loop.
For complex workflows with multiple producers and consumers, introduce a dispatcher coroutine that owns the resume loop and routes messages. Lua's standard library does not provide a built-in scheduler for nested yielding.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.