A lot of published ad-automation scripts share the same bug. The script pulls ROAS from the Google Ads or Meta API, compares it to a target someone typed into a config file, and acts. The target is usually a round number like 3.0 that came from a benchmark post. Nobody checked whether 3.0 is above or below the point at which the account loses money on every sale.

The fix is two lines of arithmetic. This post is the two lines, a small implementation in Python and JavaScript, and the reasoning for why they should run before any bid or budget logic does.

The formula

Break-even ROAS is the inverse of contribution margin. Strip every variable cost out of an order (product cost, shipping, payment fees) and what is left is the most you can spend acquiring that order before you lose money. Divide the order value by that, and you have the revenue you need per dollar of ad spend. Written out:

break-even ROAS = order value / (order value - COGS - shipping - fees) = 1 / contribution margin target ROAS = 1 / (contribution margin - desired profit %) 

(Both lines from https://www.get-ryze.ai/breakeven-roas-calculator.)

Worked example from the same page: a $60 order with $28 of variable cost has a $32 contribution margin, about 53%, so break-even ROAS is 60 / 32 = 1.88x. If you also want a 15% net margin on top, the target becomes 1 / (0.533 - 0.15), roughly 2.61x.

The calculator page also publishes a quick-reference table that is worth internalizing, because it shows how fast the number moves with margin (source: https://www.get-ryze.ai/breakeven-roas-calculator):

Contribution margin Break-even ROAS
20% 5.00x
30% 3.33x
40% 2.50x
50% 2.00x
60% 1.67x
70% 1.43x

A 3.0x ROAS is a healthy business at 50% margin and a slow bleed at 30%. Same number, opposite meaning. That is the whole reason a script should never compare ROAS to a constant it did not derive.

Why benchmarks make this worse

Benchmark posts are where most hard-coded targets come from, and the public data shows why that is a poor source for a threshold. Across 28 industries measured by Varos in 2025, the median return on ad spend was 3.31x on Google Ads and 2.19x on Meta (source: superscale.ai/learn/roas-benchmarks-by-industry, reporting Varos data). Triple Whale, looking at 18,000+ brands on Google and about 35,000 on Meta, put the medians at 3.68x and 1.86x (source: triplewhale.com/blog/whats-a-good-roas).

Within those medians the spread is enormous. In the Varos Google Ads data, hotels came in at 15.2x and financial services at 0.2x (source: superscale.ai/learn/roas-benchmarks-by-industry). A median across that range tells you nothing about whether your account is profitable. Your margin does.

The platforms also differ structurally. Meta's lower median is partly because it reaches people who were not searching, so first-touch returns look lower and the payoff appears in repeat purchases and retargeting. A Meta campaign that looks like it is "under benchmark" can still be well above break-even, and vice versa for a Google brand campaign.

The implementation

Python first. The function takes per-order economics, returns both thresholds, and refuses to run on a non-positive margin, which is the case where no ROAS can save you and the right action is to fix pricing, not bids.

from dataclasses import dataclass @dataclass(frozen=True) class OrderEconomics: order_value: float # average order value, in currency units  cogs: float # product cost per order  shipping: float # shipping / fulfillment per order  fee_rate: float # payment fees as a fraction of order value, e.g. 0.029  target_profit: float # desired net margin as a fraction, e.g. 0.15  def contribution_margin(o: OrderEconomics) -> float: fees = o.order_value * o.fee_rate return (o.order_value - o.cogs - o.shipping - fees) / o.order_value def roas_thresholds(o: OrderEconomics) -> tuple[float, float]: """Return (break_even_roas, target_roas).""" margin = contribution_margin(o) if margin <= 0: raise ValueError(f"Contribution margin is {margin:.1%}; no ROAS breaks even. Fix pricing first.") headroom = margin - o.target_profit if headroom <= 0: raise ValueError(f"Target profit {o.target_profit:.0%} exceeds margin {margin:.1%}.") return 1 / margin, 1 / headroom if __name__ == "__main__": # The calculator's worked example: $60 order, $28 variable cost (~53% margin)  example = OrderEconomics(order_value=60, cogs=22, shipping=4.5, fee_rate=0.025, target_profit=0.15) be, target = roas_thresholds(example) print(f"break-even {be:.2f}x, target {target:.2f}x") # break-even 1.88x, target 2.61x 

Note the example constructs $28 of variable cost as $22 COGS + $4.50 shipping + 2.5% fees ($1.50). The split is mine; the $60 / $28 / 1.88x figures are from the calculator page. If your fees are a flat amount per order rather than a rate, change fee_rate to a fees field; the formula does not care how the costs are grouped, only that they are all in.

The same thing in JavaScript, for anyone running this inside a Google Ads script or a Node job that calls the Marketing API:

function roasThresholds({ orderValue, cogs, shipping, feeRate, targetProfit }) { const fees = orderValue * feeRate; const margin = (orderValue - cogs - shipping - fees) / orderValue; if (margin <= 0) throw new Error(`Margin ${(margin * 100).toFixed(1)}%: no ROAS breaks even.`); const headroom = margin - targetProfit; if (headroom <= 0) throw new Error(`Target profit exceeds margin.`); return { breakEven: 1 / margin, target: 1 / headroom }; } // Dropshipping example from the calculator page: // $40 price, $18 landed cost, $5 shipping, ~3% fees -> ~40% margin, 2.53x break-even const t = roasThresholds({ orderValue: 40, cogs: 18, shipping: 5, feeRate: 0.03, targetProfit: 0 }); console.log(t.breakEven.toFixed(2)); // 2.53 

(The $40 / $18 / $5 / 3% / 2.53x example is from https://www.get-ryze.ai/breakeven-roas-calculator.)

Where it goes in the pipeline

The point of computing the threshold in code rather than typing it into a config is that it changes when the inputs change, and the inputs change more often than anyone updates configs. Shipping rates move. A supplier reprices. Refund rates drift. The calculator page's dropshipping note is that a 5% refund rate on thin margins can move break-even by half a turn (source: https://www.get-ryze.ai/breakeven-roas-calculator). Half a turn is the difference between 2.5x and 3.0x, which is the difference between scaling an ad set and killing it.

So the order of operations for any script that touches bids or budgets:

  1. Pull current per-order economics from the source of truth (Shopify COGS from supplier invoices, not the product page; actual carrier rates; processor fee schedule; trailing refund rate).
  2. Compute break_even and target from those numbers.
  3. Pull ROAS from the ad platform, for the same date range and attribution window you will judge it by.
  4. Only then compare, and compare against target, not break_even. The calculator page's rule of thumb is to run 20 to 50% above break-even to absorb attribution error, returns and seasonality (source: https://www.get-ryze.ai/breakeven-roas-calculator). Ryze's LinkedIn write-up on ROAS benchmarks lands in the same range, 25 to 50% above.

Step 3 deserves one more sentence. Platform-reported ROAS depends on the conversion window and the conversion action set, so make sure the revenue side of the ratio is the same definition you used to compute margin. Mixing a gross-revenue ROAS with a net-of-refunds margin will overstate how safe you are.

Two things the formula does not tell you

It does not tell you about lifetime value. The break-even above is per first order. A subscription or repeat-purchase business can rationally run below first-order break-even if retention is measured, not assumed; the formula still tells you how much of that bet you are making.

And it does not tell you where the break-even is per campaign. Blended account margin hides the fact that a campaign selling your 70%-margin hero product has a 1.43x break-even while the one clearing 20%-margin accessories needs 5.0x. If the data is there, compute per product line or per campaign. If you want to try the numbers interactively before coding anything, the free calculator at https://www.get-ryze.ai/breakeven-roas-calculator does the same arithmetic with a cost breakdown and no signup; Ryze, for disclosure, is who I work with, and their paid product uses the same break-even number as the guardrail for automated budget moves.

The formula is two lines. The discipline is running them first.