Mailglass.RateLimiter (Mailglass v1.0.0)

Copy Markdown View Source

Multi-bucket ETS token bucket rate limiter (RATE-01).

Hot path is :ets.update_counter/4 — no GenServer mailbox serialization. The TableOwner GenServer exists only to own the table (see D-22). ≈1-3μs on OTP 27 with decentralized_counters: true plus write_concurrency: :auto.

Invariants

  • :transactional bypass (D-24): check/1 with stream == :transactional returns :ok BEFORE any ETS read. Password-reset / magic-link / verify-email MUST NOT be throttled because a marketing campaign saturated the bucket. Documented as a reserved invariant in docs/api_stability.md; this is NOT a tunable.
  • Leaky-bucket continuous refill (D-23): capacity tokens refill at capacity / 60_000 tokens/ms.

Configuration

config :mailglass, :rate_limit,
  tenant_recipient: [
    default: [capacity: 100, per_minute: 100],
    overrides: [
      {{"premium-tenant", "gmail.com"}, [capacity: 500, per_minute: 500]}
    ]
  ],
  global_recipient: [
    default: [capacity: 1000, per_minute: 1000]
  ],
  sender_domain: [
    default: [capacity: 500, per_minute: 500]
  ]

Telemetry

Single-emit [:mailglass, :outbound, :rate_limit, :stop] with:

  • Measurements: %{duration_us: integer()}
  • Metadata: %{allowed: boolean(), tenant_id: String.t()}

No PII — domains are NOT emitted in telemetry (to stay inside the Phase 1 D-31 whitelist).

Summary

Functions

Returns :ok when the delivery is allowed, or {:error, %RateLimitError{}} when any bucket is depleted. :transactional stream always returns :ok.

Backward compatibility shim for check/3. Delegates to check/1 by building a synthetic message.

Functions

check(msg)

(since 0.5.0)
@spec check(Mailglass.Message.t()) :: :ok | {:error, Mailglass.RateLimitError.t()}

Returns :ok when the delivery is allowed, or {:error, %RateLimitError{}} when any bucket is depleted. :transactional stream always returns :ok.

check(tenant_id, domain, stream)

(since 0.1.0)
@spec check(String.t(), String.t(), atom()) ::
  :ok | {:error, Mailglass.RateLimitError.t()}

Backward compatibility shim for check/3. Delegates to check/1 by building a synthetic message.