vimtricks.wiki Concise Vim tricks, one at a time.

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() and timer: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 control
  • vim.uv replaced the older vim.loop alias (both still work, but vim.uv is preferred in Neovim 0.10+)
  • Always stop() and close() timers you no longer need to avoid memory leaks

Next

What is the difference between the inner word (iw) and inner WORD (iW) text objects in Vim?