{"record":{"id":"7add4a413d976325","repo":"agalwood/Motrix","slug":"plugin-storage-quota-exceeded","errorCode":"plugin.storage.quota_exceeded","errorMessage":"plugin.storage.quota_exceeded: projected usage ${projected} exceeds quota ${this.quotaBytes}","messagePattern":"plugin\\.storage\\.quota_exceeded: projected usage (.+?) exceeds quota (.+?)","errorType":"exception","errorClass":"StorageError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/capabilities/storage.ts","lineNumber":144,"sourceCode":"    projectedSize: number\n  ): void {\n    const row = this.db\n      .prepare<[string], { total: number }>(\n        'SELECT COALESCE(SUM(size), 0) AS total FROM plugin_storage WHERE plugin_id = ?'\n      )\n      .get(pluginId) ?? { total: 0 }\n\n    const currentRow = this.db\n      .prepare<[string, string], { size: number } | undefined>(\n        'SELECT size FROM plugin_storage WHERE plugin_id = ? AND key = ?'\n      )\n      .get(pluginId, key)\n\n    const currentSize = currentRow?.size ?? 0\n    const projected = row.total - currentSize + projectedSize\n\n    if (projected > this.quotaBytes) {\n      throw new StorageError(\n        'plugin.storage.quota_exceeded',\n        `plugin.storage.quota_exceeded: projected usage ${projected} exceeds quota ${this.quotaBytes}`\n      )\n    }\n  }\n\n  // -------------------------------------------------------------------------\n  // get\n  // -------------------------------------------------------------------------\n\n  async get(pluginId: string, key: string): Promise<StorageGetResult> {\n    const row = this.db\n      .prepare<\n        [string, string],\n        { value: string; version: number } | undefined\n      >(\n        'SELECT value, version FROM plugin_storage WHERE plugin_id = ? AND key = ?'\n      )","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/capabilities/storage.ts#L126-L162","documentation":"Thrown by assertQuota() before any write when the projected total bytes for that plugin would exceed quotaBytes (default 5 MB, override via constructor). The projection subtracts the existing size of the same key (so re-writing the same key with a smaller/equal value does not double-count) and adds the incoming value's UTF-8 byte length. It is a pre-flight guard, so the row is never partially written.","triggerScenarios":"host.set or host.compareAndSet where (currentTotalBytes - existingSizeForKey + newSize) > quotaBytes. Concretely: writing many keys whose SUM(size) passes 5 MB, replacing a small key with a much larger value that crosses the threshold, or writing a single multi-MB blob.","commonSituations":"A plugin accumulates unbounded history/cache rows over time and eventually crosses 5 MB; a plugin stores large base64 blobs or serialized images; the default quota was not raised for a data-heavy plugin. Note the check is per-pluginId, not per-key, so many small keys add up.","solutions":["Reduce the value size — compress, downsample, or trim the structure before storing.","Delete or overwrite old keys (host.delete / host.keys + cleanup loop) to bring the running total down before writing the new key.","If the plugin legitimately needs more space, construct StorageCapabilityHost with a larger quotaBytes: new StorageCapabilityHost({ db, quotaBytes: 20 << 20 }).","Split one large value across multiple plugins only if the data model genuinely shards; otherwise prefer trimming."],"exampleFix":"// before\nconst host = new StorageCapabilityHost({ db })\nawait host.set(pluginId, 'log', bigLogString)\n\n// after\nconst host = new StorageCapabilityHost({ db, quotaBytes: 20 << 20 })\nawait host.set(pluginId, 'log', bigLogString.slice(0, 2_000_000))","handlingStrategy":"validation","validationCode":"const totalBytes = (await host.keys(pluginId)).reduce(async (acc, k) => acc + (await host.get(pluginId, k))?.__size ?? 0, 0)\n// simpler: pre-check incoming size\nif (Buffer.byteLength(JSON.stringify(value) ?? '', 'utf8') + approxCurrentTotal > QUOTA) { /* trim first */ }","typeGuard":null,"tryCatchPattern":"try { await host.set(pluginId, key, value) }\ncatch (e) { if (e.code === 'plugin.storage.quota_exceeded') { await cleanupOldKeys(pluginId); await host.set(pluginId, key, value) } else throw e }","preventionTips":["Track approximate per-plugin usage and prune before writes.","Avoid storing unbounded histories; cap list lengths.","Raise quotaBytes at host construction for data-heavy plugins."],"tags":["storage","quota","plugin","limits"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}