Skip to content

feat(sequences): durable test-send limits and shared-mailbox quota for tests - #120

Merged
clbotdev merged 4 commits into
mainfrom
test-send-limits
Oct 22, 2026
Merged

clbotdev merged 4 commits into
mainfrom
test-send-limits

Conversation

@clbotdev

Copy link
Copy Markdown
Contributor

Test sends follow the industry pattern (Mailchimp, Klaviyo, Omnisend): any address is allowed, every recipient counts once, and there's a stored cap per account. Tests through an organization's shared mailbox now draw on that mailbox's quota.

Before

  • The 50-per-hour limit was held in process memory (Map). It reset on restart or deploy and multiplied with API replicas.
  • Tests through a shared mailbox skipped the team's shared-delivery quota, so one team could send about 1,200 tests a day on the organization's ESP.

Now

  • Redis sliding windows (sequences/test-send-limits.ts), one atomic Lua script per test:
    • 100 recipients per team in any 24 h, and 50 in any 3 h;
    • 20 per email in any 24 h;
    • all or nothing: a test that doesn't fit records nothing.
    • Errors are test_send_limit_reached and test_send_email_limit_reached, each with retryAfter.
    • Configurable with TEST_SEND_DAILY_LIMIT, TEST_SEND_BURST_LIMIT, TEST_SEND_BURST_HOURS and TEST_SEND_EMAIL_DAILY_LIMIT.
  • Shared mailbox quota:
    • chargeOrganizationQuotaForTests counts tests as accepted usage on the grant and organization day/month buckets, all or nothing.
    • refundOrganizationQuotaForTests returns any tests that didn't go out.
    • Bucket checks are shared with reserveOrganizationQuota (lockBucketsWithRoom, lockActiveGrant), so the behaviour is the same. Tests through the team's own ESP aren't charged.
  • REST returns 429 for the two test-limit codes and the two shared-quota codes. The MCP tool, contract description, docs (Broadcasts → Test limits) and .env.example are updated.
  • Web shows plain-language messages for the new codes.
  • PostHog: a sequence_email_test_limited event records error_code, kind, email_id and recipients_count.

Tests

  • test-send-limits.test.ts against real Redis: the per-email cap is all or nothing, the team burst spans emails, the rolling day is enforced, retry-after is sane, and the defaults are correct.
  • organization/queries.test.ts: charging tests through a shared mailbox (all or nothing at the grant's daily limit), then a refund.
  • test-send.test.ts: each deliverable recipient counts once (deduped and case-insensitive), and nothing is sent past the limit.
  • Lint, format and typecheck pass for both the API and web.

…r tests

Test sends were capped at 50 recipients per team per hour in process memory,
which reset on restart and multiplied with replicas, and tests through an
organization's shared mailbox skipped its quota.

- Redis sliding windows: 100 per team per 24h, 50 per 3h, 20 per email per
  24h (configurable), all or nothing, with retry-after.
- Tests through a shared mailbox are charged to the grant's quota up front
  and refunded if they don't go out.
- 429 for the new codes, friendly web messages, PostHog event when limited,
  docs, contract and MCP descriptions.
The chip field accepted any number and only disabled Send. It now stops
adding at five (typed or pasted), hides the input when full and says why.
Each send stores (or reuses) an immutable copy of the email's subject and
content, and its view-in-browser link carries that version's public id, so
editing a running sequence's email no longer changes what earlier
recipients and test links show. Older links keep showing current content.
A sent broadcast's title (its subject) is now locked like its content.
Web versions show the content that was sent
@clbotdev
clbotdev merged commit 0d8531e into main Oct 22, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants