Return on Ad Spend (ROAS)
Revenue generated for every dollar spent on advertising campaigns
Overview
Return on Ad Spend (ROAS) measures the revenue generated for every dollar spent on advertising. This critical marketing metric helps evaluate the effectiveness and profitability of advertising campaigns across different channels, campaigns, and time periods.
ROAS provides immediate feedback on campaign performance and helps optimize budget allocation across marketing channels. It's essential for understanding which advertising investments drive the most revenue and ensuring profitable growth.
ROAS = Revenue from Ads / Ad Spend × 100
4:1+
Excellent
3:1 - 4:1
Good
2:1 - 3:1
Average
<2:1
Needs Improvement
Search Ads
Typically 3-5:1 ROAS
Display Ads
Typically 1-3:1 ROAS
Attribution Considerations
ROAS calculations depend heavily on attribution models. Consider multi-touch attribution and customer lifetime value for more accurate assessment of advertising effectiveness.
Why It Matters
- Budget Optimization: Identifies highest-performing channels for budget allocation
- Campaign Effectiveness: Measures immediate impact of advertising investments
- Profitability Tracking: Ensures advertising spend generates positive returns
- Strategic Decisions: Guides marketing strategy and channel mix
- Competitive Advantage: Optimizes efficiency compared to competitors
- Stakeholder Reporting: Demonstrates marketing value to leadership
How to Measure It
Calculate ROAS by dividing attributed revenue by advertising spend, with analysis across channels, campaigns, time periods, and attribution windows.
Basic ROAS Calculation
-- Calculate ROAS across channels and campaigns
WITH campaign_performance AS (
SELECT
campaign_id,
campaign_name,
channel,
DATE_TRUNC('month', ad_date) as month,
SUM(ad_spend) as total_spend,
SUM(impressions) as total_impressions,
SUM(clicks) as total_clicks,
SUM(conversions) as total_conversions
FROM advertising_data
WHERE ad_date >= '2024-01-01'
GROUP BY 1, 2, 3, 4
),
attributed_revenue AS (
SELECT
ar.campaign_id,
ar.channel,
DATE_TRUNC('month', ar.conversion_date) as month,
SUM(ar.revenue) as attributed_revenue,
COUNT(DISTINCT ar.customer_id) as unique_customers,
AVG(ar.revenue) as avg_revenue_per_conversion
FROM ad_revenue_attribution ar
WHERE ar.conversion_date >= '2024-01-01'
AND ar.attribution_window <= 30 -- 30-day attribution window
GROUP BY 1, 2, 3
),
roas_analysis AS (
SELECT
cp.month,
cp.channel,
cp.campaign_name,
cp.total_spend,
cp.total_impressions,
cp.total_clicks,
cp.total_conversions,
COALESCE(ar.attributed_revenue, 0) as attributed_revenue,
COALESCE(ar.unique_customers, 0) as customers_acquired,
ROUND(cp.total_spend / NULLIF(cp.total_clicks, 0), 2) as cost_per_click,
ROUND(cp.total_spend / NULLIF(cp.total_conversions, 0), 2) as cost_per_conversion,
ROUND(COALESCE(ar.attributed_revenue, 0) / NULLIF(cp.total_spend, 0), 2) as roas,
ROUND(100.0 * cp.total_clicks / NULLIF(cp.total_impressions, 0), 2) as ctr,
ROUND(100.0 * cp.total_conversions / NULLIF(cp.total_clicks, 0), 2) as conversion_rate
FROM campaign_performance cp
LEFT JOIN attributed_revenue ar ON cp.campaign_id = ar.campaign_id
AND cp.channel = ar.channel
AND cp.month = ar.month
)
SELECT
month,
channel,
campaign_name,
total_spend,
attributed_revenue,
customers_acquired,
roas,
cost_per_click,
cost_per_conversion,
ctr,
conversion_rate,
RANK() OVER (PARTITION BY month ORDER BY roas DESC) as roas_rank
FROM roas_analysis
WHERE total_spend > 0
ORDER BY month DESC, roas DESC;
Multi-Touch Attribution ROAS
-- Calculate ROAS with multi-touch attribution
WITH customer_journeys AS (
SELECT
customer_id,
conversion_date,
total_revenue,
STRING_AGG(
CONCAT(channel, ':', campaign_id),
' -> '
ORDER BY touchpoint_date
) as journey_path,
COUNT(*) as touchpoint_count
FROM customer_attribution_path
WHERE conversion_date >= '2024-01-01'
GROUP BY 1, 2, 3
),
touchpoint_attribution AS (
SELECT
cap.campaign_id,
cap.channel,
cap.customer_id,
cap.conversion_date,
cj.total_revenue,
cj.touchpoint_count,
-- Linear attribution: equal weight to all touchpoints
cj.total_revenue / cj.touchpoint_count as linear_attributed_revenue,
-- Time decay: more weight to recent touchpoints
cj.total_revenue * POWER(0.7, cj.touchpoint_count - cap.touchpoint_position) as time_decay_attributed_revenue,
-- First-touch attribution
CASE WHEN cap.touchpoint_position = 1 THEN cj.total_revenue ELSE 0 END as first_touch_attributed_revenue,
-- Last-touch attribution
CASE WHEN cap.touchpoint_position = cj.touchpoint_count THEN cj.total_revenue ELSE 0 END as last_touch_attributed_revenue
FROM customer_attribution_path cap
JOIN customer_journeys cj ON cap.customer_id = cj.customer_id
AND cap.conversion_date = cj.conversion_date
),
attribution_roas AS (
SELECT
ta.channel,
ta.campaign_id,
DATE_TRUNC('month', ta.conversion_date) as month,
SUM(ta.linear_attributed_revenue) as linear_revenue,
SUM(ta.time_decay_attributed_revenue) as time_decay_revenue,
SUM(ta.first_touch_attributed_revenue) as first_touch_revenue,
SUM(ta.last_touch_attributed_revenue) as last_touch_revenue,
COUNT(DISTINCT ta.customer_id) as attributed_customers
FROM touchpoint_attribution ta
GROUP BY 1, 2, 3
),
campaign_costs AS (
SELECT
campaign_id,
channel,
DATE_TRUNC('month', ad_date) as month,
SUM(ad_spend) as total_spend
FROM advertising_data
WHERE ad_date >= '2024-01-01'
GROUP BY 1, 2, 3
)
SELECT
cc.month,
cc.channel,
cc.campaign_id,
cc.total_spend,
ar.attributed_customers,
ROUND(ar.linear_revenue / NULLIF(cc.total_spend, 0), 2) as linear_roas,
ROUND(ar.time_decay_revenue / NULLIF(cc.total_spend, 0), 2) as time_decay_roas,
ROUND(ar.first_touch_revenue / NULLIF(cc.total_spend, 0), 2) as first_touch_roas,
ROUND(ar.last_touch_revenue / NULLIF(cc.total_spend, 0), 2) as last_touch_roas,
ROUND(cc.total_spend / NULLIF(ar.attributed_customers, 0), 2) as customer_acquisition_cost
FROM campaign_costs cc
JOIN attribution_roas ar ON cc.campaign_id = ar.campaign_id
AND cc.channel = ar.channel
AND cc.month = ar.month
ORDER BY cc.month DESC, linear_roas DESC;
Lifetime Value ROAS
-- Calculate ROAS including customer lifetime value
WITH customer_ltv AS (
SELECT
customer_id,
first_purchase_date,
acquisition_channel,
acquisition_campaign_id,
SUM(revenue) as total_revenue,
COUNT(DISTINCT order_date) as order_count,
MAX(order_date) as last_purchase_date,
EXTRACT(days FROM AGE(MAX(order_date), first_purchase_date)) as customer_lifespan_days
FROM customer_orders
WHERE first_purchase_date >= '2023-01-01'
GROUP BY 1, 2, 3, 4
),
predicted_ltv AS (
SELECT
customer_id,
total_revenue,
customer_lifespan_days,
-- Simple LTV prediction based on current behavior
CASE
WHEN customer_lifespan_days > 0 THEN
total_revenue * (365.0 / customer_lifespan_days) * 2 -- Assume 2-year lifetime
ELSE total_revenue
END as predicted_lifetime_value
FROM customer_ltv
),
ltv_roas_analysis AS (
SELECT
cltv.acquisition_channel,
cltv.acquisition_campaign_id,
DATE_TRUNC('month', cltv.first_purchase_date) as acquisition_month,
COUNT(DISTINCT cltv.customer_id) as customers_acquired,
SUM(cltv.total_revenue) as actual_revenue_to_date,
SUM(pltv.predicted_lifetime_value) as predicted_total_ltv,
AVG(cltv.total_revenue) as avg_revenue_per_customer,
AVG(pltv.predicted_lifetime_value) as avg_predicted_ltv
FROM customer_ltv cltv
JOIN predicted_ltv pltv ON cltv.customer_id = pltv.customer_id
GROUP BY 1, 2, 3
),
acquisition_costs AS (
SELECT
channel,
campaign_id,
DATE_TRUNC('month', ad_date) as month,
SUM(ad_spend) as total_spend
FROM advertising_data
WHERE ad_date >= '2023-01-01'
GROUP BY 1, 2, 3
)
SELECT
lra.acquisition_month,
lra.acquisition_channel,
lra.acquisition_campaign_id,
ac.total_spend,
lra.customers_acquired,
lra.actual_revenue_to_date,
lra.predicted_total_ltv,
ROUND(ac.total_spend / NULLIF(lra.customers_acquired, 0), 2) as cac,
ROUND(lra.actual_revenue_to_date / NULLIF(ac.total_spend, 0), 2) as current_roas,
ROUND(lra.predicted_total_ltv / NULLIF(ac.total_spend, 0), 2) as predicted_ltv_roas,
ROUND(lra.avg_predicted_ltv / NULLIF(ac.total_spend / lra.customers_acquired, 0), 2) as ltv_to_cac_ratio
FROM ltv_roas_analysis lra
JOIN acquisition_costs ac ON lra.acquisition_channel = ac.channel
AND lra.acquisition_campaign_id = ac.campaign_id
AND lra.acquisition_month = ac.month
ORDER BY lra.acquisition_month DESC, predicted_ltv_roas DESC;
Measurement Best Practices
Use consistent attribution windows, account for customer lifetime value, track both short-term and long-term ROAS, and segment by audience and creative variations.
Best Practices
1. Attribution Strategy
- Use appropriate attribution windows (7, 14, 30 days)
- Consider multi-touch attribution models
- Account for view-through conversions
- Implement proper cross-device tracking
2. Comprehensive Measurement
- Track both direct and assisted conversions
- Include customer lifetime value in calculations
- Monitor brand impact and awareness metrics
- Account for organic lift from paid campaigns
3. Optimization Strategies
- Audience Targeting: Refine targeting based on ROAS performance
- Creative Testing: A/B test ad creative and messaging
- Bid Optimization: Adjust bids based on conversion value
- Landing Page Optimization: Improve post-click experience
4. Channel-Specific Analysis
- Set different ROAS targets by channel
- Consider channel role in customer journey
- Analyze seasonal performance patterns
- Factor in competitive dynamics
5. Long-term Perspective
- Balance short-term ROAS with customer lifetime value
- Consider brand building vs. performance goals
- Monitor cohort-based ROAS evolution
- Track incrementality and cannibalization