LUCENTCOMMERCEGET A FREE STORE AUDITFREE AUDIT

FUNCTIONS · CHECKOUT · CRO · 24 JUNE 2025 · 8 MIN READ

Discount logic that survives your next promotion

Shopify Functions will express almost any discount rule you can describe. The limits that break a promotion are the boring ones: how many discounts can be active, and which of them combine.

A release note pinned beside the storefront it changed

Start with native discounts — they cover more than most merchants think, including tiered spend thresholds and buy-X-get-Y — and reach for a Shopify Function only when the rule genuinely cannot be expressed in the admin. Functions run as WebAssembly at checkout across three discount classes: product, order and shipping. The constraints that actually sink promotions are not the code. They are the cap of 25 active automatic discounts, the rule that a customer may use at most five product or order discount codes plus one shipping code, and the fact that stacking two product discounts on the same line item is a Shopify Plus capability.

IN SHORT

  • Shopify has three discount classes — product, order and shipping — and which ones combine is a platform rule, not something a Function can override.
  • Shopify documents a maximum of 25 active automatic discounts per store, app-based discounts included.
  • A customer can apply at most five product or order discount codes and one shipping discount code to the same order, and only where the discounts are configured to combine.
  • Multiple product discounts on the same line item is documented as a Shopify Plus capability; combining product with order discounts requires an eligible store without checkout.liquid customisation.
  • Functions run as WebAssembly with published budgets: a 256 kB compiled binary, 128 kB input, 20 kB output and around 11 million instructions for carts up to 200 lines, scaling with cart size beyond that.
  • Shopify recommends Rust over JavaScript for Functions because JavaScript exhausts the instruction budget sooner — which is a large-cart problem, so it will surface in peak week rather than in testing.

Do the native discounts first

Every discount project should begin with somebody actually trying to build the promotion in the admin, because the answer is often that it already works. Native discounts cover amount and percentage off, buy X get Y, free shipping, minimum quantity and minimum spend conditions, customer segment targeting, product and collection targeting, and both automatic and code-based application.

The reason to care is not purity. A native discount is visible to everyone in the business, editable by a merchandiser at 9pm without a deploy, and understood by every report and app that reads discount data. A Function is a codebase with a release process. Both are legitimate; only one of them is free to change on the day of the campaign.

The honest boundary: if you can describe the rule in one sentence and it refers only to quantities, spend, products and customers, try it natively first. If describing it needs the word "unless" twice, you are in Function territory.

What Functions add, and what they do not

Discount Functions inject your logic into Shopify's backend at the point the cart is priced. You define a GraphQL input query, Shopify hands your function the resulting JSON, and you return operations describing the discounts to apply. Product and order class discounts are returned through the cart lines discounts generate run; shipping discounts through the delivery options equivalent. The discountClasses field on the discount instance declares which targets run.

That gets you rules the admin cannot express: discounts driven by metafields on the customer or product, tiering computed across a mixed basket, bundle logic that depends on which variants are present, contract pricing for B2B accounts, and exclusions expressed as code rather than as a collection somebody has to maintain by hand.

What it does not get you is an escape from the platform rules. A Function cannot make a shipping discount stack with another shipping discount — Shopify documents only one shipping discount per order. It cannot raise the 25 active automatic discount ceiling. It cannot make two product discounts apply to the same line item on a plan where that is not available. Those are decided above your code, and discovering them after the logic is built is the most common way one of these projects goes over budget.

The combination rules, which are the real design constraint

This is the section worth reading twice, because promotions fail here far more often than they fail in code. Shopify's documented combination behaviour is not symmetric.

  • Order discounts combine with free shipping discounts, and product discounts combine with free shipping discounts, on any plan.
  • Product discounts combine with other product discounts on separate items on any plan.
  • Product with order discounts, and order with other order discounts, are available to eligible merchants — stores without checkout.liquid customisation.
  • Multiple product discounts on the same line item is documented as Shopify Plus only.
  • Multiple shipping discounts never combine. One per order.
  • A customer may apply at most five product or order discount codes plus one shipping discount code to an order, and only where each discount has been configured to combine. Nothing combines by default.

Design the promotion around the ceilings

Two numbers deserve to be on the plan before anyone writes code.

Twenty-five active automatic discounts, app-based ones included. That sounds generous until you meet a store with a permanent free-shipping-over-£50 rule, a loyalty tier discount, a bundle discount, an outlet rule, and then a seasonal campaign with eleven variants of the same offer by category. Retailers hit this in November, at which point the fix is a rewrite of the campaign structure under time pressure.

The structural answer is to prefer one Function with data-driven rules over many discounts with identical shapes. A single Function reading tiers from a metaobject or metafield is one active discount regardless of how many tiers it expresses, and the tiers become data a merchandiser can edit. Eleven near-identical automatic discounts is eleven of your twenty-five spent on what is really one rule.

Five codes plus one shipping code, and only among discounts explicitly marked as combinable. If your customer service team habitually issues goodwill codes, they are competing for those five slots with your campaign codes. Decide deliberately which discounts are combinable rather than ticking the box everywhere, because "combines with everything" is how a basket ends up at 80% off through a route nobody modelled.

The performance budget is a peak-week problem

Functions run with published limits, and they are generous right up until the cart is large. Shopify documents a compiled binary limit of 256 kB, runtime linear memory of 10,000 kB, stack memory of 512 kB, and — for carts up to 200 line items — around 11 million execution instructions, 128 kB of function input and 20 kB of output. Beyond 200 lines the dynamic limits scale with the number of cart lines. The input query itself is capped at 3,000 bytes with a maximum query cost of 30, list arguments at 100 elements, and metafield values over 10,000 bytes are not returned at all.

Shopify states plainly that languages compiling directly to WebAssembly outperform dynamic ones, and recommends Rust for public apps, large line-item volumes or complex computation, noting that JavaScript exhausts the instruction budget sooner. Take that seriously in proportion to your biggest carts, not your average one.

The failure mode this creates is nasty precisely because it is invisible in development. A function that comfortably prices a five-line test cart can run out of budget on the 80-line trade order that arrives during your busiest week. Test against a cart the size of your largest real one, measure it with the CLI, and treat headroom as a requirement rather than a nice result.

The metafield ceiling has a similar shape: a rules metaobject that grows through the year is fine until the day it passes 10,000 bytes and simply stops being returned. If your Function reads configuration from metafields, assert that the configuration arrived before you use it, and fail loudly rather than silently discounting nothing.

Making it survivable

The difference between a discount setup that survives four promotions and one that is rewritten each time comes down to a few habits.

  • Put the rules in data, the mechanics in code. Thresholds, percentages and date windows belong in metafields or a metaobject a merchandiser can edit. A campaign that needs a deploy to change a number will be changed wrongly, at speed, by someone else.
  • Write down the combination matrix before building: which of your discounts may stack with which, and what the worst-case basket discount is. Then build a test basket that reaches it deliberately.
  • Cap the outcome. A maximum discount percentage enforced inside the Function is cheap insurance against a combination nobody predicted.
  • Keep a kill switch. A metafield flag the Function reads and returns no discounts on. Disabling a discount in the admin is usually enough, but not when the problem is one branch of a Function that also drives three live campaigns.
  • Test the boundaries, not the happy path. One penny under the threshold and one penny over. An empty cart. A cart with a gift card. A cart with the maximum lines you have ever seen. A customer with no segment membership.
  • Audit after each campaign. Deactivate what has ended. The 25-discount ceiling is reached by accumulation, not by planning.

When the honest answer is "do less"

A fair number of complex discount briefs are attempts to encode a pricing strategy that has never been written down anywhere else. The Function becomes the only place the commercial rules exist, which makes it both a development dependency and a single point of confusion, and it is why these builds get expensive.

If the promotion cannot be explained to a customer in one line, it will not convert as well as a simpler one, and it will generate support tickets from people who believe they qualified. We have more than once quoted a discount Function, walked the client through the combination matrix and the code limits, and watched them choose a simpler offer that performed at least as well. That is a good outcome, and it is cheaper for everyone.

Questions this raises

How do you build complex discount logic on Shopify?

Try the native discounts first — they handle spend and quantity thresholds, buy X get Y, segments, collections and free shipping. Beyond that, write a Shopify Function targeting the product, order or shipping discount class. Functions run as WebAssembly, take a GraphQL input query you define, and return the discount operations to apply.

How many discounts can a Shopify store have active at once?

Shopify documents a maximum of 25 active automatic discounts, including app-based ones. There is no stated limit on inactive or draft discounts. Consolidating several similarly shaped discounts into one data-driven Function is the usual way to stay under it.

How many discount codes can a customer use on one order?

Up to five product or order discount codes plus one shipping discount code, and only where those discounts are configured to combine. Discounts do not combine automatically — combinability is set per discount.

Can two discounts apply to the same product?

Product discounts combine with other product discounts on separate items on any plan. Stacking multiple product discounts on the same line item is documented as a Shopify Plus capability. Combining product with order discounts is available to eligible stores, which excludes those with checkout.liquid customisation.

Should a discount Function be written in Rust or JavaScript?

Shopify recommends Rust for public apps, large line-item volumes and complex computation, because languages compiling directly to WebAssembly outperform dynamic ones and JavaScript exhausts the instruction budget sooner. JavaScript is reasonable for prototyping. If your largest carts run to dozens of lines, that recommendation is about you.

What are the resource limits on a Shopify Function?

Shopify publishes a 256 kB compiled binary limit, 10,000 kB linear memory, 512 kB stack, and for carts up to 200 line items roughly 11 million instructions, 128 kB input and 20 kB output, with the dynamic limits scaling as cart lines increase. The input query is capped at 3,000 bytes and a query cost of 30, and metafield values over 10,000 bytes are not returned.

NEXT STEP

Free store audit

A senior Shopify engineer reviews your storefront, theme performance and checkout, then sends a prioritised list of fixes.