uCheckeruChecker
11 min read

Predictive Email Send Optimization: Beyond STO

Send Time Optimization picks the best hour to send. But the hour is just one variable. How many emails should go out this week? Which channel? Is this subscriber ready for a promo, or should you wait? Predictive send optimization answers all of these at once. Here is the architecture, the models, and the engineering decisions behind it.


Why STO is only the beginning

STO answers a narrow question: at what hour should this specific email reach this specific person? Useful, but incomplete. A marketer makes dozens of decisions STO never touches: send frequency, campaign priority when four compete for the same week, whether to pause for a subscriber who got three emails in two days and opened none, and which channel — email, push, SMS — will actually get a response.

Predictive send optimization combines time, frequency, priority, and channel into one decision model. Instead of four isolated tools, you get a single pipeline that outputs a concrete answer per subscriber per campaign: send now, delay, or skip.

The difference is structural. STO optimizes one parameter with everything else fixed. Predictive optimization works across a decision space where parameters interact. Sending at the perfect hour does nothing if the subscriber is already overloaded.

The decision space

For each (subscriber, campaign) pair, the system resolves four dimensions:

  • Time — the hourly send slot. Classic STO.
  • Priority — when two campaigns share the same day, which goes first? Which can shift to tomorrow without losing conversion?
  • Frequency — how many emails per week can this subscriber absorb before open rates drop or they unsubscribe?
  • Channel — email, push notification, or SMS? Response probability varies per subscriber per channel.

With 24 hourly slots, 3 channels, and a binary send/skip decision per campaign, the decision space for one subscriber over one week runs into hundreds of combinations. Manual enumeration is out. For an ML model, it is routine.

Subscriber fatigue prediction

Fatigue is the biggest threat STO cannot see. A subscriber gets five promos in a week, each sent at their optimal hour. By the fifth email they stop opening — not because the timing is wrong, but because their attention ran out.

Formally: for each subscriber u at time t, define a fatigue score — the probability the next email will be ignored due to saturation rather than irrelevant content. Input signals: emails delivered in the past 7 and 14 days; rolling open rate; open rate trend (the gradient matters more than the absolute value); days since last click (a click is a strong signal, an open alone is weak); cohort unsubscribe rate over the past week.

The model — gradient boosting or logistic regression works here — trains on historical data: label 1 if the subscriber skipped the next three consecutive emails, 0 if they opened at least one. The fatigue score updates after every send.

Python
import lightgbm as lgb
import numpy as np

# emails_7d, emails_14d, open_rate_7d, open_rate_14d,
# open_rate_gradient, days_since_last_click,
# cohort_unsub_rate_7d

model = lgb.LGBMClassifier(n_estimators=300, max_depth=5, learning_rate=0.05)
model.fit(X_train, y_fatigue)

def fatigue_score(user_features: np.ndarray) -> float:
    return model.predict_proba(user_features.reshape(1, -1))[0, 1]

FATIGUE_THRESHOLD = 0.65

def should_send(user_features: np.ndarray) -> bool:
    return fatigue_score(user_features) < FATIGUE_THRESHOLD

The 0.65 threshold is a starting point. Calibrate it through an A/B test: control group receives every scheduled email; test group goes through the fatigue filter. Compare cumulative open rate over 30 days, unsubscribe count, and revenue per subscriber.

Dynamic frequency: the personal cap

A fixed cap — "no more than three emails per week" — is a compromise. For an engaged subscriber who opens everything, three is too few. For someone who opens one in five, three is already too many.

Dynamic frequency computes a personal cap. The straightforward approach: find the maximum weekly email count at which average open rate stays above a floor, say 15%. The more accurate approach treats frequency as a variable inside the fatigue model, predicting expected open rate at each possible weekly count — 1 through 5 — and picking the frequency that maximizes total opens.

Example

Subscriber A: at 2 emails/week opens 80% (1.6 opens); at 5, opens 35% (1.75). Optimum: 5. Subscriber B: at 2, opens 50% (1.0); at 5, opens 10% (0.5). Optimum: 2. A fixed cap of 3 for both underperforms the personal cap in both cases.

Campaign prioritization

Marketing calendars are crowded. One week might have a promo blast, a digest, a webinar announcement, and a cart-abandonment trigger. A subscriber with a cap of two gets exactly two. Which two?

Prioritization runs on expected value. For each (subscriber, campaign) pair, the model estimates conversion probability and multiplies by conversion value. A cart-abandonment email for a $150 item at 12% predicted purchase probability yields $18 expected value. A digest with 8% click probability and $0.50 indirect value yields $0.04. Sort by expected value, take the top K where K is the personal cap. Remaining campaigns either get dropped or shifted to the next available slot.

Python
from dataclasses import dataclass

@dataclass
class CampaignScore:
    campaign_id: str
    p_conversion: float
    value: float
    expected_value: float  # p_conversion * value

def prioritize(scores: list[CampaignScore], weekly_limit: int) -> list[CampaignScore]:
    return sorted(scores, key=lambda s: s.expected_value, reverse=True)[:weekly_limit]

scores = [
    CampaignScore("cart_abandon", 0.12, 15000, 1800),
    CampaignScore("promo_sale",   0.06,  8000,  480),
    CampaignScore("webinar",      0.09,   500,   45),
    CampaignScore("digest",       0.08,    50,    4),
]
selected = prioritize(scores, weekly_limit=2)
# -> cart_abandon (1800), promo_sale (480)

Channel routing

Some subscribers respond to push notifications at a higher rate than email. Others act on SMS but ignore their inbox. Predictive optimization adds channel selection to the equation: estimate response probability for each (subscriber, campaign, channel) triple, then rank (campaign, channel) pairs instead of campaigns alone. The constraint becomes the total cross-channel frequency cap.

Channel costs differ. SMS is often 10-50x more expensive than email. Account for this through a cost-per-send parameter subtracted from expected value. A more expensive channel is only justified when the conversion lift covers the cost difference.

Pipeline architecture

Four models — STO, fatigue, prioritization, channel — need to run as a single pipeline before each campaign send.

pipeline
Campaign Calendar (next 7 days)
  │
  ▼
Weekly Planner (batch, runs Sunday night)
  ├── 1. Fetch subscriber features (feature store)
  ├── 2. Fatigue scoring  → fatigue_score per subscriber
  ├── 3. Dynamic frequency → weekly_limit per subscriber
  ├── 4. Campaign prioritization → top-K per subscriber
  ├── 5. Channel routing → (campaign, channel) per subscriber
  ├── 6. STO per selected pair → send_slot assignment
  └── 7. Output: send_plan (subscriber_id, campaign_id,
                             channel, send_slot, expected_value)
  │
  ▼
Daily Dispatcher
  ├── Filter today's sends from send_plan
  ├── Group by slot → queues
  └── Throttled send via ESP / MTA
  │
  ▼
Feedback Loop
  ├── Events: open, click, convert, unsub
  └── → feature store + model retrain

The Weekly Planner runs once and generates the full seven-day send plan. The Daily Dispatcher executes it: pulls that day's jobs, groups by slot, sends through the ESP or MTA. The feedback loop updates the feature store after every event.

Splitting planner and dispatcher gives flexibility. If an unplanned urgent campaign appears mid-week, the planner recomputes the remaining days without touching emails already sent. Personal caps recalculate based on attention budget already spent.

STO vs. predictive optimization

Parameter
STO
Predictive optimization
Optimized variable
Send hour
Time + frequency + priority + channel
Fatigue awareness
None
Fatigue score per subscriber
Send frequency
Fixed
Dynamic, per subscriber
Campaign priority
Manual
By expected value
Multi-channel
Email only
Email + push + SMS
Implementation complexity
One model, one ESP button
Pipeline of 4+ models
Typical impact
+5-15% open rate
+15-30% revenue per subscriber

Metrics: what to measure

STO is evaluated by open rate. Predictive optimization needs different metrics, because part of its job is deliberately not sending.

Revenue per subscriber per month. The primary number. It covers sent and suppressed emails alike. If suppressing some campaigns raised per-subscriber revenue, the system is working.

Unsub rate. Reduced unsubscribes are a direct consequence of the fatigue filter. Typical outcome: 20-40% fewer unsubscribes at the same or higher revenue.

Emails per conversion. How many sends it takes to produce one conversion. Lower means the system spends subscriber attention more efficiently.

Suppression rate. If the system blocks 40% of planned sends and revenue rises, that 40% was either useless or harmful. Uncomfortable, but useful to know.

Data quality as a prerequisite

The fatigue model trains on opens and clicks. If 15% of your list is dead addresses, the model reads them as maximally fatigued subscribers and starts suppressing sends to live people with similar profiles.

Predictive optimization is more sensitive to list quality than STO. STO has one model and one input. Here you have four models, each with its own data, and errors cascade. An invalid address corrupts the fatigue score; the inflated score lowers the personal cap; the lower cap filters out campaigns the subscriber would have opened.

Three points where validation is critical:

  1. Before model training. Remove events tied to invalid addresses from the training set. Otherwise the fatigue model learns from false negatives, and the prioritization model underestimates conversion rates for entire categories.
  2. Before batch planning. The Weekly Planner should not waste compute on nonexistent addresses. Run bulk validation 12-24 hours before planning.
  3. At subscription time, in real time. Disposable addresses, typos, and spam traps get cut before they enter the feature store and pollute the data.

With STO, dirty data causes linear degradation. With a four-model pipeline, damage compounds — each bad record touches multiple models, and each model amplifies the error downstream.

Common mistakes

Measuring open rate instead of revenue. When the fatigue filter suppresses 50% of sends, open rate on the remainder rises from audience selection alone. That is a sampling artifact, not an efficiency gain. Measure at the subscriber level.

Too aggressive suppression early on. The fatigue model trains on historical data with high send frequency. Cut frequency sharply in the test period and the model underestimates engagement. Start with a loose threshold; tighten it as new data accumulates under the lower-frequency regime.

No fallback for new subscribers. A new subscriber has no history and no fatigue score. Without a cohort-based default, the system either sends nothing or floods them. A starter profile derived from cohort averages is required.

Training on invalid addresses. Worth repeating: this single mistake degrades the entire pipeline. Dead addresses are noise the models read as signal.

Implementation checklist

  1. Validate the list. Remove invalid addresses, disposable inboxes, and spam traps. Everything else depends on this.
  2. Turn on STO first. If it is not running yet, enable it. One button in most ESPs, +5-15% open rate. Get the baseline before adding complexity.
  3. Collect fatigue model data. Minimum three months of history: sends, opens, clicks, unsubscribes. Without that, there is nothing to train on.
  4. Train fatigue scoring. Start with logistic regression. Graduate to gradient boosting only after confirming the effect in an A/B test.
  5. Add dynamic frequency. Personal caps based on fatigue score. Start with a loose threshold.
  6. Add campaign prioritization. Rank by expected value. Requires a conversion model or at minimum historical CTR per campaign category.
  7. Multi-channel last. Add it only once the first three components are stable. Channel routing multiplies the decision space considerably.

Key takeaway

Predictive send optimization extends STO, it does not replace it. STO answers "when." Predictive optimization answers "when," "how often," "what," and "where" at the same time. Each additional model tightens the dependency on data quality. A clean list is the prerequisite — without it, the cascade degrades rather than compounds.


Before building a predictive pipeline, make sure your models will train on clean data. Validate your list with uChecker — address validation, risk scoring, spam trap detection, and disposable inbox filtering.

predictive email optimizationsend time optimizationfatigue predictiondynamic send frequencyML email pipelineemail validation