---
title: "Worker and API Limits"
canonical: "https://pharos.watch/docs/worker-and-api-limits/"
description: "Operational limits for the Pharos Worker and API: cron budgets, fetch connection caps, polling intervals, cache behavior, and guardrail checks."
---

# Worker & API Limits

Reference for limits we can verify from repo code and checked-in config.

This document intentionally focuses on:

- limits enforced in code
- budgets encoded in config
- runtime assumptions the scheduler is explicitly designed around

It intentionally does **not** treat vendor pricing-plan quotas as source of truth. Cloudflare, CoinGecko, Alchemy, Etherscan, Anthropic, X, and similar providers can change those independently of this repo. Re-check official vendor docs or your live account dashboard before making spend-sensitive or capacity-sensitive changes.

---

## Primary Sources

- `worker/wrangler.toml`
- `shared/lib/cron-jobs.ts`
- `worker/src/lib/rate-limit.ts`
- `shared/lib/ops-limits.ts`
- `worker/src/lib/api-keys.ts`
- `worker/src/lib/circuit-breaker.ts`
- `worker/src/handlers/http/gates.ts`
- `worker/src/cron/sync-blacklist.ts`
- `worker/src/cron/sync-mint-burn.ts`
- `worker/src/cron/sync-live-reserves.ts`
- `worker/src/cron/sync-live-reserves-config.ts`
- `worker/src/lib/address-price-providers/index.ts`
- `worker/src/cron/dex-discovery/orchestrator.ts`
- `worker/src/cron/sync-stablecoins/enrich-prices.ts`
- `worker/src/cron/sync-fx-rates.ts`
- `worker/src/cron/publish-report-card-cache.ts`
- `worker/src/cron/daily-digest.ts`
- `worker/src/cron/sync-yield-data.ts`
- `worker/src/cron/sync-yield-supplemental.ts`

---

## Worker Runtime

| Constraint                       | Current repo value                              | Source                                                        | Notes                                                                                                                                                        |
| -------------------------------- | ----------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Worker CPU budget per invocation | `30000` ms                                      | `worker/wrangler.toml`                                        | Hard repo-configured CPU cap via `[limits].cpu_ms`                                                                                                           |
| Cron expressions / trigger slots | `19`                                            | `worker/wrangler.toml`, `shared/lib/cron-jobs.ts`, `shared/lib/scheduled-runner-registry.ts` | Public status tooling groups around these trigger slots; the shared runner registry is the dispatch authority checked by `npm run check:cron-sync`; `check:cron-connections` models chained jobs and budget-only scheduled surfaces with `connectionGroup` metadata. 2026-04-17: added `daily0300Utc` slot for `prune-status-probe-runs` (90d retention). 2026-04-19: added `dewsPsiOffset` (`26,56 * * * *`) so DB-only DEWS/PSI publication is not coupled to DEX-liquidity CPU usage. 2026-04-23: added `halfHourlyChartsOffset` (`16,46 * * * *`) so `sync-stablecoin-charts` no longer shares an invocation budget with `sync-dex-liquidity`. 2026-05-11: added `daily0810Utc` (`10 8 * * *`) so `discovery-scan` no longer shares the 08:05 lane. |
| Status-tracked cron jobs         | `40`                                            | `shared/lib/cron-jobs.ts`                                     | These are the jobs expected by `/api/status`. Latest additions include `cron-staleness-watchdog` (quarter-hour freshness alerting), `project-tape` (DB-only `dewsPsiOffset` projection from existing source tables into `tape_events`), `telegram-pulse-snapshot` and `telegram-disambiguation-cleanup` (DB-only five-minute sidecars), `telegram-inactive-cleanup` and `telegram-retention-cleanup` (`daily0300Utc`), and `yield-coverage-audit` (monthly). |
| Runtime jobs actually scheduled  | `40` status-tracked jobs, plus `2` budget-only scheduled surfaces | `shared/lib/cron-jobs.ts`, `shared/lib/scheduled-runner-registry.ts` | Runtime scheduling matches the shared status metadata set for `/api/status`. The additional budget-only surfaces are Telegram registration reconciliation on the five-minute Telegram slot and the `*/5 * * * *` digest-trigger poll, which executes pending manual requests under the existing `daily-digest` lease. |
| API key default limiter          | `120 requests / 60 seconds` per key             | `shared/lib/ops-limits.ts`, `worker/src/lib/api-keys.ts`        | Non-exempt `/api/*` requests require a valid `X-API-Key`; the no-key public exceptions are health, OG images, feedback, self-serve key request/verification, the Telegram webhook, and Telegram Mini App session/mutation endpoints authenticated by signed `initData`. The D1-backed `api_key_rate_limit` table enforces per-key quotas in the normal path, and per-key overrides live in `api_keys.rate_limit_per_minute`. Protected cacheable `GET` edge-cache hits can use a bounded isolate-local fast path only for recently verified non-self-serve keys; cold, unknown, self-serve, stale-cache, cache-miss, cache-bypass, and non-`GET` requests stay on the D1-backed path or fail closed. After repeated D1 limiter failures, protected cacheable `GET` routes open a 60-second isolate-local fallback circuit capped at the self-serve quota (`30/min`); cold or unknown keys still fail closed with `503` and `Retry-After: 60`. Last-used metadata writes are best-effort. |
| Self-serve API key policy        | `30 requests / 60 seconds`, `60 days` expiry    | `shared/lib/ops-limits.ts`, `worker/src/api/api-key-requests.ts` | Email-verified key issuance through `/api/`; one active/pending self-serve key claim per normalized email. |
| Feedback limiter                 | `3 submissions / 10 minutes` per salted IP hash | `worker/src/api/feedback.ts`, `worker/src/lib/rate-limit.ts`  | Separate from the per-key limiter                                                                                                                           |
| Self-serve request limiter       | `5/hour` per salted IP hash, `3/day` per private email hash | `shared/lib/ops-limits.ts`, `worker/src/api/api-key-requests.ts` | Protects `POST /api/api-key-requests`; dependency failures fail closed with `503` and `Retry-After: 60`. |
| Self-serve verification limiter  | `20/10 minutes` per salted IP hash, `5/10 minutes` per token hash | `shared/lib/ops-limits.ts`, `worker/src/api/api-key-requests.ts` | Protects `POST /api/api-key-requests/verify`; issuance is also capped to one creation per salted IP hash per 24 hours. |
| Request attribution telemetry retention | `35 days`                                 | `worker/src/lib/request-source-attribution.ts`, `functions/lib/request-attribution.ts` | Total site-vs-external demand, worker-lane load, and per-key public-API load buckets in `api_request_consumer_stats` / `site_data_request_stats` / `api_key_request_stats` are pruned opportunistically. Worker route/source and Pages site-data counters use short isolate-local batching before D1 upsert. Set `REQUEST_SOURCE_ATTRIBUTION_DISABLED=true` on Worker and/or Pages to pause low-value route/source writes without disabling API-key auth, D1-backed rate limiting, or per-key public API load telemetry. Set `API_KEY_REQUEST_ATTRIBUTION_DISABLED=true` on the Worker only for keyed public-API spikes where per-key observability writes also need a D1 pressure relief valve; auth, rate limiting, and last-used metadata stay enabled. |

### Connection-budget operating assumption

The scheduler is deliberately structured around the repo's six-connection-per-trigger operating constraint:

- heavy lanes get isolated trigger slots (`sync-blacklist`, `sync-mint-burn`, `sync-mint-burn-extended`, `sync-dex-discovery`, `sync-dex-liquidity`)
- shared slots bundle only related work
- the quarter-hourly handler sequences jobs instead of fanning them out blindly
- `npm run check:cron-connections` fails any trigger at or above `6/6` and reports `5/6` triggers as **headroom full**
- the connection check includes budget-only scheduled surfaces that do not create separate `cron_runs` rows, currently `telegram-registration-reconciliation` and `digest-trigger-poll`

Treat any new fetch-heavy work added to an existing trigger slot as competing for the same trigger-wide outbound connection budget. A trigger at `5/6` must be treated as full for new fetch-heavy work unless the change also reduces existing peak usage or moves work to a different slot.

Current state: no job-bearing trigger is intentionally operated at `5/6`. The former full slots were given headroom by reducing Telegram send batches to 4, running supplemental yield source families serially, and moving `discovery-scan` from the 08:05 daily lane to its own 08:10 trigger.

For `sync-stablecoins`, failed upstream responses must be consumed or canceled before later passes start. Leaving non-OK bodies unread can strand the same trigger-local connection slots and starve the late fallback phase (`CoinMarketCap` -> `Jupiter` -> `DexScreener`).

The same rule applies to Worker-side integration clients. Telegram delivery, X posting, and GitHub feedback submission should always consume or cancel response bodies before returning so one idle stream does not pin a scarce connection slot.

---

## Cron Budgeting

| Area                                   | Current repo budget | Source                                                            | Notes                                                                       |
| -------------------------------------- | ------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------- |
| DEX discovery overall deadline         | `12 minutes`        | `worker/src/cron/dex-discovery/orchestrator.ts`                   | Shared deadline for the discovery pass before persistence/cleanup tail work |
| DEX discovery per-coin budget          | `25 seconds`        | `worker/src/cron/dex-discovery/orchestrator.ts`                   | Prevents one slow coin from consuming the whole staging lane |
| Live reserve sync outer deadline       | `12 minutes`        | `worker/src/lib/cron-lease.ts`                                    | Explicit wrapper budget for the serialized reserve loop before the rest of the 4-hourly slot |
| Live reserve sync internal run budget  | `10 minutes`        | `worker/src/cron/sync-live-reserves-config.ts`                    | Default cursoring budget; if the remaining budget drops below one adapter attempt, the untouched tail is marked deferred and resumed from cursor on the next run, leaving tail room for D1 cleanup and cron logging |
| Live reserve adapter I/O peak          | `2` outbound operations per adapter attempt | `worker/src/cron/reserve-adapters/concurrency.ts`, `shared/lib/cron-jobs.ts` | Coin loop is serialized, but individual adapters can fan out internally; shared fetch/RPC helpers enforce the per-attempt limiter |
| Yield publication overall deadline     | `10 minutes`        | `worker/src/lib/cron-lease.ts`                                    | Dedicated hourly `sync-yield-data` timeout after moving off the half-hourly lane |
| Yield supplemental overall deadline    | `12 minutes`        | `worker/src/lib/cron-lease.ts`                                    | Dedicated 4-hour `sync-yield-supplemental` timeout for optional protocol families |
| Telegram dispatch overall deadline     | `14 minutes`        | `worker/src/lib/cron-lease.ts`, `worker/src/handlers/scheduled/context.ts` | Dedicated five-minute Telegram lane timeout plus 30-second lease heartbeat; sized for 5,000-watcher normal SLO modeling under the 15-minute scheduled-event ceiling |
| Telegram safety source stale threshold | `2` producer intervals (`30 minutes`) | `worker/src/lib/alert-safety-source-cache.ts`, `shared/lib/cron-jobs.ts` | Safety alerts remain suppressed until `publish-report-card-cache` republishes a fresh generation-valid source snapshot |
| Telegram safety source serialized cache budget | `<= 1.5 MB` for 401 report-card rows in tests | `worker/src/lib/__tests__/alert-safety-source-cache.test.ts` | The optional safety `explain` payload is kept compact so `cache["alert:safety-source-cache"]` remains a single D1 cache row with operational headroom |
| Telegram registration reconciliation peak | `1` outbound Bot API call at a time | `worker/src/lib/telegram-webhook-registration.ts`, `shared/lib/cron-jobs.ts` | Runs serially before `dispatch-telegram-alerts` when 15-minute cache markers expire; modeled as the budget-only `telegram-registration-reconciliation` entry in the same five-minute Telegram connection group. |
| Telegram load simulation targets       | `500`, `1,000`, `5,000`, `10,000` active watchers | `scripts/ci/check-telegram-load.ts` | `npm run check:telegram-load` estimates drain time and D1 operations for single depeg, market-wide burst, DEWS+safety burst, admin broadcast, and Telegram 429 storm scenarios using the 3,600/run sender budget, 900/run pending drain, Bot API broadcast pacing, p95 send latency, and D1 write cost. The 5,000-watcher target is the next milestone; 10,000 is exploratory headroom. |
| Telegram group admin membership cache  | `5 minutes` for cached denial/admin-list copy; mutating auth revalidates per webhook | `worker/src/lib/telegram-chat-member.ts`, `worker/src/api/telegram-webhook-auth.ts` | `/subscribe`, `/unsubscribe`, `/set` in group/supergroup chats use a fresh `getChatMember` lookup for mutation authorization and fail closed if Telegram cannot confirm admin status. Soft-launch warnings and denial copy can still use the cached `getChatAdministrators` list (`telegram:chat-admins:<chat_id>`). These calls happen on webhook ingress, not inside the dispatch cron lane. |
| Live reserve history retention         | `90 days`           | `worker/src/lib/live-reserves-store-write.ts`                     | `reserve_composition_history` and `reserve_sync_attempt_history` are pruned during reserve-sync cleanup |
| Redemption route-status producer       | D1-free/static plus live-reserve metadata | `worker/src/lib/redemption-backstop-route-status.ts`, `worker/src/cron/sync-redemption-backstops.ts` | v4 route status remains four-hour snapshot data. `sync-redemption-backstops` does not add outbound route-status feed fetches; route availability comes from existing live-reserve adapter metadata, reviewed static policy, and market-implied severe-depeg overlays. |
| Blacklist sync runtime budget          | `7 minutes`         | `worker/src/cron/sync-blacklist.ts`                               | Guardrail before the trigger wrapper times out                              |
| Blacklist sync subrequest budget       | `900`               | `worker/src/cron/sync-blacklist.ts`, `worker/src/lib/evm-logs.ts` | Covers explorer/RPC calls for a single run                                  |
| Blacklist amount-recovery batch        | `100` rows / run    | `worker/src/cron/blacklist/amount-recovery.ts`                    | Conservative per-run EVM recovery cap on the 6-hour blacklist lane; statements are chunked through the shared D1 batch helper and stay under the sync subrequest budget |
| Mint/burn global request budget        | `200`               | `worker/src/cron/sync-mint-burn.ts`                               | Shared per-run request ceiling                                              |
| Mint/burn per-config budget (critical) | `60`                | `worker/src/cron/sync-mint-burn.ts`                               | Prevents one hot config from consuming the full run                         |
| Mint/burn per-config budget (extended) | `25`                | `worker/src/cron/sync-mint-burn.ts`                               | Lower ceiling for long-tail backlog drain                                   |
| Mint/burn max scan range               | `50,000` blocks     | `worker/src/cron/sync-mint-burn.ts`                               | Keeps per-request log scans bounded                                         |
| Mint/burn SQL `IN` chunk size          | `90` ids            | `worker/src/cron/sync-mint-burn.ts`                               | Current safeguard for large batched SQL                                     |
| Mint/burn event insert batch size      | `50` statements     | `worker/src/lib/mint-burn-pipeline/persistence.ts`                | Each insert binds 18 values; chunked to stay below D1 batch bind ceilings   |

### D1 overload retry posture

Cron persistence helpers retry transient D1 queue pressure through `runWithOverloadRetry()` in `worker/src/lib/cron-lease.ts`. Retried errors include `D1 DB is overloaded`, `Requests queued for too long`, D1 storage-operation reset timeouts (`D1 DB storage operation exceeded timeout ...`), and Cloudflare D1 internal-reference errors (`D1_ERROR: internal error; reference = ...`). Live reserve, redemption-backstop, cache-sentinel, and DEWS persistence paths should route bursty run-manifest writes, cleanup, prune, and chunked batch work through this helper or `batchExecute()` so one transient D1 queue spike does not fail a whole scheduled run.

---

## Upstream Fetch Budgets

| Path                                  | Current repo throttle / budget                                               | Source                                    | Notes                                                                                                              |
| ------------------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| CoinGecko onchain discovery           | `250 ms` between requests                                                    | `worker/src/lib/rate-limit.ts`            | Used by discovery crawlers                                                                                         |
| CoinGecko onchain crawl budget        | `5 minutes`                                                                  | `worker/src/lib/rate-limit.ts`            | Per-source crawl budget, not full-run deadline                                                                     |
| CoinGecko backfill throttle           | `200 ms` between requests                                                    | `worker/src/lib/rate-limit.ts`            | Used by CoinGecko backfill/admin flows                                                                             |
| GeckoTerminal crawl throttle          | `2000 ms` between requests                                                   | `worker/src/lib/rate-limit.ts`            | Conservative crawl pacing                                                                                          |
| GeckoTerminal crawl budget            | `3 minutes`                                                                  | `worker/src/lib/rate-limit.ts`            | Per-source crawl budget                                                                                            |
| GeckoTerminal probe budget            | `3 minutes` per `sync-stablecoins` run                                       | `worker/src/lib/constants.ts`             | Prevents the serialized soft-source cross-check from consuming the full 8-minute stablecoin sync timeout           |
| DexScreener discovery fallback budget | `2 minutes` shared fallback window                                           | `worker/src/lib/rate-limit.ts`            | Shared with other late-stage discovery fallbacks                                                                   |
| Jupiter price fallback                | `50` ids/request, `5 s` timeout/request, `0` retries; up to `25` low-depth primary augmentation targets/run | `worker/src/cron/sync-stablecoins/enrich-prices-jupiter-pass.ts` | Solana-only enrichment pass between CMC and DexScreener; can append agreeing Jupiter evidence to low-depth primary prices |
| DexScreener price-enrichment pass     | `10` total requests, `5 s` timeout/request, `45 s` total budget, `0` retries | `worker/src/cron/sync-stablecoins/enrich-prices-dexscreener-pass.ts` | Best-effort final fallback for missing prices through exact token-address lookups; symbol search is retired |
| Address-price augmentation group      | `90 s` total budget, `5 s` timeout/request, `0` retries                      | `worker/src/lib/address-price-providers/index.ts` | Runs during primary pricing for assets with missing prices, low-confidence prices, or previous source depth below 3 |
| DexScreener address augmentation      | opt-in; `1` request/run, `30` addresses/request                              | `worker/src/lib/address-price-providers/dexscreener.ts` | Exact `/tokens/v1/{chain}/{addresses}` lane; no symbol search; stops on hard upstream refusal                     |
| DexPaprika address augmentation       | `60` token-detail requests/run                                               | `worker/src/lib/address-price-providers/index.ts` | Public exact token-detail lookup                                                                                  |
| CoinGecko Onchain address augmentation | `5` requests/run, `30` addresses/request                                    | `worker/src/lib/address-price-providers/index.ts` | Keyed exact onchain token lookup, separate from the serialized GeckoTerminal pool probe                            |
| Alchemy Prices address augmentation   | `20` requests/run, `25` addresses/request                                    | `worker/src/lib/address-price-providers/index.ts` | Optional keyed lookup, grouped by Alchemy network id                                                              |
| Moralis address augmentation          | `3` requests/run, `100` addresses/request                                    | `worker/src/lib/address-price-providers/moralis.ts` | Optional keyed EVM batch lookup; capped below the free-plan 40k CU/day envelope for the 15-minute sync cadence     |
| Birdeye address augmentation          | `10` Solana requests/run, `1000 ms` between requests                         | `worker/src/lib/address-price-providers/index.ts` | Optional keyed Solana-only targeted gap lookup                                                                    |
| CoinMarketCap fallback                | `1 call / hour`, `10 s` timeout, `0` retries                                 | `worker/src/cron/sync-stablecoins/enrich-prices-cmc-pass.ts` | Rate-limited through cache key `cmc_last_fetch`                                                                    |
| Direct DEX API fetch phase            | `2` protocol fetches in parallel, `15 s` request timeout, deterministic page caps (`50` default) | `worker/src/cron/dex-liquidity/direct-api-policy.ts`, `worker/src/cron/dex-liquidity/direct-api-paginated.ts`, protocol fetchers | Runs inside the existing `sync-dex-liquidity` trigger; page-cap errors include resume markers for large or drifting upstream responses |
| Generic circuit breaker               | opens after `3` consecutive failures, probes every `30 minutes`              | `worker/src/lib/circuit-breaker.ts`       | Used to stop hammering degraded upstreams                                                                          |

### What this means operationally

- `sync-dex-liquidity` no longer owns discovery. It consumes staged output written by `sync-dex-discovery`.
- `sync-dex-discovery` is deliberately best-effort. Short per-source request timeouts and the 12-minute shared budget are there to force a partial `degraded` result before the platform can hard-kill the invocation. Lower-priority tier-2/tier-3 candidates are deterministically sharded across their cadence windows so one modulo run does not inherit the entire tier queue at once.
- Missing-price fallback is intentionally time-bounded so a bad upstream day cannot consume the whole `sync-stablecoins` slot.
- Any new provider added to discovery or price enrichment should come with both a throttle and a hard stop budget.

---

## Request Timeouts Worth Preserving

| Area                                | Current timeout              | Source                                            |
| ----------------------------------- | ---------------------------- | ------------------------------------------------- |
| CoinMarketCap price fallback        | `10_000 ms`                  | `worker/src/cron/sync-stablecoins/enrich-prices-cmc-pass.ts` |
| Jupiter price fallback              | `5_000 ms`                   | `worker/src/cron/sync-stablecoins/enrich-prices-jupiter-pass.ts` |
| DexScreener price fallback requests | up to `5_000 ms` per request | `worker/src/cron/sync-stablecoins/enrich-prices-dexscreener-pass.ts` |
| Direct DEX API requests             | `15_000 ms` per request     | `worker/src/cron/dex-liquidity/direct-api-policy.ts`                 |
| Ops admin proxy reads               | `20_000 ms` for `/api/status` and `/api/status-history`; `45_000 ms` for `/api/audit-depeg-history` | `functions/api/admin/[[path]].ts` |
| Live reserve adapter attempt        | `20_000 ms`                  | `worker/src/cron/sync-live-reserves-config.ts`             |
| Live reserve D1 finalize timeout    | `30_000 ms`                  | `worker/src/cron/sync-live-reserves-config.ts`             |
| Blacklist explorer / RPC reads      | `15_000 ms`                  | `worker/src/lib/fetch-retry.ts` (default timeout)          |
| Daily digest LLM call (outer)       | `12 * 60_000 ms`             | `worker/src/lib/constants.ts`                              |
| Daily digest per-attempt fetch      | `11 * 60_000 ms`             | `worker/src/cron/digest/platform.ts` (`DIGEST_FETCH_PER_ATTEMPT_TIMEOUT_MS`) |

Live reserve timeout values are resolved through `LiveReserveSyncBudgetConfig`. Production uses the checked-in defaults above; tests and operational wrappers can inject smaller or larger positive finite values to validate deferred-tail and D1-finalize behavior without changing cron code.

`/api/audit-depeg-history` is also hard-capped at `25` items per request (`limit` default and max) so the CoinGecko-backed admin audit stays page-sized over the Pages proxy.

---

## Anthropic / Digest Runtime

Current digest generation constraints that are actually encoded in repo code:

- model: `claude-opus-4-7`
- thinking: adaptive (`thinking.type = "adaptive"`)
- reasoning effort: xhigh (`output_config.effort = "xhigh"`) — dropped from `max` on 2026-04-18 after runaway-thinking exhausted `max_tokens` twice (`stopReason=max_tokens` with only a `signature_delta` at both 16k and 32k). `max` has no constraint on thinking depth on Opus 4.7; `xhigh` is Anthropic's recommended level for complex editorial work and Claude Code's own default.
- Anthropic outer timeout: `12 * 60_000 ms` (12 min), bound by `AbortSignal.timeout(ANTHROPIC_TIMEOUT_MS)` in `platform.ts`
- per-attempt fetch timeout: `11 * 60_000 ms` (11 min), local to `requestDigestCopy`; safety net so a single stalled attempt cannot consume the outer budget
- retry depth for the digest Anthropic call: `2` (max 3 attempts); the outer `AbortSignal` caps total wall time regardless
- corrective retry skip: if first-pass elapsed `>= 50%` of the outer budget (6 min), the in-process retry after quality failures is skipped; the parse is accepted with `qualityIssues` flagged `degraded`
- daily cron lease (wrapper timeout): `14 * 60_000 ms` (14 min), leaves ~2 min under Cloudflare's 15-min scheduled-event ceiling for D1 persistence, Twitter/Telegram delivery, and cron_runs logging.
- daily-digest heartbeat override: `heartbeatSec = 30`, `maxRenewFailures = 3` (see `worker/src/handlers/scheduled/context.ts` — default policy unchanged for other jobs)
- weekly cron lease: `12 * 60_000 ms` (12 min)
- max_tokens: `64000` daily, `64000` weekly — Anthropic's documented floor for Opus 4.7 at `xhigh`/`max` effort with adaptive thinking. Earlier settings of 16k → 32k at `effort: "max"` both hit `stop_reason=max_tokens` with no text emitted; the root-cause fix on 2026-04-18 lowered effort to `xhigh` and raised the ceiling to 64k in one change.
- cadence: daily scheduled run plus deferred manual admin trigger (see "Manual trigger runtime model" below)
- cost envelope (approximate, assuming single-attempt runs): Opus 4.7 input ~$5/Mtok, output ~$25/Mtok. Daily worst-case at 64k tokens ≈ $4.80; weekly worst-case at 64k ≈ $4.80. Annualized ≈ $2000 at cap. Actual usage is typically much lower since most runs don't approach the cap; the ceiling exists to survive adaptive-thinking-heavy runs. The current worker does not persist token-usage telemetry in `digest:last-trigger-result` or `cron_runs`; use provider-side Anthropic usage logs for exact spend.

### Manual trigger runtime model

`POST /api/trigger-digest` does **not** execute the digest synchronously. It writes a `digest:force-run-request` flag into the `cache` D1 table and returns 202. A dedicated `*/5 * * * *` cron slot (`digestTriggerPoll`) reads the flag on its next tick and runs the digest under scheduled-event wall-clock (15 min). Outcome is persisted to `digest:last-trigger-result` for D1 inspection and future ops-UI surfacing; the current admin panel still shows only the enqueue result from the browser session.

This two-step model exists because the repo treats long HTTP-triggered `ctx.waitUntil()` digest execution as unsafe on Cloudflare Workers. The external platform assumption is that HTTP request tail work can be canceled after a short post-response window, while scheduled events get the full scheduled-event wall-clock; an Opus 4.7 digest run takes 5–10 min, so enqueue + scheduled polling is the repo-verified safe path.

Source: `worker/src/api/admin-actions.ts` (enqueue-only HTTP handler), `worker/src/handlers/scheduled/digest-trigger-poll.ts` (polling consumer).

Source: `worker/src/lib/constants.ts` (Anthropic timeout/retries), `worker/src/lib/cron-lease.ts` (`CRON_TIMEOUT_MS` per-job lease budget), `worker/src/cron/digest/platform.ts` (model/thinking/effort, per-attempt timeout, corrective-retry skip), `worker/src/cron/daily-digest.ts` and `worker/src/cron/weekly-recap.ts` (max_tokens), `worker/src/handlers/scheduled/context.ts` (`PER_JOB_LEASE_OPTIONS` heartbeat override)

This doc deliberately does not restate Anthropic account-tier RPM / token-plan numbers because those are not repo-enforced.

---

## Design Guidance

Before adding a worker feature that touches external services:

1. Pick the trigger slot first. Shared slots are a capacity decision, not just a schedule decision.
2. Add explicit throttle constants and an overall time budget before writing the fetch loop.
3. Prefer chunked / batched writes and bounded SQL fan-out.
4. Add or reuse a circuit breaker when the feature depends on a flaky upstream.
5. Run `npm run check:cron-connections` and document the trigger-slot impact for any new outbound I/O.
6. Update this doc only with limits the repo actually enforces or depends on architecturally.

If you need current provider-plan quotas, verify them outside the repo before relying on them.
