Skip to content

Email API: idempotency keys and throttles for safe retries #472

Closed as not planned

Description

@Silthus

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: curl the same send twice with one Idempotency-Key; maildev shows one email and both responses carry the same id.

Acceptance criteria

  • A replay returns the same id and url and 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_limited with Retry-After.
  • Local proof: one email in maildev for two calls.

How to build (map #461)

  • /tdd red-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

  1. self-assigned this
    on Oct 14, 2026
  2. Silthus commented on Oct 14, 2026

    @Silthus
    OwnerAuthor

    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.

  3. Silthus commented on Oct 14, 2026

    @Silthus
    OwnerAuthor

    Claimed by conductor dispatch. Runtime: Claude Code background Agent, worker implement-472-idempotency-throttles, Opus 5.5 high. Integration branch: spec/email-send-api.

  4. Silthus commented on Oct 14, 2026

    @Silthus
    OwnerAuthor

    Lane orphaned (2026-10-14). Worker implement-472-idempotency-throttles ended on a model rate limit before it landed or reported.

    • Its work is one local, unlanded commit 555f2a81acd on spec/email-send-api/472, kept in its worktree. Nothing reached spec/email-send-api.
    • Its local dev servers are stopped.
    • Not redispatched: the map is paused on the overlap blocker in its Notes. The ticket stays claimed until Michael decides.
  5. Silthus commented on Oct 16, 2026

    @Silthus
    OwnerAuthor

    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 /emails path and is no longer needed. See the map, #461, "Orchestration blocker".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions