diff --git a/docs/explore-notes/usage-monitoring.md b/docs/explore-notes/usage-monitoring.md
index b5841d62..d57be351 100644
--- a/docs/explore-notes/usage-monitoring.md
+++ b/docs/explore-notes/usage-monitoring.md
@@ -21,6 +21,11 @@ authenticated with the Bearer access token Claude Code itself keeps fresh at
Because the endpoint is undocumented and can change without notice, **every consumer
fails open**. That is the single most important invariant here.
+**Measured 2026-09-16** (clean burst against the live endpoint, Worker otherwise idle): exactly
+5 requests succeed, the 6th 429s with `Retry-After: 300` — a flat 5-requests-per-5-minute quota,
+no `ratelimit-*`/`x-ratelimit-*` headers on either the 200s or the 429. The quota is per-account,
+not per-process, so a manual refresh or another consumer of the same OAuth token shares it.
+
## Components (`Usage/`)
| Type | Role |
@@ -28,7 +33,7 @@ fails open**. That is the single most important invariant here.
| `UsageModels` | `UsageBucket` / `UsageLimitRow` / `UsageSnapshot`. `UsageBucket.Utilization` is already a 0–100 percent — compare directly against thresholds, don't rescale. |
| `ClaudeOAuthUsageClient` | Reads the token, calls the endpoint. Defensive parsing: missing/null buckets → null, missing `limits` → empty list. |
| `UsageState` | Threadsafe singleton. A failed poll **never** overwrites the last good snapshot — it only sets `LastError`. |
-| `UsageMonitorService` | `BackgroundService`; polls on `usage_poll_interval_seconds` (default 60, clamped to min 15 on config load), one poll at startup. Logs a failure at most once per distinct error message. Broadcasts `HubBroadcaster.UsageUpdated` after **every** tick, success or failure. |
+| `UsageMonitorService` | `BackgroundService`; polls on `usage_poll_interval_active_seconds` / `usage_poll_interval_idle_seconds` (default 120 each, clamped to min 90 on config load), one poll at startup. Logs a failure at most once per distinct error message. Broadcasts `HubBroadcaster.UsageUpdated` after **every** tick, success or failure. |
| `UsageSnapshotBuilder` | Builds the Hub-facing `UsageSnapshotDto` from `UsageState` + `IUsageGate` + `AppSettings`. The one shared place for stale/threshold/gate logic — `WorkerHub.GetUsageSnapshot` and `UsageMonitorService` must not diverge. |
| `UsageGate` | Hard pause decision → `UsageGateDecision(IsBlocked, Reason)`. |
| `UsageThrottle` | Pure static staging of parallelism ahead of the gate. |
diff --git a/src/ClaudeDo.Worker/CLAUDE.md b/src/ClaudeDo.Worker/CLAUDE.md
index c46c087a..d3e9c139 100644
--- a/src/ClaudeDo.Worker/CLAUDE.md
+++ b/src/ClaudeDo.Worker/CLAUDE.md
@@ -254,7 +254,10 @@ non-obvious, behaviour-changing. A fixed bug is git history, not a finding.
Both edit the DI-singleton `WorkerConfig` in place *and* persist via their own `SaveKey` call
(`SaveClaudeBin`/`SaveTicketApiBaseUrl`), so each applies without a restart. Every other field
here is hand-edit only.
-- `usage_poll_interval_seconds` (60, clamped to min 15 on load)
+- `usage_poll_interval_active_seconds` / `usage_poll_interval_idle_seconds` (120 each, clamped to
+ min 90 on load) — measured 2026-09-16: the endpoint allows exactly 5 requests per 5-minute
+ window, then 429s with `Retry-After: 300`; 120s leaves margin for manual refreshes and other
+ consumers of the same OAuth token, 60s eats the whole quota (see [usage-monitoring](../../docs/explore-notes/usage-monitoring.md)).
- `online_inbox` — `enabled` (false by default; when false the entire `Online/` stack is not registered), `api_base_url` (must be HTTPS or loopback, validated at startup), `poll_interval_seconds` (60), `zitadel.authority`/`client_id`/`scopes`. The refresh token is **not** in this file — DPAPI-encrypted at `~/.claudeDo/online-inbox.token`.
Per-list config (`list_config` in DB) provides defaults for `model`, `system_prompt`,
diff --git a/src/ClaudeDo.Worker/Config/WorkerConfig.cs b/src/ClaudeDo.Worker/Config/WorkerConfig.cs
index f5f7026d..9d0cc234 100644
--- a/src/ClaudeDo.Worker/Config/WorkerConfig.cs
+++ b/src/ClaudeDo.Worker/Config/WorkerConfig.cs
@@ -46,16 +46,25 @@ public sealed class WorkerConfig
///
/// Usage-monitor poll interval while at least one task is Running. Clamped to a minimum
- /// of 60s on load — the endpoint rate-limits (429) on tighter polling.
+ /// of 90s on load. Measured 2026-09-16 against the live endpoint: a clean burst allows
+ /// exactly 5 requests before a 429, with Retry-After: 300 (a flat 5-req/5-min quota,
+ /// no ratelimit-* headers). 120s leaves ~2.5 requests of headroom per 5-minute window
+ /// for manual refreshes and any other consumer of the same OAuth token — 60s consumes the
+ /// whole quota by itself with zero margin, which is what made the old flat-60s cadence 429
+ /// (see commit 2700c3d8). Same default as : the
+ /// quota is a flat per-account limit, not tied to ClaudeDo's own activity, so an active/idle
+ /// split buys nothing safety-wise. The two config keys stay separate for compatibility.
///
[JsonPropertyName("usage_poll_interval_active_seconds")]
- public int UsagePollIntervalActiveSeconds { get; set; } = 300;
+ public int UsagePollIntervalActiveSeconds { get; set; } = 120;
///
- /// Usage-monitor poll interval while nothing is running. Clamped to a minimum of 60s on load.
+ /// Usage-monitor poll interval while nothing is running. Clamped to a minimum of 90s on
+ /// load. Same default as — see that doc for the
+ /// measured rate limit this is sized against.
///
[JsonPropertyName("usage_poll_interval_idle_seconds")]
- public int UsagePollIntervalIdleSeconds { get; set; } = 900;
+ public int UsagePollIntervalIdleSeconds { get; set; } = 120;
/// Base URL of the Bandel ticket-system API. Null = ticket integration off entirely.
[JsonPropertyName("ticket_api_base_url")]
@@ -88,8 +97,8 @@ public sealed class WorkerConfig
cfg.SandboxRoot = Paths.Expand(cfg.SandboxRoot);
cfg.LogRoot = Paths.Expand(cfg.LogRoot);
cfg.CentralWorktreeRoot = Paths.Expand(cfg.CentralWorktreeRoot);
- cfg.UsagePollIntervalActiveSeconds = Math.Max(60, cfg.UsagePollIntervalActiveSeconds);
- cfg.UsagePollIntervalIdleSeconds = Math.Max(60, cfg.UsagePollIntervalIdleSeconds);
+ cfg.UsagePollIntervalActiveSeconds = Math.Max(90, cfg.UsagePollIntervalActiveSeconds);
+ cfg.UsagePollIntervalIdleSeconds = Math.Max(90, cfg.UsagePollIntervalIdleSeconds);
return cfg;
}