agalwood/Motrix · error · Error
timer_quota_exceeded
Error message
timer_quota_exceeded
What it means
The worker caps simultaneously-scheduled timers at MAX_ACTIVE (100). A new setTimeout beyond that throws inside the VM; quickjs-emscripten converts the synchronous throw into a VM-level exception the plugin sees as a thrown Error. Delays are also clamped to 30s.
Source
Thrown at src/core/plugin/host/quick-js-worker.ts:569
}
// --- Globals (timers) ---------------------------------------------------
function setupGlobals(vm: QuickJSContext): () => void {
// setTimeout / setInterval enforce caps:
// - MAX_ACTIVE: hard cap on simultaneously-scheduled timers per plugin
// - MAX_DELAY: clamps absurdly large delays to 30s
// When the active cap is exceeded we throw an Error from inside the
// newFunction callback — quickjs-emscripten converts that to a VM-level
// exception so the plugin sees a thrown error rather than a silent no-op.
const timers = new Map<number, NodeJS.Timeout>()
let nextId = 1
const MAX_DELAY = 30_000
const MAX_ACTIVE = 100
const setTimeoutFn = vm.newFunction('setTimeout', (cbHandle, delayHandle) => {
if (timers.size >= MAX_ACTIVE) {
throw new Error('timer_quota_exceeded')
}
const delay = Math.min(vm.getNumber(delayHandle), MAX_DELAY)
const id = nextId++
// Persistent handle survives the synchronous callback return; we
// dispose it when the timer fires (one-shot) or is cleared.
const persistent = cbHandle.dup()
timers.set(
id,
setTimeout(() => {
timers.delete(id)
const res = vm.callFunction(persistent, vm.undefined)
persistent.dispose()
if (res.error) {
res.error.dispose()
} else {
res.value.dispose()
}
}, delay)View on GitHub (pinned to 1a708ee577)
Solutions
- clearTimeout timers as soon as they are no longer needed.
- Batch work so the number of outstanding timers stays well under 100.
- Replace many one-shot timers with a single scheduler/interval that drains a queue.
- Track your own outstanding-timer count and back off when approaching the cap.
Example fix
// before — one timer per item, never cleared
items.forEach((it) => setTimeout(() => process(it), 1000))
// after — single draining scheduler
const queue = items.slice()
function pump() { const it = queue.shift(); if (it) { process(it); setTimeout(pump, 1000) } }
pump() Defensive patterns
Strategy: validation
Validate before calling
const MAX_ACTIVE = 100
const outstanding = new Set<number>()
function safeSetTimeout(cb: () => void, delay: number): number {
if (outstanding.size >= MAX_ACTIVE) {
throw new Error('timer_quota_exceeded (setTimeout) — clear outstanding timers first')
}
const id = setTimeout(() => { outstanding.delete(id); cb() }, Math.min(delay, 30_000)) as unknown as number
outstanding.add(id)
return id
} Type guard
function canScheduleTimer(outstanding: number, max = 100): boolean {
return outstanding < max
} Try / catch
try {
return setTimeout(cb, delay)
} catch (e) {
if (/timer_quota_exceeded/.test(e.message)) {
// drain/cancel stale timers, or queue the work instead of scheduling a timer
} else throw e
} Prevention
- clearTimeout timers as soon as they are no longer needed.
- Batch work into a single scheduler instead of one timer per item.
- Track outstanding timers and back off before reaching 100.
When it happens
Trigger: A plugin schedules more than 100 outstanding setTimeout callbacks without clearing earlier ones (e.g. one timer per item in a large fan-out, or an unbounded retry/backoff loop).
Common situations: Retry/backoff loops that stack timers; one timer per queued task with no clearing; long-lived plugin that leaks timers over time.
Related errors
- plugin.storage.quota_exceeded
- PLUGIN_ACTIVATION_CAP_EXCEEDED
- plugin.commands.id_out_of_namespace
- plugin.command.not_declared_in_manifest
- plugin.commands.not_found
AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12).
Data as JSON: /api/errors/07ce42d92bcf6b2f.
Report an issue: GitHub.