Skip to main content

Isolate and throttle transactional and marketing email streams

Split transactional and marketing email into separate streams covers the reasoning behind the split. This guide is the operator walkthrough: provision the streams, cap each one’s throttle, pin the default, read per-stream health, and migrate an org that currently sends everything through a single stream. You will:
  1. Classify your mail into three mailbox kinds
  2. Provision three streams with separate ceilings
  3. Map templates to streams
  4. Pin sender isolation
  5. Read bounce and complaint telemetry per stream
  6. Migrate from a single stream

1. Three mailbox kinds

Almost every org’s outbound mail falls into three kinds:
  • Transactional. The recipient triggered it: password resets, one-time codes, order receipts, account alerts. Complaint rates should sit near zero, and these are the messages you cannot afford to lose.
  • Marketing. You initiated it: campaigns, newsletters, promotions. A healthy marketing program runs at complaint rates several times higher than transactional mail. That is normal, and it must never be averaged into your resets.
  • Searchable / aggregate. The recipient opted into a class, not a message: activity digests, weekly summaries, saved-search alerts. Mailbox providers grade this traffic closer to bulk than to transactional, even when it feels operational.
Orbit pins each kind to its own stream because a stream separates three things at once: the reputation surface mailbox providers grade, the throttle ceilings (maxPerMinute and dailyQuota), and the sender roster. One stream per kind means a compromised marketing blast cannot consume the transactional throttle or drag the transactional reputation down with it. Two streams are enough if you genuinely send only two kinds. The third exists for orgs whose aggregate volume is large enough to deserve its own reputation surface.

2. Provision three streams

Create each stream with POST /email/streams. Only name is required; kind defaults to transactional when omitted. The first stream you create becomes the org default.
Cap the marketing stream at creation so a campaign bug cannot empty your org’s sending headroom in one retry loop:
And the aggregate class on its own custom stream:
Leave the transactional stream uncapped (throttle omitted, both ceilings null) or capped far above peak receipt volume. GET /email/streams lists every stream with its current ceilings and default flag at any time. Stream names are unique per org, so a duplicate name returns 409: edit the existing stream instead of creating a second one.

3. Map templates to streams

In Orbit a template carries no stream field. The stream rides on the sender: each sender row holds a streamId, and a send resolves to the stream of the sender whose from address it uses (or the org default when the sender is unassigned). Mapping a template to a stream therefore means sending it from a sender bound to that stream. A workable mapping for six common templates: Bind a sender to its stream at creation with streamId, or re-assign an existing sender with PUT:
A streamId that does not resolve is rejected with 400 before anything is persisted, and "streamId": null clears the assignment, falling the sender back to the default stream. To confirm which stream a send travelled on, read the sender back with GET /email/senders and match the from address.

4. Pin sender isolation

The default stream is where any send lands when its sender carries no explicit assignment, so pin the transactional stream: anything you forget to classify takes the safe path, never the blast path.
Verify the pin with GET /email/streams: exactly one row shows "isDefault": true, and it is the transactional one.
The default is forward-only. isDefault: false on a PATCH is a no-op; the flag moves only by promoting another stream with set-default (or "isDefault": true on create/update), which demotes the previous holder in the same write. Treat a promotion as a deliberate act: there is no “un-default”, only moving the pin forward to another stream. With the pin in place, marketing and digest senders stay reserved for their own traffic because each carries an explicit streamId, and the only traffic that ever reaches the default by fallthrough is unclassified traffic, which is exactly the traffic you want on the safe path.

5. Read per-stream telemetry

Because a sender belongs to exactly one stream, per-stream health is the roll-up of that stream’s senders. Subscribe message.delivered, message.failed, and the suppression events as described in Suppression event payloads, then group events by the sending address: address to sender, sender to stream. A healthy split reads like this after a marketing incident:
Working thresholds: a complaint or hard-bounce rate above ~0.1% on the transactional stream is an incident (something misclassified is sending there); a several-percent spike on marketing is a list-hygiene problem that stays a marketing problem. The dashboard’s Channels → Email view carries the sending address on every row, so you can read the same split without webhooks. Suppressions stay org-wide on purpose: a recipient who hard-bounced a campaign is held out of future sends on every stream. Streams separate reputation exposure, not the suppression list. When per-stream isolation is not enough and a class needs its own IP reputation, the warm-up path is covered in the Email channel lifecycle guide. The two controls compose: streams split traffic classes, a dedicated IP splits at the IP layer.

6. Migration playbook

If the org currently sends everything through one stream, move in steps:
  1. Audit. GET /email/senders and label each sender by what it actually sends: transactional, marketing, aggregate, or mixed.
  2. Provision and pin. Create the streams per section 2 and pin the transactional default per section 4, so any sender you miss keeps resolving to the safe path.
  3. Move clean senders first. Purely transactional or purely marketing senders get their streamId set immediately with PUT.
  4. Split mixed senders. Register a second address for one class (for example news@yourdomain.com for marketing), shift a slice of that traffic onto it, and raise the share over one to two weeks while watching both streams’ delivery and complaint deltas.
  5. Tighten as you go. Once marketing volume lives on its own stream, cap it to the blast you actually plan:
Omitting throttle on a PATCH leaves the existing ceilings unchanged; sending { "throttle": {} } clears both back to null; each ceiling is independent, so setting maxPerMinute alone keeps dailyQuota as it was. Two behaviours to plan around while migrating:
  • In-flight sends keep their path. A send that already returned 202 resolved its stream at acceptance and completes there. A throttle PATCH applies to sends accepted after it lands, and a sender re-assignment applies to that sender’s subsequent sends. There is no mid-queue re-routing, so throttling down mid-blast slows everything still queued without changing where it sends from.
  • The default pin only moves forward. If you promote the wrong stream, promote the right one afterwards; the flag advances rather than reverting. During the migration window that makes the transactional pin the safe choice: every un-migrated sender falls through to it.
Deleting a stream later never breaks its senders: they read as unassigned and fall back to the default stream, and deleting the default promotes the most recently created remaining stream.