Did you check docs and existing issues?
Neovim version (nvim -v)
v0.11.6
Operating system/version
Ubuntu 24.03
Describe the bug
With noice loaded and Neovim completely idle, the process wakes up ~31 times/second forever. It never settles, even with no messages, no cmdline, no LSP progress and no UI activity of any kind. On a laptop this is a measurable idle-power cost — in my own config it was the second-largest source of idle wakeups.
The cause is that the router's updater interval is created with no enabled predicate:
-- lua/noice/message/router.lua:31
function M.enable()
if not M._updater then
M._updater = Util.interval(Config.options.throttle, Util.protect(M.update))
end
M._updater()
end
Util.interval only stops itself when keep() returns false:
-- lua/noice/util/init.lua:126
function T.keep()
if M.is_exiting() then
return false
end
return opts.enabled == nil or opts.enabled()
end
Since opts.enabled is nil here, keep() reduces to not is_exiting() — always true. So once Router.enable() runs at setup, the chained vim.defer_fn loop re-arms every throttle ms (default 1000/30 ≈ 33ms) for the whole session, calling M.update() on an empty queue.
Worth noting the contrast with the two intervals that do pass a predicate and correctly go quiet: hacks.fix_nohlsearch (enabled = vim.o.hlsearch and vim.v.hlsearch == 1) and health.checker (enabled = Config.is_running).
Steps To Reproduce
-
Save the repro.lua below and start it with a known socket so the right process can be measured:
nvim -u repro.lua --listen /tmp/repro.sock
-
Leave it completely idle for ~45s (measuring earlier than that catches lazy.nvim still settling and reads high).
-
Measure wakeups of the nvim --embed server process — get its pid from the instance itself rather than pgrep, so any other nvim you have open can't be picked by mistake (Linux):
p=$(nvim --server /tmp/repro.sock --remote-expr 'getpid()')
a=$(awk '/ctxt_switches/{s+=$2} END{print s}' /proc/$p/status); sleep 10
b=$(awk '/ctxt_switches/{s+=$2} END{print s}' /proc/$p/status); echo "$(( (b-a)/10 )) wakeups/s"
-
Inspect the loop's live timers from inside that same instance:
:lua vim.uv.walk(function(h) if h:get_type()=="timer" and vim.uv.is_active(h) then
print(("repeat=%s due_in=%s"):format(vim.uv.timer_get_repeat(h), vim.uv.timer_get_due_in(h))) end end)
Measured, running exactly the repro.lua below:
| config |
idle wakeups/s |
active timers |
repro.lua with spec = {} |
0 |
none |
repro.lua with noice only |
31 |
two chained one-shots: due_in≈12 (router, 33ms) + due_in≈799 (health checker, 1000ms) |
Raising opts.throttle scales it exactly as you'd expect — throttle = 200 drops it to ~5/s — which confirms the interval is the source, but it trades cmdline/message render latency for idle power, so it isn't really a fix.
Steps To Reproduce
-- repro.lua
vim.env.LAZY_STDPATH = ".repro"
load(vim.fn.system("curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))()
require("lazy.minit").repro({
spec = {
{ "folke/noice.nvim", opts = {}, dependencies = { "MunifTanjim/nui.nvim" } },
},
})
Expected Behavior
With nothing queued to render, the router shouldn't poll at all — 0 wakeups/s at idle, the way Neovim itself behaves (nvim --clean idles at exactly 0).
Passing an enabled predicate to Util.interval (roughly "queue non-empty / something is displayed") would make keep() return false and let the existing machinery stop the timer.
One wrinkle worth flagging, because it means this isn't a pure one-liner: nothing would restart it. The T.__call re-arm path is only invoked from Router.enable(), whereas the sites that react to new messages — lsp/progress.lua:57, source/notify.lua:67, ui/init.lua:88 and :147 — call Router.update() directly rather than poking the interval. Those callsites would need to call M._updater() instead, so the loop wakes back up when there's actually work.
Happy to put up a PR along those lines if that's the design you'd want, or to follow whatever shape you prefer.
Repro
vim.env.LAZY_STDPATH = ".repro"
load(vim.fn.system("curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))()
require("lazy.minit").repro({
spec = {
{ "folke/noice.nvim", opts = {} },
-- add any other plugins here
},
})
Did you check docs and existing issues?
Neovim version (nvim -v)
v0.11.6
Operating system/version
Ubuntu 24.03
Describe the bug
With noice loaded and Neovim completely idle, the process wakes up ~31 times/second forever. It never settles, even with no messages, no cmdline, no LSP progress and no UI activity of any kind. On a laptop this is a measurable idle-power cost — in my own config it was the second-largest source of idle wakeups.
The cause is that the router's updater interval is created with no
enabledpredicate:Util.intervalonly stops itself whenkeep()returns false:Since
opts.enabledisnilhere,keep()reduces tonot is_exiting()— always true. So onceRouter.enable()runs at setup, the chainedvim.defer_fnloop re-arms everythrottlems (default1000/30≈ 33ms) for the whole session, callingM.update()on an empty queue.Worth noting the contrast with the two intervals that do pass a predicate and correctly go quiet:
hacks.fix_nohlsearch(enabled = vim.o.hlsearch and vim.v.hlsearch == 1) andhealth.checker(enabled = Config.is_running).Steps To Reproduce
Save the
repro.luabelow and start it with a known socket so the right process can be measured:Leave it completely idle for ~45s (measuring earlier than that catches lazy.nvim still settling and reads high).
Measure wakeups of the
nvim --embedserver process — get its pid from the instance itself rather thanpgrep, so any other nvim you have open can't be picked by mistake (Linux):Inspect the loop's live timers from inside that same instance:
Measured, running exactly the
repro.luabelow:repro.luawithspec = {}repro.luawith noice onlydue_in≈12(router, 33ms) +due_in≈799(health checker, 1000ms)Raising
opts.throttlescales it exactly as you'd expect —throttle = 200drops it to ~5/s — which confirms the interval is the source, but it trades cmdline/message render latency for idle power, so it isn't really a fix.Steps To Reproduce
Expected Behavior
With nothing queued to render, the router shouldn't poll at all — 0 wakeups/s at idle, the way Neovim itself behaves (
nvim --cleanidles at exactly 0).Passing an
enabledpredicate toUtil.interval(roughly "queue non-empty / something is displayed") would makekeep()return false and let the existing machinery stop the timer.One wrinkle worth flagging, because it means this isn't a pure one-liner: nothing would restart it. The
T.__callre-arm path is only invoked fromRouter.enable(), whereas the sites that react to new messages —lsp/progress.lua:57,source/notify.lua:67,ui/init.lua:88and:147— callRouter.update()directly rather than poking the interval. Those callsites would need to callM._updater()instead, so the loop wakes back up when there's actually work.Happy to put up a PR along those lines if that's the design you'd want, or to follow whatever shape you prefer.
Repro