Repository navigation
Email API: idempotency keys and throttles for safe retries #472
Description
Map: Map: send email from code with one PostHog call
Spec: Spec: email send API
Cast: Opus 5.5, high
Repository: PostHog/posthog
Question
Can a caller retry a send after a timeout or a rate limit without ever getting the same email twice?
Delivers end to end. The optional Idempotency-Key header (at most 256 characters) reserves {request_hash, send_id, state} in the cache for 24 hours per team and key. Same key and body after acceptance returns the same id and url and calls CDP no more. Same key, other body answers 422 idempotency_key_reused. Same key in flight answers 409 idempotency_key_in_progress. A CDP timeout keeps the reservation, so the retry reuses the send id; a definite rejection releases it. The project-secret-key-aware throttles, per key and per team, burst and sustained, with rates as settings, answer 429 rate_limited with Retry-After. Spec section: "Endpoint" (idempotency and throttles), "Error catalog".
Test seams. The public endpoint, as in the endpoint ticket. Cases: replay, reuse with another body, in-flight, timeout then retry (the CDP mock sees the same invocation id twice and the caller gets one id), definite failure then retry (a fresh attempt), and a throttle hit with Retry-After. Prior art: the hog_flow.run idempotency handling and the loops throttles.
Write scope. products/workflows/backend/services/api_send_service.py, api_send_errors.py, products/workflows/backend/presentation/views/email_sends.py, the throttle classes and settings, their tests.
Proof.
- Red: two identical calls with one key queue two invocations.
- Green: they return the same
id, and CDP is called once. - Local:
curlthe same send twice with oneIdempotency-Key; maildev shows one email and both responses carry the sameid.
Acceptance criteria
- A replay returns the same
idandurland sends nothing. - Reuse with another body answers 422; an in-flight key answers 409.
- A timeout then a retry with the same key sends at most one email.
- Throttle hits answer 429
rate_limitedwithRetry-After. - Local proof: one email in maildev for two calls.
How to build (map #461)
/tddred-green-refactor, outside-in, at the seams named above. Real Postgres, Valkey and ClickHouse; mock only what the seam section allows.- Repo skills apply where the diff touches their area:
/writing-tests,/improving-drf-endpoints,/django-migrations,/adding-api-scopes,/writing-ui-components,/writing-kea-logics,/writing-user-facing-copy,/writing-code-comments. - Public repo: no usage numbers, customer names, internal metrics, Slack content or security reasoning in code, commits or the PR. Fixture addresses use
example.com. The private note the map points at is background only; never quote it. - Shipped means a draft PR handed off for review (see the map's Notes for remotes and branches). Never mark ready, enqueue or merge.
Activity
Silthus commentedon Oct 14, 2026
Runtime: Claude Code background Agent
Worker: implement-472-idempotency-throttles
Cast: Opus 5.5, high. Wall-time cap: 120 minutes.
Lands on integration branch
spec/email-send-api.Silthus commentedon Oct 14, 2026
Claimed by conductor dispatch. Runtime: Claude Code background Agent, worker implement-472-idempotency-throttles, Opus 5.5 high. Integration branch: spec/email-send-api.
Silthus commentedon Oct 14, 2026
Lane orphaned (2026-10-14). Worker
implement-472-idempotency-throttlesended on a model rate limit before it landed or reported.555f2a81acdonspec/email-send-api/472, kept in its worktree. Nothing reachedspec/email-send-api.Silthus commentedon Oct 16, 2026
Closed: the map converged on the upstream transactional email stack that landed on PostHog/posthog master on 2026-10-16 (send through
POST /messaging_templates/{id}/send/). This lane built the parallel/emailspath and is no longer needed. See the map, #461, "Orchestration blocker".