Detection: Frequency saturation with declining CTR
Key: frequency_saturation
Severity: Medium–High
Confidence: 75–90%
What this detection looks for
We flag an ad set as frequency-saturated when all of these are true:
- The ad set is active
- It has at least 14 days of observed metrics in the audit date range
- Average frequency in the last 7 days is at or above 5.0
- Click-through rate in the last 7 days is at least 20% lower than the prior 7 days
The signal is the combination of high frequency and declining CTR. Frequency alone is not enough — some audiences sustain high frequency without performance loss. CTR decline alone is not enough either — that can come from creative fatigue across a broad audience rather than saturation in a narrow one.
Why this matters
When the same users see your ad five or more times in a week and clicks are dropping, you have run out of new audience to reach. Continuing to spend into a saturated audience produces three consequences:
- Cost per click rises because the auction must bid more aggressively to win impressions you have already won.
- Conversion rate among those clicks drops because the remaining clickers are weaker fits.
- Brand safety risk grows — the people most likely to engage with a 10th-impression ad are not always the people you want associated with the brand.
The fix is to expand the audience or refresh the creative. Lowering the budget without doing either will only delay the same problem.
How we calculate confidence
| Condition | Confidence |
|---|---|
| Recent frequency ≥ 6.0 AND CTR drop ≥ 30% week-over-week | 90% |
| Recent frequency 5.0–6.0 AND CTR drop 20–30% | 75% |
| Any condition above not met | We don't surface the finding |
How we calculate the estimated monthly cost
We take the share of last-week spend that has become inefficient — defined as the proportional CTR drop, capped at 50% — and project it to a 30-day month.
inefficient_share = min(ctr_drop_pct, 0.50)
monthly_waste = (recent_week_spend × inefficient_share) × (30 / 7)
The cap exists because we are measuring an upper bound on inefficiency. A 60% CTR drop does not mean 60% of spend is wasted — it means a meaningful share of it is, and we are deliberately conservative in what we surface as recoverable.
What would change our mind
This finding can be a false positive in a small number of cases:
- A short, time-bound promotion. During a Black Friday or product launch promo, frequency intentionally rises and CTR is expected to decline as the urgency window closes. If the campaign is winding down on schedule, the finding is informational rather than actionable.
- Retargeting ad sets target small high-intent audiences. A retargeting ad set may sustain frequency 7+ profitably because the audience converts at high rates even on the 8th impression. If ROAS is steady or improving, the finding does not apply.
- The creative was just refreshed within the window. A creative refresh in the prior 7-day window will produce a higher CTR baseline that the recent week cannot match. Check the creative refresh date before acting.
How to fix it
- Look at your audience size lower/upper bounds for the ad set. If you are targeting fewer than ~500K people, expand the audience.
- Refresh the creative — see also
creative_concentration_risk. A single new creative variation often resets frequency to under 2.0. - Consider duplicating the ad set with a fresh audience (lookalike, broader interest stack, or larger lookalike percentage). Pause the old ad set once the duplicate is converting.
- Avoid reducing budget without changing audience or creative. You will slow the bleed but not stop it.
What we look at to make this detection
effective_statuson the ad set- Daily
frequencyfrom the ad-set-level Insights API - Daily
impressionsandclickssummed to compute click-through rate per 7-day window - Daily
spendto compute the recoverable amount - Each window's average frequency is the average of daily frequency values (not a recomputed reach/impressions ratio), since Meta reports frequency at the day level and recomputing across days requires the full unique-user set, which is not available
Source
This methodology page is generated from
apps/api/app/services/detections/frequency_saturation.py. The detection
code is open for inspection. We do not have hidden rules.