Skip to main contentSkip to data table
Pharos
PHAROSlive stablecoin signals

Worker and API Limits

Operational limits for the Pharos Worker and API: cron budgets, fetch connection caps, polling intervals, cache behavior, and guardrail checks.

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

ConstraintCurrent repo valueSourceNotes
Worker CPU budget per invocation30000 msworker/wrangler.tomlHard repo-configured CPU cap via [limits].cpu_ms
Cron expressions / trigger slots19worker/wrangler.toml, shared/lib/cron-jobs.ts, shared/lib/scheduled-runner-registry.tsPublic 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 jobs40shared/lib/cron-jobs.tsThese 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 scheduled40 status-tracked jobs, plus 2 budget-only scheduled surfacesshared/lib/cron-jobs.ts, shared/lib/scheduled-runner-registry.tsRuntime 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 limiter120 requests / 60 seconds per keyshared/lib/ops-limits.ts, worker/src/lib/api-keys.tsNon-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 policy30 requests / 60 seconds, 60 days expiryshared/lib/ops-limits.ts, worker/src/api/api-key-requests.tsEmail-verified key issuance through /api/; one active/pending self-serve key claim per normalized email.
Feedback limiter3 submissions / 10 minutes per salted IP hashworker/src/api/feedback.ts, worker/src/lib/rate-limit.tsSeparate from the per-key limiter
Self-serve request limiter5/hour per salted IP hash, 3/day per private email hashshared/lib/ops-limits.ts, worker/src/api/api-key-requests.tsProtects POST /api/api-key-requests; dependency failures fail closed with 503 and Retry-After: 60.
Self-serve verification limiter20/10 minutes per salted IP hash, 5/10 minutes per token hashshared/lib/ops-limits.ts, worker/src/api/api-key-requests.tsProtects POST /api/api-key-requests/verify; issuance is also capped to one creation per salted IP hash per 24 hours.
Request attribution telemetry retention35 daysworker/src/lib/request-source-attribution.ts, functions/lib/request-attribution.tsTotal 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

AreaCurrent repo budgetSourceNotes
DEX discovery overall deadline12 minutesworker/src/cron/dex-discovery/orchestrator.tsShared deadline for the discovery pass before persistence/cleanup tail work
DEX discovery per-coin budget25 secondsworker/src/cron/dex-discovery/orchestrator.tsPrevents one slow coin from consuming the whole staging lane
Live reserve sync outer deadline12 minutesworker/src/lib/cron-lease.tsExplicit wrapper budget for the serialized reserve loop before the rest of the 4-hourly slot
Live reserve sync internal run budget10 minutesworker/src/cron/sync-live-reserves-config.tsDefault 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 peak2 outbound operations per adapter attemptworker/src/cron/reserve-adapters/concurrency.ts, shared/lib/cron-jobs.tsCoin loop is serialized, but individual adapters can fan out internally; shared fetch/RPC helpers enforce the per-attempt limiter
Yield publication overall deadline10 minutesworker/src/lib/cron-lease.tsDedicated hourly sync-yield-data timeout after moving off the half-hourly lane
Yield supplemental overall deadline12 minutesworker/src/lib/cron-lease.tsDedicated 4-hour sync-yield-supplemental timeout for optional protocol families
Telegram dispatch overall deadline14 minutesworker/src/lib/cron-lease.ts, worker/src/handlers/scheduled/context.tsDedicated 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 threshold2 producer intervals (30 minutes)worker/src/lib/alert-safety-source-cache.ts, shared/lib/cron-jobs.tsSafety 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 testsworker/src/lib/__tests__/alert-safety-source-cache.test.tsThe 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 peak1 outbound Bot API call at a timeworker/src/lib/telegram-webhook-registration.ts, shared/lib/cron-jobs.tsRuns 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 targets500, 1,000, 5,000, 10,000 active watchersscripts/ci/check-telegram-load.tsnpm 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 cache5 minutes for cached denial/admin-list copy; mutating auth revalidates per webhookworker/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 retention90 daysworker/src/lib/live-reserves-store-write.tsreserve_composition_history and reserve_sync_attempt_history are pruned during reserve-sync cleanup
Redemption route-status producerD1-free/static plus live-reserve metadataworker/src/lib/redemption-backstop-route-status.ts, worker/src/cron/sync-redemption-backstops.tsv4 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 budget7 minutesworker/src/cron/sync-blacklist.tsGuardrail before the trigger wrapper times out
Blacklist sync subrequest budget900worker/src/cron/sync-blacklist.ts, worker/src/lib/evm-logs.tsCovers explorer/RPC calls for a single run
Blacklist amount-recovery batch100 rows / runworker/src/cron/blacklist/amount-recovery.tsConservative 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 budget200worker/src/cron/sync-mint-burn.tsShared per-run request ceiling
Mint/burn per-config budget (critical)60worker/src/cron/sync-mint-burn.tsPrevents one hot config from consuming the full run
Mint/burn per-config budget (extended)25worker/src/cron/sync-mint-burn.tsLower ceiling for long-tail backlog drain
Mint/burn max scan range50,000 blocksworker/src/cron/sync-mint-burn.tsKeeps per-request log scans bounded
Mint/burn SQL IN chunk size90 idsworker/src/cron/sync-mint-burn.tsCurrent safeguard for large batched SQL
Mint/burn event insert batch size50 statementsworker/src/lib/mint-burn-pipeline/persistence.tsEach 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

PathCurrent repo throttle / budgetSourceNotes
CoinGecko onchain discovery250 ms between requestsworker/src/lib/rate-limit.tsUsed by discovery crawlers
CoinGecko onchain crawl budget5 minutesworker/src/lib/rate-limit.tsPer-source crawl budget, not full-run deadline
CoinGecko backfill throttle200 ms between requestsworker/src/lib/rate-limit.tsUsed by CoinGecko backfill/admin flows
GeckoTerminal crawl throttle2000 ms between requestsworker/src/lib/rate-limit.tsConservative crawl pacing
GeckoTerminal crawl budget3 minutesworker/src/lib/rate-limit.tsPer-source crawl budget
GeckoTerminal probe budget3 minutes per sync-stablecoins runworker/src/lib/constants.tsPrevents the serialized soft-source cross-check from consuming the full 8-minute stablecoin sync timeout
DexScreener discovery fallback budget2 minutes shared fallback windowworker/src/lib/rate-limit.tsShared with other late-stage discovery fallbacks
Jupiter price fallback50 ids/request, 5 s timeout/request, 0 retries; up to 25 low-depth primary augmentation targets/runworker/src/cron/sync-stablecoins/enrich-prices-jupiter-pass.tsSolana-only enrichment pass between CMC and DexScreener; can append agreeing Jupiter evidence to low-depth primary prices
DexScreener price-enrichment pass10 total requests, 5 s timeout/request, 45 s total budget, 0 retriesworker/src/cron/sync-stablecoins/enrich-prices-dexscreener-pass.tsBest-effort final fallback for missing prices through exact token-address lookups; symbol search is retired
Address-price augmentation group90 s total budget, 5 s timeout/request, 0 retriesworker/src/lib/address-price-providers/index.tsRuns during primary pricing for assets with missing prices, low-confidence prices, or previous source depth below 3
DexScreener address augmentationopt-in; 1 request/run, 30 addresses/requestworker/src/lib/address-price-providers/dexscreener.tsExact /tokens/v1/{chain}/{addresses} lane; no symbol search; stops on hard upstream refusal
DexPaprika address augmentation60 token-detail requests/runworker/src/lib/address-price-providers/index.tsPublic exact token-detail lookup
CoinGecko Onchain address augmentation5 requests/run, 30 addresses/requestworker/src/lib/address-price-providers/index.tsKeyed exact onchain token lookup, separate from the serialized GeckoTerminal pool probe
Alchemy Prices address augmentation20 requests/run, 25 addresses/requestworker/src/lib/address-price-providers/index.tsOptional keyed lookup, grouped by Alchemy network id
Moralis address augmentation3 requests/run, 100 addresses/requestworker/src/lib/address-price-providers/moralis.tsOptional keyed EVM batch lookup; capped below the free-plan 40k CU/day envelope for the 15-minute sync cadence
Birdeye address augmentation10 Solana requests/run, 1000 ms between requestsworker/src/lib/address-price-providers/index.tsOptional keyed Solana-only targeted gap lookup
CoinMarketCap fallback1 call / hour, 10 s timeout, 0 retriesworker/src/cron/sync-stablecoins/enrich-prices-cmc-pass.tsRate-limited through cache key cmc_last_fetch
Direct DEX API fetch phase2 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 fetchersRuns inside the existing sync-dex-liquidity trigger; page-cap errors include resume markers for large or drifting upstream responses
Generic circuit breakeropens after 3 consecutive failures, probes every 30 minutesworker/src/lib/circuit-breaker.tsUsed 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

AreaCurrent timeoutSource
CoinMarketCap price fallback10_000 msworker/src/cron/sync-stablecoins/enrich-prices-cmc-pass.ts
Jupiter price fallback5_000 msworker/src/cron/sync-stablecoins/enrich-prices-jupiter-pass.ts
DexScreener price fallback requestsup to 5_000 ms per requestworker/src/cron/sync-stablecoins/enrich-prices-dexscreener-pass.ts
Direct DEX API requests15_000 ms per requestworker/src/cron/dex-liquidity/direct-api-policy.ts
Ops admin proxy reads20_000 ms for /api/status and /api/status-history; 45_000 ms for /api/audit-depeg-historyfunctions/api/admin/[[path]].ts
Live reserve adapter attempt20_000 msworker/src/cron/sync-live-reserves-config.ts
Live reserve D1 finalize timeout30_000 msworker/src/cron/sync-live-reserves-config.ts
Blacklist explorer / RPC reads15_000 msworker/src/lib/fetch-retry.ts (default timeout)
Daily digest LLM call (outer)12 * 60_000 msworker/src/lib/constants.ts
Daily digest per-attempt fetch11 * 60_000 msworker/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.