Skip to main content

Overview

When your Meta CAC starts climbing, it is rarely obvious whether a specific ad has peaked or whether the whole account is under pressure. Without a structured way to assess each ad’s health relative to its own history and the campaign blend, you end up pausing ads reactively or, worse, leaving fatigued ads running and burning budget. This report solves two connected problems. First, it classifies every active ad into a precise fatigue state each day, so you always know which ads to act on and which to leave running. Second, it forecasts how your viable ad portfolio will evolve over the next 60 days and tells you exactly how many new ads you need to launch each week to sustain a target daily spend.

How fatigue is classified

Each ad is assessed daily using two comparison axes:
  • pct_worse_than_peak — how much worse the ad’s rolling CAC is compared to its own historical best (peak_cac_normalised)
  • pct_vs_blended — how much worse the ad’s rolling CAC is compared to the campaign-type blended average across all active ads
The model uses a 4-day rolling window by default and evaluates the following classification hierarchy in order. An ad takes the first status that matches.
The ad recorded zero impressions on the current date. It is excluded from all fatigue calculations until it becomes active again.
The ad has not yet accumulated enough spend or run for enough days to produce a reliable signal. It is ineligible for fatigue assessment and treated as neutral in portfolio calculations. You can customise your minimum spend threshold, just contact us to discuss.
The ad is active and eligible but does not have a valid rolling CAC or peak CAC to compare against. This typically indicates zero recorded orders in the rolling window. No classification is possible until order data is available.
The ad’s rolling CAC is at least 20% above the blended average for its campaign type. This is a portfolio-level signal: the campaign type as a whole may have a cost problem, not just this individual ad. LOW_EFFICIENCY takes priority in the hierarchy so it is not masked by a favourable comparison to the ad’s own peak.
The ad’s rolling CAC is 20% or more worse than its own peak, but the 3-day CAC trend is negative (CAC is falling). The ad has deteriorated from its best performance but is actively recovering. Recommendation is to monitor closely.
The ad’s rolling CAC is 50% or more worse than its own peak, the 3-day trend is flat or worsening, and it was already classified as FATIGUING on the previous day. The requirement to have passed through FATIGUING first prevents a single anomalous day from triggering a FATIGUED classification. This is the strongest signal to pause or refresh the creative.
The ad’s rolling CAC is 20% or more worse than its own peak and the trend is flat or worsening. The ad has not yet crossed the 50% degradation threshold, and it may not yet have been FATIGUING yesterday, so it is in an early warning state. Review the creative and monitor daily.
The ad’s rolling CAC is within 20% of its own peak and within the campaign-type blend tolerance. No action required.
The default thresholds (20% for FATIGUING, 50% for FATIGUED, 20% above blend for LOW_EFFICIENCY, 4-day rolling window) are all configurable per customer. Each ad also carries a numeric fatigue_score between 0 and 1, where 0 is fully healthy and 1 is fully fatigued. Use this field to sort ads by severity or to build calculated fields in Looker Studio.

How the forecast works

A viable ad is one the survival model expects to still be in a non-fatigued state, meaning not yet classified as FATIGUED or LOW_EFFICIENCY. Viability is expressed as a probability derived from historical survival curves: at each ad age, the model looks at what fraction of real ads in your account remained non-fatigued at that age, and uses that as the survival rate. An ad with a survival rate of 0.8 contributes 0.8 to the expected_viable_ads count. As an ad ages, that survival rate decays to reflect the increasing likelihood it has crossed a fatigue threshold, so older ads contribute progressively less to projected spend until they effectively age out of the portfolio. The forecast takes all currently active ads on the reference date and applies a blended survival curve derived from historical data. The survival curve represents the probability that an ad launched on any given day remains viable as it ages. Beyond the maximum observed ad age in the historical data, the model extrapolates using a 5% daily decay applied to the last observed survival rate. Two summary outputs are produced:
  • avg_viable_lifespan — the expected number of days an ad remains viable after launch. It is used to calculate how many new ads you need to launch per day to keep your portfolio at a given size, because a longer lifespan means each ad covers more days before it needs replacing.
  • cumulative_survival_rate at day t — if you launched exactly one new ad per day from today, this is the number of viable ads you would have from that stream on day t. It is the building block for the reactive launch model.
The forecast outputs per-campaign-type rows and a pre-aggregated ALL row. Always filter to campaign_type = 'ALL' when building totals in Looker Studio to avoid double-counting across campaign types. The forecast horizon is 60 days forward from the reference date.

Planning with a target daily spend

The Looker Studio data source accepts @target_daily_spend as a user-input parameter. This allows you to set a daily spend goal and see whether your current ad portfolio is sufficient to support it, and how many new ads you need to launch to close any gap. Key outputs from the planning model:
FieldDescription
portfolio_adequacy_pctThe percentage of your target daily spend that your current viable ad portfolio can cover based on projected ad survival and spend rates.
target_viable_adsThe number of viable ads required to cover the target spend at the current average spend per viable ad.
steady_state_daily_adsThe average daily launch rate that would sustain the target portfolio in steady state, calculated using Little’s Law. This is a reference figure, not the reactive launch rate.
daily_ads_churnedThe number of ads estimated to leave the viable pool each day, driven by the survival curve dynamics of your current portfolio.
daily_new_ads_launchedThe reactive launches needed each day to replace churn and close any gap between the current portfolio and the target. Zero on Saturdays and Sundays.
portfolio_net_changeThe net daily change in viable ad count, equal to launches minus churn. Negative on weekends when no launches occur.
projected_total_viable_adsThe projected total viable ad count over the forecast horizon. The trajectory dips at weekends and recovers on Monday when churn accumulates and is then absorbed.
target_ad_velocity_7dThe rolling 7-day total of new ad launches needed. Use this as your weekly creative production target.
Reactive launch logic The model does not assume a constant daily launch rate. Instead, it replaces churn reactively each weekday. No launches are scheduled on Saturdays or Sundays. Monday absorbs the accumulated churn from Saturday, Sunday, and Monday itself in a single batch. If there is an initial gap between your current viable portfolio and the target at day 0, that gap is spread across the first 10 weekdays (14 calendar days) rather than requiring you to close it all on day 1. This gives you a realistic ramp plan rather than an unachievable immediate target.

Data dictionary

Ad-level daily grain. Use COUNT_DISTINCT(ad) for ad counts and SUM(spend) / SUM(new_orders) for CAC. Filter is_active_today = TRUE for active ad metrics.
NameDescription
idSurrogate key generated from date, campaign, adset, and ad.
dateThe date of the record.
day_of_weekDay of the week for the record date.
campaignMeta campaign name.
campaign_typeCampaign classification (e.g. TOF, RET, ASC).
adsetMeta ad set name.
adMeta ad name.
ad_launch_dateDate the ad first recorded impressions.
ad_ageAge of the ad in days since first impression.
impressionsDaily impressions for the ad.
spendDaily spend for the ad.
new_ordersDaily new customer orders attributed to the ad.
daily_cacDaily customer acquisition cost (spend / new_orders).
dow_cac_indexDay-of-week CAC index used for normalisation.
rolling_spendTotal spend over the rolling window.
rolling_new_ordersTotal new orders over the rolling window.
rolling_impressionsTotal impressions over the rolling window.
days_in_windowNumber of days with data in the rolling window.
rolling_cacRolling customer acquisition cost (rolling_spend / rolling_new_orders).
rolling_cac_normalisedRolling CAC adjusted for day-of-week seasonality.
cumulative_spendTotal spend since ad launch.
cumulative_ordersTotal new orders since ad launch.
cumulative_cacAll-time customer acquisition cost since launch.
peak_cac_normalisedThe lowest normalised CAC this ad has ever achieved (its historical best).
peak_dateDate on which the ad reached its peak (lowest) normalised CAC.
peak_ad_ageAd age in days at the time of peak performance.
pct_worse_than_peakHow much worse the current rolling CAC is relative to peak (0.2 = 20% worse).
campaign_type_blended_cacBlended CAC across all active ads in the same campaign type on this date.
pct_vs_blendedHow much worse the ad’s rolling CAC is relative to the campaign-type blend.
is_eligible_for_fatigueWhether the ad has met minimum spend and age thresholds for fatigue assessment.
is_active_todayWhether the ad recorded impressions on this date.
fatigue_statusFatigue classification: HEALTHY, FATIGUING, FATIGUED, IMPROVING, LOW_EFFICIENCY, RAMPING, INACTIVE, or UNKNOWN.
fatigue_scoreNumeric fatigue severity from 0 (healthy) to 1 (fully fatigued). Null for inactive or unknown ads.
window_days_configRolling window size used in this model run.
threshold_fatiguing_configThreshold for FATIGUING classification used in this model run.
threshold_fatigued_configThreshold for FATIGUED classification used in this model run.
threshold_low_efficiency_configThreshold for LOW_EFFICIENCY classification used in this model run.
Daily forecast grain per campaign type. Filter campaign_type = 'ALL' for portfolio totals.
NameDescription
idSurrogate key generated from forecast_date and campaign_type.
reference_dateThe date on which the forecast was generated (current active ads snapshot).
forecast_dateThe projected date, up to 60 days ahead of reference_date.
days_aheadNumber of days between reference_date and forecast_date.
campaign_typeCampaign type, or ‘ALL’ for the pre-aggregated portfolio total.
current_active_ad_countNumber of active ads on the reference date for this campaign type.
expected_viable_adsProjected number of viable ads on the forecast date, based on survival curve decay of the current cohort.
expected_viable_spendProjected total daily spend from viable ads on the forecast date.
avg_spend_per_viable_adAverage daily spend per viable ad, pinned to day-0 so the target does not drift as ads age.
avg_spend_per_viable_ad_rawAverage daily spend per viable ad at each projected age (not pinned).
historical_avg_daily_spendAverage daily spend over the preceding 14 days, used as a reference baseline.
historical_avg_daily_active_adsAverage number of active ads per day over the preceding 14 days.
historical_spend_per_adHistorical average spend per active ad over the preceding 14 days.
capacity_vs_historicalRatio of projected viable spend to historical average daily spend. Values below 1.0 indicate declining portfolio capacity.
avg_viable_lifespanExpected number of days an ad remains viable, calculated as the area under the blended survival curve (Little’s Law).
cumulative_survival_rateIf one new ad were launched per day from day 0, the number of viable ads from that stream on day t.
forecast_horizon_configForecast horizon in days used in this model run.
window_days_configRolling window size used in this model run.
improving_credit_configPartial credit applied to IMPROVING ads in the survival blend (0 = pure healthy basis).
Extends the forecast model with reactive launch planning based on the @target_daily_spend parameter. Filter campaign_type = 'ALL' for portfolio totals.
NameDescription
portfolio_adequacy_pctPercentage of the target daily spend covered by the current viable ad portfolio.
target_viable_adsNumber of viable ads required to cover the target spend at the current average spend per viable ad.
steady_state_daily_adsLittle’s Law reference: average daily launch rate needed to sustain the target portfolio in steady state.
daily_ads_churnedAds lost to fatigue each day (shown as a negative value for waterfall visualisations).
daily_new_ads_launchedReactive new ad launches per day (zero on Saturdays and Sundays; elevated on Mondays).
portfolio_net_changeNet daily change in viable ad count (launches minus churn). Negative on weekends.
projected_total_viable_adsProjected running total of viable ads each day. Dips at weekends, recovers on Monday.
target_ad_velocity_7dRolling 7-day total of new ad launches needed. Use as your weekly creative production target.
surviving_viable_adsYesterday’s projected portfolio minus today’s churn, before new launches are added.
cumulative_new_ads_launchedRunning total of all new ad launches across the forecast horizon.

Questions this report answers

Filter the fatigue status table to fatigue_status = 'FATIGUED' to find ads that have deteriorated significantly from their own peak, are not recovering, and confirmed that decline the previous day. These are the strongest candidates for pausing or replacing with a refreshed creative. Ads showing FATIGUING are early warnings: worth reviewing but not necessarily pausing yet (unless performance warrants).
Set your @target_daily_spend parameter in the Looker Studio report and check portfolio_adequacy_pct. If it is below 100%, your current portfolio cannot cover the target. Use target_ad_velocity_7d to understand your weekly creative output requirement and compare it against your actual launch rate to close the gap.
The reactive launch model does not schedule new ad launches on Saturdays or Sundays, reflecting typical team working patterns. Ads continue to churn over the weekend through natural fatigue, but replacements are not added until Monday. Monday absorbs the accumulated churn from Saturday and Sunday in a single catch-up batch, which is why the portfolio trajectory dips slightly on weekends and recovers sharply on Mondays.
The model uses three layers of protection against single-day noise. First, it uses a 4-day rolling CAC rather than the daily figure, smoothing short-term fluctuations. Second, it requires the 3-day CAC trend to be flat or worsening (not a temporarily bad day that is already recovering). Third, it requires the ad to have been FATIGUING on the previous day before promoting it to FATIGUED. An ad cannot jump straight to FATIGUED on the basis of a single spike.
The first two to three weeks are driven by the observed survival curves of your current active ad cohort and are the most reliable. Beyond that, the forecast extrapolates with a 5% daily decay beyond the maximum observed ad age and assumes constant average spend per ad. The 60-day horizon is best read as a directional trajectory rather than a precise prediction. Use it to understand structural gaps in your creative pipeline, and revisit the forecast weekly as new ads launch and old ones retire.

Need help? Contact our team or book a demo.