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:- Classify your mail into three mailbox kinds
- Provision three streams with separate ceilings
- Map templates to streams
- Pin sender isolation
- Read bounce and complaint telemetry per stream
- 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.
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 withPOST /email/streams. Only name is required; kind
defaults to transactional when omitted. The first stream you create becomes the
org default.
custom stream:
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 astreamId, 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:
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.GET /email/streams: exactly one row shows
"isDefault": true, and it is the transactional one.
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. Subscribemessage.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:
6. Migration playbook
If the org currently sends everything through one stream, move in steps:- Audit.
GET /email/sendersand label each sender by what it actually sends: transactional, marketing, aggregate, or mixed. - 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.
- Move clean senders first. Purely transactional or purely marketing senders
get their
streamIdset immediately withPUT. - Split mixed senders. Register a second address for one class (for example
news@yourdomain.comfor 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. - Tighten as you go. Once marketing volume lives on its own stream, cap it to the blast you actually plan:
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
PATCHapplies 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.