Skip to content
AgileLogics
5 min read

Coroutine Schedulers

Prerequisites: Coroutines

Games and servers use coroutines to run many "tasks" that each wait for something — a timer, an animation, a network reply — without callbacks.

local tasks = {}
local tick = 0

local function spawn(name, fn)
  tasks[#tasks + 1] = {name = name, co = coroutine.create(fn), wakeAt = 0}
end

local function wait(ticks)
  coroutine.yield(ticks)
end

local function run()
  while #tasks > 0 do
    tick = tick + 1
    for i = #tasks, 1, -1 do
      local t = tasks[i]
      if tick >= t.wakeAt then
        local ok, ticks = coroutine.resume(t.co)
        if not ok then error(ticks) end
        if coroutine.status(t.co) == "dead" then
          table.remove(tasks, i)
        else
          t.wakeAt = tick + ticks
        end
      end
    end
  end
end

spawn("blink", function()
  for i = 1, 3 do
    print(tick, "blink " .. i)
    wait(2)
  end
end)

spawn("door", function()
  print(tick, "door opening")
  wait(3)
  print(tick, "door open")
end)

run()
1	door opening
1	blink 1
3	blink 2
4	door open
5	blink 3

Each task reads like straight-line code ("blink, wait 2 ticks, blink again"), but the tasks interleave. In a game, run would advance once per frame; in OpenResty, the equivalent wait is a network call.

Where you'll see this

  • Roblox — task.wait() and task.spawn are coroutine-based.
  • LÖVE — libraries like hump.timer and knife.coroutine script sequences with coroutines.
  • OpenResty — each request runs in a coroutine; I/O calls yield while waiting, so Nginx serves thousands of requests on one thread.
  • Neovim — async plugins use coroutines to await jobs and callbacks.