Skip to content

Ecommerce Margin: Why Sales Grow While Profit Stands Still

9 min read

A store does 500,000 in monthly revenue, campaigns are running, ROAS looks fine, and the bank balance still does not move. Almost every time I see this on an audit, the problem is not the selling, but what leaks out of the sale before it becomes profit. This article covers how to calculate ecommerce margin properly, and where the data for it actually lives, because it is not in GA4.

Four margins, but in practice one does most of the work

In marketing - finance - ecommerce conversations, the word "margin" usually means four different things: gross margin, contribution margin, operating margin and net margin. The last two are the financial level, accounting for fixed costs, salaries and taxes. For campaign decisions you need the first two.

Gross margin: is the product priced correctly

Gross margin is the first filter. It shows how much is left after deducting the cost of the goods themselves.

Gross margin = (selling price − cost of goods) / selling price × 100%

Example: price 100, cost of goods 45, gross profit 55, gross margin 55%. At this level the product looks healthy. Except ecommerce does not end at the cost of goods. After that come shipping, packaging, payment fees, platform fees, returns, advertising, and sometimes marketplace commission. Gross margin tells you whether the product has pricing headroom. It does not tell you whether the order turns a profit.

Contribution margin: what stays from an order

For marketing decisions, contribution margin is usually more useful than gross, because it subtracts the costs tied directly to selling and delivering the order: cost of goods, packaging, payment fee, shipping, marketplace commission, discount, expected return cost, and if you define the model that way, also the advertising cost attributed to the order.

Contribution margin = (revenue − variable costs) / revenue × 100%

The problem shows up at this level. A product with a solid gross margin, after a 15% discount, free shipping, a payment fee and the cost of clicks, can drop to a few percent of contribution. Sometimes it goes below zero. From a marketing perspective, contribution margin answers one question: how much can I spend to acquire this order and still make money.

Margin vs markup: not the same thing

Margin and markup get confused constantly, and the difference can flip your planning. Markup is calculated on cost. Margin is calculated on selling price.

Cost 50, price 100. Markup is (100 − 50) / 50 = 100%. Margin is (100 − 50) / 100 = 50%. A hundred percent markup is a fifty percent margin. If marketing plans a discount "within margin" while someone in the background is thinking in markup, your acquisition targets and discount depth drift apart by half.

ROAS without margin lies

ROAS is simple: ROAS = revenue / ad cost. But the same ROAS means something different at different margins.

If contribution margin before ads is 40%, the break-even point is ROAS 1 / 0.40 = 2.5. If contribution margin is 20%, break-even is ROAS 1 / 0.20 = 5.0. In the second case, ROAS 3.0 in the Google Ads panel looks good, and the order is underwater.

That is the trap in optimising for ROAS alone. Platforms optimise within their own view of revenue and spend. They do not know your cost of goods, shipping, payment fee, return rate or marketplace commission. From the same formula you derive maximum CPA: max CPA = contribution margin in currency before ads. Above that amount, every additional order adds to the loss, even if the bars in the panel keep rising. Marketing needs its own profitability layer, because no platform will calculate it for you.

A discount does not work the way it looks

Discounts are necessary. They lift conversion, clear stock, activate demand. They go wrong when planned as if revenue and profit fell at the same rate. They do not.

Price 100, costs before discount 60, profit 40. You give a 20% discount: price drops to 80, costs stay at 60, profit is 20. A 20% discount, and profit is down by half. That is why before every promotion it is worth calculating the uplift, meaning how much volume has to grow to hold the same profit. With the numbers above, you need to sell twice as many units to break even on profit. If the promotion has no chance of delivering that increase, it lowers profit rather than raising it.

Free shipping works financially like a discount, it is just rarely counted as one. The question is not whether it lifts conversion, because it usually does. The question is at what basket value it stops eating margin. The free shipping threshold comes from the components that drive it: average margin, shipping cost, packaging, payment fee, expected return rate, AOV and product mix. A threshold copied from a competitor is a shot in the dark, because they have a different cost structure than you.

Average margin lies, the mix is what matters

Average margin is convenient for a single slide and misleading for planning. A store can have a 45% average margin, which does not mean every campaign can be planned at 45%. Under that average sit products with high margin and low volume, products with low margin and high volume, lines with a good margin but a high return rate, and products that only become profitable above a certain basket value.

So margin only makes sense once it is sliced: per SKU, per category, per campaign, per traffic source, per marketplace, per promotion type. Sliced that way, the problem lands in the data warehouse, not the finance spreadsheet.

Where the margin data is (and why it is not in GA4)

This is the part most articles about margin skip, and the part that decides whether you can control margin or only talk about it.

GA4 and Piwik PRO measure revenue, not cost. In a standard purchase event you have revenue, products, quantities, source. You do not have the cost of goods. COGS lives in the ERP, the product file or a PIM, and has to be pulled in separately. You have two routes:

  1. Send unit cost as an item-scoped parameter alongside the purchase event. Tempting, but cost changes over time (currency, suppliers, purchase-price markdowns), and you cannot fix an event after it has been sent. Rarely worth it.
  2. Keep costs in a separate table and join them to the GA4 export in BigQuery on item_id. This is cleaner, because you update costs in one place and recalculate history whenever you want.

The most common reason this join fails is mundane: the item_id in GA4 does not match the key in the cost table. The store sends a variant identifier to GA4 while costs are held on the parent product. Or GA4 gets the SKU and the cost table holds the EAN. Before you calculate any margin, check the match rate in the join. If 30% of line items do not pick up a cost, the margin in the report is fiction.

A basic query calculating contribution margin at the transaction level from the GA4 export and a cost table looks like this:

-- Contribution margin per transaction: GA4 export + product cost table
WITH line_items AS (
  SELECT
    ecommerce.transaction_id                        AS transaction_id,
    i.item_id,
    COALESCE(i.item_revenue, i.price * i.quantity)  AS item_revenue,   -- revenue after discount
    i.quantity * c.unit_cost                        AS cost_of_goods
  FROM `project.analytics_XXXXXX.events_*`,
    UNNEST(items) AS i
  LEFT JOIN `project.costs.product_costs` AS c
    ON i.item_id = c.item_id
  WHERE event_name = 'purchase'
    AND _TABLE_SUFFIX BETWEEN '20260101' AND '20260131'
),
transactions AS (
  SELECT
    transaction_id,
    SUM(item_revenue)   AS revenue,
    SUM(cost_of_goods)  AS cogs
  FROM line_items
  WHERE transaction_id IS NOT NULL
  GROUP BY transaction_id
)
SELECT
  transaction_id,
  revenue,
  cogs,
  ROUND(revenue * 0.019, 2)  AS payment_fee,   -- e.g. 1.9%, replace with your rate
  12.00                      AS shipping,        -- averaged cost, eventually per method
  2.50                       AS packaging,
  ROUND(revenue - cogs - revenue * 0.019 - 12.00 - 2.50, 2)
                             AS contribution_margin_value,
  ROUND((revenue - cogs - revenue * 0.019 - 12.00 - 2.50)
        / NULLIF(revenue, 0) * 100, 1)
                             AS contribution_margin_pct
FROM transactions
ORDER BY contribution_margin_pct;

Payment fee, shipping and packaging are hardcoded here as simplifications. In a production version you connect them from separate tables: the fee as the payment provider's percentage, shipping cost per delivery method, returns as an expected cost based on the historical rate for the category. The margin calculated this way is aggregated in Looker Studio per SKU, category and campaign, and that is the layer marketing plans on.

The last, harder step is the advertising cost attributed to an order, needed for contribution margin after ads. It requires assigning spend from Google Ads and Meta to specific transactions, which means sensible attribution. Without it you stay on contribution margin before ads, which is enough to set break-even ROAS and CPA limits per product group. Implement that threshold first, then add cost attribution.

Checklist: is your margin reporting trustworthy

Before you base budget decisions on margin, run through these points. If any one is not met, the numbers in the report are misleading.

  1. Margin calculated from the actual selling price after discount, not from the list price.
  2. Product costs current, with a known last-updated date on the cost table (currency, suppliers, purchase prices).
  3. The item_id in GA4 matches the key in the cost table, and the match rate in the join is close to 100%.
  4. Payment and platform fees included, together with marketplace commission wherever you sell.
  5. Shipping and packaging in the calculation, including subsidised free shipping.
  6. Returns counted as a real cost or as an expected cost based on the historical rate, especially in fashion, footwear and home goods.
  7. Margin available per SKU, category and campaign, not only as a single average.
  8. Break-even ROAS and maximum CPA calculated per product group, not globally.
  9. Campaign plan compared with actuals after close: real discount, AOV, mix, ad cost per order, return rate.
  10. One definition of margin across the company, calculated the same way by marketing, finance and ecommerce.

The most common mistakes in calculating margin

List price instead of the price paid, which makes margin look better than it is. Skipped payment and platform fees, which add up to a serious sum over a month. Shipping treated as "an operations matter", when it is often one of the most important margin drivers. Looking only at the average, which hides weak SKUs and unprofitable campaigns. Optimising for ROAS alone, detached from margin. Ignoring returns, which lower revenue, add handling cost and distort the profitability picture of a product. Promotions launched without an uplift calculation, planned around a sales increase rather than the protection of profit.

Closing

Growth in ecommerce is not only more sales. It is sales that, after cost of goods, fees, shipping, discount, returns and advertising, leave money behind. Revenue is the easy number, the one everyone reports. What survives all those deductions is the number the budget runs on, and most stores only see it a month too late, after the spend is already committed. The whole point is to move that number forward, in front of the decision instead of behind it.

Free analytics tools

Calculators and generators for marketers and analysts

GA4 Events Encyclopedia All GA4 events in one place
GA4 Auditor Check your setup
Attribution Simulator Compare attribution models
BigQuery Calculator Estimate GA4 + BQ costs
UTM Link Creator Tag GA4/Piwik campaigns
dataLayer Generator Ecommerce, forms, events
ROAS/ROI Calculator Campaign profitability
LTV Calculator Customer lifetime value
View all tools →

Read also

More from the English blog