All posts

Understanding Instruction Count in Shopify Functions

Learn how Shopify Functions instruction count works, how to measure it with Shopify CLI, and practical ways to optimise Function performance.

Shopify

If you've built Shopify Functions, you've probably come across the 11 million instruction limit.

It sounds like a lot, but what actually counts as an instruction? More importantly, how do you know how many instructions your Function is using?

What is an instruction?

Shopify Functions run as WebAssembly (Wasm), and Shopify limits the number of WebAssembly instructions that can be executed.

For carts with up to 200 line items, that limit is currently:

11,000,000 instructions

For carts larger than 200 lines, Shopify dynamically scales the limit based on the number of cart lines.

An instruction isn't the same thing as a line of TypeScript. Something simple like this:

const eligibleLines = input.cart.lines.filter(isEligible);

can result in many WebAssembly instructions once compiled and executed.

This means the important question isn't how much TypeScript you've written. It's how much work the compiled Function actually performs.

Repeated work adds up

Consider something like:

for (const line of input.cart.lines) {
  const config = JSON.parse(
    line.merchandise.product.metafield?.value ?? '{}'
  );

  // apply some logic
}

JSON.parse() might not look particularly expensive, but we're now doing it for every line in the cart.

A useful mental model is:

base cost + (number of lines × work per line)

That's not how Shopify literally calculates your limit, but it's a useful way to think about how a Function scales.

A Function that works perfectly with three cart lines might behave very differently with 100 or 200.

How to check instruction count

Fortunately, there's no need to try to calculate this yourself.

Shopify CLI can execute the compiled Function locally against an input:

shopify app function run --input input.json

You can also use JSON output:

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

The performance measurements from the execution let you see how your Function is behaving against its resource limits.

I like to test the same Function against several different inputs:

10 lines
50 lines
100 lines
200 lines

The interesting part isn't only whether you're below 11 million. It's how the instruction count grows as the input grows.

If your instruction count increases disproportionately as the cart gets larger, it's worth looking at what you're doing repeatedly.

A few optimisation tricks

Once you start measuring instruction count, small changes can sometimes make a surprising difference.

Do work once

Look for anything inside a loop that doesn't need to be there.

Instead of:

for (const line of input.cart.lines) {
  const config = JSON.parse(input.shop.config.value);

  // ...
}

parse it once:

const config = JSON.parse(input.shop.config.value);

for (const line of input.cart.lines) {
  // ...
}

It's obvious in a small example, but this kind of repeated work is much easier to miss when it's hidden inside a helper.

Consider bitmasks for flags

If you're dealing with a known set of boolean flags, you might not need arrays, objects or sets to represent them.

A bitmask can store several flags in a single integer:

const CUSTOMER = 1 << 0;
const VIP = 1 << 1;
const WHOLESALE = 1 << 2;

const flags = CUSTOMER | VIP;

if (flags & VIP) {
  // VIP logic
}

It's probably not something I'd introduce into an ordinary React component just to save a few operations.

Inside a Function that's performing the same checks repeatedly, though, simpler data representations can be worth exploring.

Watch repeated searches

Convenient code can hide quite a lot of work.

For example:

for (const line of input.cart.lines) {
  const rule = rules.find(
    rule => rule.productId === line.merchandise.id
  );
}

For every cart line, we potentially scan the rules array again.

You could instead build some form of lookup before processing the cart. Whether that's actually cheaper depends on your data, implementation and compiled Function, which brings us back to the important part: measure it.

Don't assume an optimisation is faster because it looks faster in TypeScript.

Input query cost is different

There's another limit worth knowing about that can easily be confused with instruction count.

Shopify Function input queries have a maximum calculated query cost of 30.

For example, Shopify currently assigns different costs to different GraphQL fields. A normal leaf field such as a product variant id or sku costs 1, while requesting a Metafield object costs 3.

This calculation happens from your GraphQL query before the Function executes.

So these are two different things:

Input query cost

Shopify builds your Function input

Your Function executes

WebAssembly instruction count

Optimising your GraphQL input and optimising your Function execution are related performance concerns, but they're not the same limit.

Measure, change, repeat

Instruction count is more useful when you treat it as a performance measurement rather than simply a limit you need to stay below.

Use a realistic input, establish a baseline, make a change and run it again.

Before: 6.8m instructions
After:  4.2m instructions

Those numbers are just an example, but that's the workflow that matters.

Shopify Functions operate in a deliberately constrained environment. You can't simply throw more CPU or memory at the problem, so the cost of the code you write becomes much more visible.

And if you've optimised the algorithm, reduced unnecessary work and you're still pushing against the instruction limit, it may be worth considering Rust.

Shopify recommends Rust for computationally intensive Functions and Functions that need to support large carts. JavaScript and TypeScript are great for developer productivity, but their runtime overhead means they can reach instruction limits sooner than equivalent Rust implementations.

That doesn't mean every Shopify Function should be written in Rust. For many Functions, TypeScript is more than enough.

But when performance really matters, sometimes the next optimisation isn't another clever loop or bitmask.

It's choosing the right language for the job.