Shopify Function Instruction Limit: Measure and Cut It

The Shopify Function instruction limit is 11 million WebAssembly instructions on carts of up to 200 lines, and it scales with line count above that. Go over it in production and Shopify stops the run with InstructionCountLimitExceededError. Measure with shopify app function run, then trim input and nested loops before you port anything to Rust.

That order differs from Shopify’s own guide, which lists Rust first. The figures below show why, and where Rust still earns its place.

What is the Shopify Function instruction limit?

Shopify’s resource limits give carts of up to 200 lines 11 million instructions, 128 kB of input and 20 kB of output. Above 200 lines all three “scale proportionally”. The mechanism is visible in the function’s schema.graphql, where cart.lines carries @scaleLimits(rate: 0.005). That rate multiplied by the line count gives the multiplier, so 250 lines allowed 13.75M and 1,000 lines allowed 55M when run locally. As of the 2026-07 templates, lines and cartLines use that rate. The fulfilment option generator’s packages field uses 0.05, so check your own schema.

Other limits fail with their own errors, such as InputSizeLimitExceededError, OutputTooLargeError and LinearMemoryLimitExceededError. The error list has them all.

The Dev Dashboard shows the count and the applied limit for each production run. For validations, what a failure does to the buyer is set by a flag. A validation’s blockOnFailure covers “failures other than expected violations” and defaults to false. Read literally, a validation that runs out of instructions does not block checkout by default, so the rule goes unenforced on the carts where it failed. That reading comes from the docs. We have not triggered it on a live store.

Shopify’s forum has developers reporting failures on carts of 200 or more lines that they suspect came from bots. Those are community reports, not a Shopify statement, but they are a good reason to test large carts even if your merchants never see one.

How do you measure it locally?

Run the built module against a JSON input, passing the export value from your shopify.extension.toml:

shopify app function build
shopify app function run --input input.json --export run --json

The instructions field holds the count. Shopify says counts are deterministic, and three repeat runs here matched exactly.

A CI job cannot rely on the exit code, because going over the limit locally does not fail anything. A function using 65 million instructions on a 200-line cart still printed its output, reported "success": true and exited 0 on Shopify CLI 5.0.1. The --json output does not include the limit either, so a check has to work it out:

// Usage: node check-instructions.mjs <function-dir> <export> <fixture.json>...
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";

const [functionDir, exportName, ...fixtures] = process.argv.slice(2);
const BASE_LIMIT = 11_000_000;
const SCALE_RATE = 0.005; // @scaleLimits on cart.lines in schema.graphql
const MAX_SHARE = 0.5; // Our own budget: half the limit.

let failed = false;
for (const fixture of fixtures) {
  const inputPath = resolve(fixture);
  const lines = JSON.parse(readFileSync(inputPath, "utf8")).cart.lines.length;
  const limit = BASE_LIMIT * Math.max(1, lines * SCALE_RATE);

  const run = spawnSync(
    "shopify",
    ["app", "function", "run", "--path", functionDir, "--input", inputPath,
      "--export", exportName, "--json", "--no-input"],
    { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 },
  );
  if (run.status !== 0) throw new Error(`${fixture}: ${run.stderr}`);

  const { instructions } = JSON.parse(run.stdout);
  const share = instructions / limit;
  console.log(`${fixture}: ${lines} lines, ${instructions} of ${limit} (${(share * 100).toFixed(1)}%)`);
  if (share > MAX_SHARE) failed = true;
}
process.exitCode = failed ? 1 : 0;

This one fails the build at half the limit. That threshold is our choice, based on Shopify’s note that a function close to the limit on a typical cart can fail on a larger one. It assumes the input has cart.lines, so adjust it for functions whose input is shaped differently. If you use the template’s integration tests, runFunction returns metadata.instructionCount from version 1.1.0 of the test helpers, and Shopify publishes a baseline test built on it. Our guide to testing Shopify Functions locally covers where fixtures come from.

Where do the instructions actually go?

To find out, we wrote one rule several ways: cap the total quantity per product across lines, with limits held in a JSON metafield on the validation. This is the JavaScript version.

query Input {
  cart { lines { quantity merchandise { __typename ... on ProductVariant { product { id } } } } }
  validation { metafield(namespace: "$app:limits", key: "config") { jsonValue } }
}
const NOOP = { operations: [] };

export function run(input) {
  const limits = input.validation.metafield?.jsonValue?.limits;
  if (!limits) return NOOP;

  // One pass: total quantity per product.
  const totals = new Map();
  for (const line of input.cart.lines) {
    if (line.merchandise.__typename !== "ProductVariant") continue;
    const id = line.merchandise.product.id;
    if (limits[id] === undefined) continue;
    totals.set(id, (totals.get(id) ?? 0) + line.quantity);
  }

  const errors = [];
  for (const [id, total] of totals) {
    if (total > limits[id]) {
      errors.push({
        message: `Limit of ${limits[id]} per order exceeded`,
        target: "$.cart",
      });
    }
  }
  return errors.length ? { operations: [{ validationAdd: { errors } }] } : NOOP;
}

Each variant was built with the CLI and run on generated carts with two lines per product, quantity 1, and limits on every third product. The numbers below are for a 200-line cart, where the limit is 11M.

VariantJavaScriptRust
No-op, function ignores its input3.98Mabout 2,000
Rule above, 1.9 kB config5.37M3.53M
Same, 9.3 kB config6.01M6.62M
Limit in a metafield on each product, no config5.47M2.19M
1.9 kB config plus 12 unused fields per line16.60M5.17M
Nested version, 1.9 kB config65.01Mnot ported

This is one rule on synthetic carts, measured with app function run: CLI 5.0.1, the 2026-07 schema, @shopify/shopify_function 2.0.1, the shopify_function crate 2.3.0 and Rust 1.99.1. No dev store or production run is involved. Your ratios will differ, which is why the check above matters more than this table.

Which changes cut the count most?

About three-quarters of the JavaScript rule’s cost also appears in the no-op, which never looks at the cart. The library calls ShopifyFunction.readInput() and hands you a complete object, and that no-op’s count climbs with cart size, so returning early cannot refund the 3.98M. Shopify’s advice to return early still applies to the work after it, but on this cart only 1.39M was left to save.

So input comes first. Selecting 12 fields the function never reads tripled the count and took it to 16.6M, 151% of the limit, with no change to the code. Compare your input query with what your function reads before anything else. A related post covers input query variables, which move merchant settings out of function code.

Then look for repeated scans. The nested version calls Object.entries(config.limits) and filters the whole cart for each new product:

const entry = Object.entries(config.limits).find(([key]) => key === id);
const total = lines
  .filter((other) => other.merchandise.product?.id === id)
  .reduce((sum, other) => sum + other.quantity, 0);

It reached 11.87M on a 50-line cart, over the limit at a quarter of the cart size that 11M is meant to cover.

Two of Shopify’s other tips were real but small here. One console.log per line added 0.98M even though only about 1 kB of log survived truncation. Parsing a string value with JSON.parse instead of reading jsonValue added 0.46M on the 9.3 kB config. Take both savings, but on these numbers neither would bring an over-limit function back.

Does moving to Rust fix it?

Sometimes, and on this rule by less than Shopify’s examples suggest. Shopify strongly recommends Rust and calls migrating usually the largest single reduction. Its validation API examples show about 13 times fewer instructions for a PO box check (17,177 against 222,699).

On this rule the gap at 200 lines was 1.5 times with the small config, and it went the other way with the large one, 6.62M in Rust against 6.01M in JavaScript. With the small config Rust also lost on a one-line cart, 486,000 against 331,000. By 500 lines it was ahead again on the large config, 12.24M against 13.63M.

The difference is where the work falls. A Rust function reads input on demand, which is why its no-op costs about 2,000 at any cart size. But the crate’s JsonValue deserialises a JSON metafield into a BTreeMap of owned strings. That accounts for most of the 3.4M that a one-line cart cost with the 9.3 kB config. Dropping the central config and reading a metafield per product took the same Rust rule to 2.19M, a 2.5 times lead over JavaScript. Setting opt-level = 3, one of Shopify’s suggested experiments, saved about 2% and grew the binary from 58 KB to 70 KB.

Rust coped far better with unused fields, at 5.17M against 16.6M. Stepping through lines still cost more when the input was bigger, so trimming the query pays in both languages.

What we would do

Add the check to CI with a fixture at 200 lines and one at your largest realistic cart. Trim the query and remove repeated scans first. On this rule either fix alone came to more than a 3 times saving in JavaScript, against 1.5 times for the port.

Move to Rust when a trimmed function still sits above your budget, or when it runs often enough that the latency matters, and test the port with your real config shape before committing. Keep config small if you do, or put it on the products. A small function sitting at 10% of the limit on your largest fixture can stay in JavaScript. For checkout logic that stops a sale, set blockOnFailure deliberately instead of accepting the default, and our cart validation walkthrough shows where it goes.

Checkout rules and the custom logic behind them are part of our Shopify development work.

Need this built properly?

Get in touch