{"record":{"id":"07ce42d92bcf6b2f","repo":"agalwood/Motrix","slug":"timer-quota-exceeded","errorCode":null,"errorMessage":"timer_quota_exceeded","messagePattern":"timer_quota_exceeded","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/core/plugin/host/quick-js-worker.ts","lineNumber":569,"sourceCode":"}\n\n// --- Globals (timers) ---------------------------------------------------\n\nfunction setupGlobals(vm: QuickJSContext): () => void {\n  // setTimeout / setInterval enforce caps:\n  //   - MAX_ACTIVE: hard cap on simultaneously-scheduled timers per plugin\n  //   - MAX_DELAY:  clamps absurdly large delays to 30s\n  // When the active cap is exceeded we throw an Error from inside the\n  // newFunction callback — quickjs-emscripten converts that to a VM-level\n  // exception so the plugin sees a thrown error rather than a silent no-op.\n  const timers = new Map<number, NodeJS.Timeout>()\n  let nextId = 1\n  const MAX_DELAY = 30_000\n  const MAX_ACTIVE = 100\n\n  const setTimeoutFn = vm.newFunction('setTimeout', (cbHandle, delayHandle) => {\n    if (timers.size >= MAX_ACTIVE) {\n      throw new Error('timer_quota_exceeded')\n    }\n    const delay = Math.min(vm.getNumber(delayHandle), MAX_DELAY)\n    const id = nextId++\n    // Persistent handle survives the synchronous callback return; we\n    // dispose it when the timer fires (one-shot) or is cleared.\n    const persistent = cbHandle.dup()\n    timers.set(\n      id,\n      setTimeout(() => {\n        timers.delete(id)\n        const res = vm.callFunction(persistent, vm.undefined)\n        persistent.dispose()\n        if (res.error) {\n          res.error.dispose()\n        } else {\n          res.value.dispose()\n        }\n      }, delay)","sourceCodeStart":551,"sourceCodeEnd":587,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/host/quick-js-worker.ts#L551-L587","documentation":"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.","triggerScenarios":"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).","commonSituations":"Retry/backoff loops that stack timers; one timer per queued task with no clearing; long-lived plugin that leaks timers over time.","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."],"exampleFix":"// before — one timer per item, never cleared\nitems.forEach((it) => setTimeout(() => process(it), 1000))\n// after — single draining scheduler\nconst queue = items.slice()\nfunction pump() { const it = queue.shift(); if (it) { process(it); setTimeout(pump, 1000) } }\npump()","handlingStrategy":"validation","validationCode":"const MAX_ACTIVE = 100\nconst outstanding = new Set<number>()\nfunction safeSetTimeout(cb: () => void, delay: number): number {\n  if (outstanding.size >= MAX_ACTIVE) {\n    throw new Error('timer_quota_exceeded (setTimeout) — clear outstanding timers first')\n  }\n  const id = setTimeout(() => { outstanding.delete(id); cb() }, Math.min(delay, 30_000)) as unknown as number\n  outstanding.add(id)\n  return id\n}","typeGuard":"function canScheduleTimer(outstanding: number, max = 100): boolean {\n  return outstanding < max\n}","tryCatchPattern":"try {\n  return setTimeout(cb, delay)\n} catch (e) {\n  if (/timer_quota_exceeded/.test(e.message)) {\n    // drain/cancel stale timers, or queue the work instead of scheduling a timer\n  } else throw e\n}","preventionTips":["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."],"tags":["plugin","timer","quota"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}