Navigating the Ad Tech Cold Start: Simple Solutions for a Complex Problem
CK 3788 Created at Updated at
33100
The cold start problem is the challenge of optimizing for a new campaign, advertiser, or user segment when no historical performance data is available. In ad tech, campaigns typically launch with zero click-through rate (CTR) history, no conversion data, and no knowledge of which audiences or placements will perform best. A naive system that only serves high-performing campaigns wastes the budget of new campaigns on bad placements; worse, it never gathers enough data to determine true performance, creating a catch-22.
This directly impacts advertiser revenue and publisher earnings. A new campaign that should earn $10 per 1000 impressions (RPM) but is served to poor audiences might earn only $2 RPM during its learning phase, then never recover because the budget is exhausted or the advertiser gives up. This is both an optimization problem (how to allocate budget efficiently during learning) and a data problem (how to gather enough performance signals to train models).
The core tension is exploration vs. exploitation: spending to explore new campaigns (learning their true performance) costs money now but informs better decisions later. Exploitation (serving only proven campaigns) maximizes immediate revenue but starves new campaigns of data. Ad platforms handle 100M+ new campaigns per year, so cold-start algorithms must be automatic, scalable, and not require manual tuning per campaign.
This case covers both the algorithmic approach (how to decide which campaign to serve) and the infrastructure (how to deploy models in real-time, how to log performance signals, how to retrain).
Key Requirements
Functional Requirements
Deterministic exploration strategy: new campaigns must receive a proportional share of traffic, not random
Fast model inference: predict CTR/conversion rate for a campaign-audience pair in <10ms
Online feature computation: join real-time user signals (cohort, device, location) with campaign features in the ranking pipeline
Performance signal collection: log impression, click, conversion outcomes with campaign/audience tags for model training
Dynamic reallocation: as a campaign accumulates data and true performance emerges, shift budget from exploration to high-performers
Cross-campaign transfer learning: use features and models from similar past campaigns to initialize new ones
Non-Functional Requirements
Cold-start latency addition: <5ms per request (inference overhead)
Data freshness: model retraining at least daily; exploration parameters updated every 6 hours
Exploration budget efficiency: achieve >70% of optimal revenue within first week of campaign launch (before collecting significant performance data)
Throughput: handle 1M+ new campaigns/year (avg ~3000 new campaigns/day)
Storage: feature stores must serve 50B+ features (campaign attributes, audience profiles) in <10ms
Simplified Architecture
Architecture Proposals
Option 1: Forced Random Exploration Budget with Epsilon-Greedy
The simplest approach: allocate the first X% of a campaign's budget to random exploration (uniform distribution across placements/audiences), then switch to pure exploitation (serve only top performers) once threshold is met.
The flow is:
Campaign launches: allocate 10% of budget to exploration phase
For each impression: lookup campaign state in DynamoDB; if campaign_spent < exploration_budget, choose random audience/placement; else rank placements by historical CTR and serve top-N
Log all impressions with outcome (click=0/1) to MSK
After exploration phase: batch job (AWS Glue) trains a simple model (logistic regression) on click logs
Switch to exploitation: serve only placements with predicted CTR > threshold
The infrastructure is minimal: DynamoDB tracks campaign state (budget_spent, exploration_budget, model_version). On each bid request (ECS Fargate behind ALB), lookup campaign state, check if still in exploration, decide deterministically.
Pros
Extremely simple to implement: just an if-statement on campaign state
No machine learning required; deterministic random sampling is fast and debuggable
Minimal infrastructure: one DynamoDB table + offline model training
Predictable behavior: budget allocation is transparent to advertiser ("we explore for first 10%")
Safe: uniform random exploration avoids bias toward certain placements
Cons
Inefficient: wastes 10% of budget on random placements that are clearly bad. True optimal allocation (bandit) can beat this by 20–30%.
No partial information use: if random sample shows one placement is clearly worse (0% CTR), still serves it equally
Offline model training: models are retrained nightly; changes are not reflected in serving until next day
No user-level personalization: exploration is at campaign level; doesn't account for user demographics
Transfer learning disabled: each campaign learns from scratch; no reuse of knowledge from similar past campaigns
Exploration budget tuning: 10% may be too much for high-performing campaigns, too little for low-performing ones. Requires manual adjustment per industry.
Failure Modes & Mitigations
Budget tracking falls behind: DynamoDB updates are slow; campaign reaches end of exploration phase but state isn't updated. Risk: continues exploring after budget exhausted. Mitigation: use deterministic budget calculation (campaign_launch_time + impressions_since_launch) instead of stored counter; skip database hits.
Model training data is incomplete: e.g., a placement has only 10 impressions, 0 clicks. Model predicts CTR = 0, placement is never served again. Risk: true CTR is unknown; placement is blacklisted forever. Mitigation: use Bayesian prior (add pseudocount); e.g., assume 0.1 click per 100 impressions as prior, update with observations.
Exploration phase too short: campaign exhausts budget before collecting enough data. Risk: exploitation phase has insufficient signal. Mitigation: extend exploration phase for campaigns with high variance placements; use confidence interval, not fixed percentage.
When to Choose Choose this for simple use cases where exploration efficiency is not critical: direct-response campaigns with small budgets (< $10k), or verticals where all placements perform similarly. Ideal for MVP or proof-of-concept. Works well if your team has no ML expertise and needs something operational immediately. Avoid if exploration waste exceeds 5% of total platform revenue or if advertisers demand dynamic budget allocation.
Option 2: Multi-Armed Bandit (Thompson Sampling or Epsilon-Greedy with Confidence Bounds)
This approach treats campaign-placement pairs as a bandit problem: each "arm" (placement) has an unknown CTR; the goal is to balance learning (pulling arms with uncertain CTR) vs. earning (pulling high-CTR arms).
The algorithm (Thompson Sampling):
For each placement, maintain a Beta distribution (prior: alpha=1, beta=1) in ElastiCache
On each impression: (a) sample from all Beta distributions, (b) serve the placement with highest sample, (c) observe outcome (click=0/1), (d) update Beta for that placement (alpha += clicks, beta += 1 - clicks)
As data accumulates, Beta distribution tightens; high-CTR placements are sampled more often
The algorithm naturally balances exploration (uncertain arms have wide distributions, sampled occasionally) vs. exploitation (arms with many observations have tight distributions)
The infrastructure includes:
Feature store (ElastiCache or SageMaker Feature Store): (campaign_id, placement_id) → Beta(alpha, beta)
Model serving: load all Betas for a campaign, sample once, rank
Update pipeline: after click is observed, update Beta in store (within minutes, not hours)
Multi-variate capable: can extend to (campaign, audience, creative, placement) tuples; each gets its own Beta
Cons
Added complexity: requires understanding of probabilistic inference and Beta distributions
Infrastructure cost: need online feature store (ElastiCache/SageMaker Feature Store) with sub-millisecond latency
Update coordination: concurrent updates to Beta (from multiple bid servers) can cause races. Needs atomic increment or eventual consistency.
Cold-start prior tuning: Beta(1, 1) prior assumes all arms start equal; bad prior leads to suboptimal exploration
Exploration regret: Thompson Sampling explores even after high-confidence best arm found; some waste is unavoidable
Does not use user features: bandit operates at placement level; doesn't personalize by user segment
Failure Modes & Mitigations
Feature store stale: old Beta values are served; new clicks haven't been reflected. Risk: suboptimal arm selection. Mitigation: accept staleness <5 minutes; update asynchronously; monitor staleness metric in CloudWatch.
Concurrent Beta updates cause lost increments: two requests update same (campaign, placement) simultaneously; one increment is lost. Risk: Beta parameters drift from true counts. Mitigation: use atomic increment (ElastiCache HINCRBY or DynamoDB atomic update); or use eventual consistency with reconciliation job.
Pathological prior: Beta(1, 1) leads to over-exploration for high-variance placements. Risk: wastes budget. Mitigation: tune prior per industry (e.g., Beta(10, 50) if baseline CTR is ~17%); A/B test priors.
Multinomial arm explosion: if you add dimension (campaign × audience × creative × placement = 1000 × 100 × 500 × 1000 = 50B arms), store becomes huge. Risk: feature store crashes. Mitigation: cache only hot arms (accessed in last 7 days); cold arms default to prior.
When to Choose Choose this for campaigns with modest complexity (1–100 placements) and clear performance signal (clicks, conversions). Ideal for mediums where click data is frequent (display ads, search, video pre-roll). Works well if your team has some ML/statistics expertise and can operate an online feature store. Avoid if placements are highly correlated (multicollinearity) or if you need ultra-fine personalization (per-user arms would explode).
Option 3: Transfer Learning with Campaign Embeddings and Warm-Start (SageMaker)
This approach pre-trains models on historical campaign data, then uses embeddings to initialize new campaigns. The idea: a new campaign with similar attributes to past campaigns should inherit their learned patterns, reducing cold-start penalty.
The flow is:
For all past campaigns, compute embeddings using SageMaker: embed campaign attributes (vertical, targeting criteria, creative style, budget) into 64-dimensional vector space using a neural net trained on click logs
For new campaign: compute embedding, find K nearest past campaigns using Amazon OpenSearch (k-NN) or SageMaker Feature Store
Warm-start CTR model for new campaign: initialize weights from nearest neighbors, fine-tune on new campaign's data as it arrives (via SageMaker Training)
Use ensemble: combine model predictions with bandit exploration; as campaign gets more data, phase out warm-started model, use campaign-specific model
Feature store stores both campaign embeddings and trained models; model serving looks up pre-trained model via SageMaker Endpoints, applies live features, returns CTR prediction
Pros
Reduced cold-start penalty: new campaigns inherit knowledge from similar past campaigns; can achieve 80%+ of long-run performance in first day
User-level personalization: model can use user features (demographics, segments) + campaign features; captures interaction effects
Continuous learning: as new campaign gets data, model is fine-tuned; transitions smoothly from warm-start to campaign-specific
Scalable: embeddings are computed offline; serving is just embedding lookup + model inference (<5ms)
Generalizes across verticals: embedding space captures general patterns (e.g., high-budget campaigns usually perform better)
Cons
Requires sophisticated ML pipeline: need embedding model, fine-tuning loop, model serving infrastructure
Embedding quality depends on historical data: if past campaigns are unrepresentative, embeddings are misleading
Feature engineering burden: must define campaign attributes, user features, context features; requires domain expertise
Model staleness: warm-start model becomes stale as campaign drifts. Needs continuous retraining.
Bias toward past: if past campaigns were biased toward certain placements, embeddings inherit that bias
Complex debugging: hard to explain why new campaign is assigned certain CTR predictions ("it's similar to campaign X from 2 years ago")
Failure Modes & Mitigations
Embedding drift: embedding model is retrained, old campaigns' embeddings change. Risk: fine-tuned models don't align with new embedding space. Mitigation: version embeddings via SageMaker Model Registry; retrain model serving whenever embedding model updates; use canary deployment to detect misalignment.
Poor neighbor selection: new campaign finds K neighbors, but neighbors are from different vertical/audience. Risk: warm-start model is wrong. Mitigation: use hard constraints (same vertical, similar budget range); increase K; weight neighbors by similarity distance.
Overfitting during fine-tuning: model has few examples, learns noise. Risk: model is overconfident, makes bad placement decisions. Mitigation: use regularization (L2 penalty, dropout); early stopping based on validation set; blend warm-start model with campaign-specific model (0.5 * warm + 0.5 * specific initially, shift ratio as data accrues).
When to Choose Choose this if you have a large library of historical campaigns (100k+) and need to optimize for cold-start performance. Ideal for performance marketing (direct response) where historical data is abundant and pattern transfer is strong. Works well if your ML team is experienced and can operate a full SageMaker Feature Store + model serving stack. Avoid if your campaigns are highly unique or if the historical data distribution differs significantly from new campaigns.
Comparison Table
Aspect
Option 1: Random Exploration
Option 2: Thompson Sampling
Option 3: Transfer Learning (SageMaker)
Cold-Start Efficiency
60% optimal (wastes 10% budget)
90% optimal (minimal regret)
95%+ optimal (inherits knowledge)
Latency
<1ms (state lookup)
<2ms (sample + rank)
<5ms (embedding + model)
Implementation Complexity
Very Low (1 day)
Medium (1-2 weeks)
High (2-3 months)
Infrastructure
DynamoDB
ElastiCache/Feature Store
SageMaker Feature Store + Endpoints
Real-Time Adaptation
None (nightly batch)
<5 min (stream updates)
Minutes to hours (fine-tuning)
Personalization
None (placement-level)
Limited (arm-level)
Full (user + campaign features)
Scalability
High (simple logic)
High (feature store sharded)
Medium (model serving bottleneck)
Failure Tolerance
High (falls back to random)
Medium (stale Betas)
Low (model errors compound)
Recommendation
For most ad platforms, start with Option 2 (Thompson Sampling). It balances simplicity, efficiency, and operational burden. Thompson Sampling is well-understood, proven to work in production (Reddit, Netflix use it), and scales to thousands of placements per campaign.
Use Option 1 (Random Exploration) only if:
You are in MVP phase with <10M impressions/day
Your team has zero ML expertise
Exploration waste is acceptable (< 1% of revenue)
Upgrade to Option 3 (Transfer Learning) when:
You have 500M+ impressions/day and optimization matters
Your historical campaign library is large (100k+ campaigns) and new campaign volume is high (100+/day)
Your team has ML infrastructure and can maintain SageMaker endpoints and feature stores
Advertiser churn is high (need to acquire new advertisers quickly with good performance)
Implementation guide for Option 2 (recommended):
Store Beta parameters (alpha, beta) in ElastiCache: key = campaign:{campaign_id}:placement:{placement_id}, value = {alpha, beta, timestamp}.
On bid request: fetch all (placement, Beta) pairs for campaign, sample from Beta distributions (1000 samples per placement, ~0.5ms), return argmax.
After impression: if click observed, publish event to MSK: {campaign_id, placement_id, click}.
Stream processor (Kafka Streams): consume click events, aggregate per (campaign, placement), atomic INCR in ElastiCache every 10s.
Daily: log feature store state to S3 for analysis and debugging.
Monitor: track regret (revenue difference vs. oracle) via CloudWatch; aim for <5% regret after 7 days.
Capacity & Performance Notes
Sizing for 1M new campaigns/year (3000/day) with Thompson Sampling:
Feature store size: assume ~5000 placements per campaign. 1M campaigns * 5000 placements * 16 bytes (alpha + beta + timestamp) = 80 TB. Uncompressed, too large. Solution: store only recent campaigns (last 30 days) + hot campaigns (>1000 impressions/day). Active set ~10k campaigns * 5000 placements * 16B = 800 GB. Use ElastiCache Cluster Mode with 64 shards, each 16 GB, on cache.r6g.2xlarge instances.
Sampling latency: 5000 placements, sample from each Beta (~10 operations per sample), select argmax = ~0.5ms. Acceptable.
Update throughput: 100M impressions/day, assume 5% are clicks = 5M click events/day. Kafka consumer streams these; ElastiCache updates at 50k updates/sec. Acceptable.
Model serving: inference is just sampling + argmax; no neural network forward pass needed. Runs on same bid servers as ranking logic.
Option 3 sizing (Transfer Learning):
Embedding model: neural net with 1 hidden layer (256 units), output 64-D. Weights ~150KB. Run on SageMaker (single ml.g4dn.xlarge GPU handles 10k embeddings/sec).
Feature store: store embeddings + fine-tuned models for all active campaigns via SageMaker Feature Store. Embedding: 64 floats = 256 bytes. Model (small NN): 50KB. 10k campaigns: 500MB embeddings + 500GB models. Use S3 + SageMaker Feature Store.
Model serving: embedding lookup (10 microseconds) + model inference (5ms via SageMaker Endpoint). Total 5ms, acceptable.
Fine-tuning: gradient descent on new campaign data; 1000 examples → 100ms training time on ml.m5.xlarge. Run asynchronously per campaign.