How do I schedule a callback to run after a delay in Neovim using the built-in libuv timer API?
Answer
vim.uv.new_timer()
Explanation
Neovim exposes vim.uv (formerly vim.loop), a binding to the libuv event loop that powers async I/O. vim.uv.new_timer() lets you run a Lua callback after a delay or at a repeating interval — without blocking the editor. This is the foundation for debouncing, periodic checks, and custom async workflows.
How it works
local timer = vim.uv.new_timer()
timer:start(
500, -- initial delay in ms
0, -- repeat interval in ms (0 = one-shot)
vim.schedule_wrap(function()
-- this runs on the main Neovim thread after 500ms
print('Hello from a timer!')
timer:stop()
timer:close()
end)
)
- The first argument to
start()is the delay before the first call (ms) - The second argument is the repeat period (0 = fire once)
- Always wrap the callback in
vim.schedule_wrap()so it runs safely on Neovim's main thread - Call
timer:stop()andtimer:close()to clean up when done
Example: Debounce auto-format on save
local debounce_timer = vim.uv.new_timer()
vim.api.nvim_create_autocmd('TextChanged', {
callback = function()
debounce_timer:stop()
debounce_timer:start(300, 0, vim.schedule_wrap(function()
vim.lsp.buf.format({ async = true })
end))
end,
})
Tips
- Use
vim.defer_fn(callback, delay_ms)for simpler one-shot deferred calls without managing the timer lifecycle vim.uv.new_timer()is lower-level and preferable when you need repeating timers or precise lifecycle controlvim.uvreplaced the oldervim.loopalias (both still work, butvim.uvis preferred in Neovim 0.10+)- Always
stop()andclose()timers you no longer need to avoid memory leaks