Audio Abuse Handling (PHON-167)¶
The production Speech Analysis surface (POST /api/audio/analyze,
POST /api/audio/attribute) is unauthenticated and proxies to the Cloudflare
Containers inference host — the most expensive compute PhonoLex runs. This page
documents the guardrails: what the limits are, how to change them, how to read
the metrics, and what to do under abuse.
The dev-gated endpoints (/transcribe, /compare, /pronounce,
/feature-review, /acoustic) are not rate limited — they 404 in deployed
environments (AUDIO_DEV_ENDPOINTS gate).
Limits¶
Per client IP (CF-Connecting-IP), enforced by the AudioRateLimiter
Durable Object (packages/web/workers/src/durable/audioRateLimiter.ts — one DO
instance per IP, durable counters, fixed minute + UTC-day windows with lazy
reset):
| Limit | Env var | Default | Window |
|---|---|---|---|
| Burst | AUDIO_RATE_PER_MIN |
6 | fixed 60 s window |
| Daily quota | AUDIO_QUOTA_PER_DAY |
60 | UTC day |
| Worker→host timeout | AUDIO_HOST_TIMEOUT_MS |
120000 | per request |
The timeout is deliberately generous (120 s) so a container cold start (~60 s+, scale-to-zero) never trips it on a healthy host.
Fail-open by design: if the AUDIO_LIMITER binding is absent or the DO
call errors, the request proceeds with a console.warn — limiter availability
never blocks the product. Local dev (wrangler.dev.toml) has no binding, so
local work is never limited.
Error semantics¶
| Status | Meaning | Body |
|---|---|---|
429 + Retry-After |
Minute limit: detail says "try again in a minute". Daily quota: detail says the daily limit is reached (resets midnight UTC). X-RateLimit-Scope: minute\|day distinguishes them. |
{ detail } |
503 |
Host cold start / not ready — the frontend auto-retries (warming loop). | { warming: true, detail } |
504 |
AUDIO_HOST_TIMEOUT_MS expired — distinct from cold start; the frontend does not auto-retry. |
{ detail } |
host 4xx |
Passed through as-is with the host's detail (e.g. 413 clip too long, 422 undecodable) — unchanged v6.1 behavior. |
{ detail } |
The frontend treats 429 as terminal for the request
(analyzeProduction in
packages/web/frontend/src/services/audioAnalysisApi.ts): the limit message is
shown as an error notification, distinct from the cold-start copy, and the
warming auto-retry loop never runs for it. The user can still retry manually.
Changing the limits¶
The env vars live in packages/web/workers/wrangler.toml ([vars] for
production, [env.staging.vars] for staging). Change the values and redeploy
the Worker — no DO state migration needed (limits are passed per-request, not
stored). Removing a var falls back to the code defaults (6 / 60 / 120000).
Reading the metrics¶
Every production-surface audio request writes a server-emitted audio_request
point to Workers Analytics Engine (ANALYTICS dataset;
layout in analytics-events.md):
blob7 = outcome (ok | rate_limited | quota_exceeded | host_error | timeout |
invalid | warming), double6 = duration_ms. No audio content, no target
words, and no IP ever enters analytics.
Weekly view (includes the "Audio requests by outcome" and "Audio 429s per day" sections):
CF_ACCOUNT_ID=... CF_API_TOKEN=... packages/web/workers/scripts/analytics-report.sh
What to watch:
rate_limited/quota_exceededspikes → someone is hammering the surface.- sustained
warmingvolume → cold starts dominating UX (consider min instances). timeout/host_error→ container health, not abuse.
Under abuse¶
- Confirm via the report (429 outcomes climbing).
- Drop
AUDIO_RATE_PER_MIN/AUDIO_QUOTA_PER_DAYinwrangler.tomland redeploy (cd packages/web/workers && npx wrangler deploy— or land the change ondevelop/mainand let CI deploy). - For a targeted attacker, add a Cloudflare WAF rule on
api.phonolex.com/api/audio/*(dashboard, no deploy) — the limiter is a cost guard, not a WAF. - Rate limiting keys on
CF-Connecting-IP; a distributed botnet needs the WAF/Bot-Management layer instead.