SapotaCorp

Shopify Functions: Rust, JavaScript, or not a Function at all

Shopify Functions let you customize backend logic like discounts and checkout validation, running on Shopify's own infrastructure. The two questions teams get wrong are which language to write them in and, more importantly, whether the thing they want to build belongs in a Function at all. The constraints decide both, and they are easy to discover too late.

Shopify Functions: Rust, JavaScript, or not a Function at all

Key takeaways

  • Shopify Functions run your custom backend logic on Shopify's infrastructure at fixed extension points like discounts, checkout validation, and delivery customization. They are fast and have no external latency, but they are sandboxed and constrained, which is what determines what you can build.
  • The hardest constraint is that a Function cannot make network calls. It only sees the data it explicitly queries from Shopify, so any logic that needs external data has to precompute that data into metafields the Function can read, or live outside a Function entirely.
  • Rust versus JavaScript is mostly a constraints decision, not a preference. Rust produces smaller, faster WebAssembly that stays within the size and execution limits more comfortably, so complex or performance-critical Functions favour it; JavaScript is faster to write and fine for simpler logic.
  • The first question is not which language but whether it should be a Function at all. If the logic needs live external data or network access, a Function is the wrong tool, and an app or a precompute step is the right one.

A merchant asked us to build what sounded like a simple discount: give a customer a percentage off based on their lifetime spend, pulled from the CRM. The team reached for Shopify Functions, because discounts are exactly what Functions are for, started writing it, and hit a wall that is invisible until you are standing in front of it. A Function cannot call the CRM. It cannot call anything. It runs in a sandbox with only the data it queries from Shopify, and "the customer's lifetime spend from an external system" is not data Shopify has. The discount logic was fine. The architecture was wrong, and the wall was not in any of the getting-started material.

This is the pattern with Shopify Functions. They are a genuinely good way to customize backend behaviour, but they come with constraints that decide both what you can build and how you should build it, and teams tend to discover those constraints after they have committed to an approach rather than before. There are really two decisions here, and most people only think about the second one. The first is whether the thing belongs in a Function at all. The second is Rust versus JavaScript. Getting the first one wrong is the expensive mistake.

What a Function actually is, and what it costs you

Shopify Functions let you inject custom logic into specific points in Shopify's backend, the places where decisions get made: how a discount is calculated, whether a cart passes checkout validation, which payment methods show, how delivery options are customized, how a cart is transformed. Your code compiles to WebAssembly and runs on Shopify's own infrastructure at those extension points, which is what makes them fast and free of the latency you would get from calling out to your own server mid-checkout. For the things they are designed for, that is a strong model: the logic runs where the decision happens, instantly, without a round trip.

The cost of running in Shopify's sandbox is a set of constraints you do not get to negotiate. A Function only sees the data it explicitly queries from Shopify through its input query, so if a piece of information is not in Shopify, the Function cannot reach it. There are execution limits on how much work a Function can do per invocation, and a size limit on the compiled WebAssembly module. And the constraint that catches the most people is that a Function cannot make network calls at all. It is deliberately deterministic and isolated, which is exactly why it is fast and safe to run on Shopify's side, and exactly why the CRM-driven discount could not work as written.

None of these are bugs. They are the trade you make for code that runs server-side at the decision point with no latency. But they mean the design question comes before the coding question.

The first decision: should this even be a Function?

Before choosing a language, decide whether a Function is the right tool, and the test is mostly about data. A Function is the right tool when all the data the logic needs is already in Shopify or can be put there ahead of time, and the logic is one of the supported extension points. A tiered discount based on cart contents, checkout validation against order rules, delivery customization by zone, these live entirely within data Shopify already has, and they belong in a Function.

A Function is the wrong tool the moment the logic needs live external data or a network call. The CRM-driven discount is the clean example: the deciding input lives in another system, and a Function cannot fetch it. There are two honest ways out of that, and both are about getting the data to the right side of the wall. The first is to precompute the external data into Shopify ahead of time, syncing the customer's lifetime-spend tier into a customer metafield on a schedule or via webhook, so that by the time the Function runs, the value it needs is already sitting in Shopify where the Function can query it. That is usually the right answer, and it leans on the same metafield modelling you would use for any custom data. The second is to do the logic outside a Function entirely, in an app with its own backend, when the decision genuinely cannot be reduced to precomputed data. Choosing between those is the same kind of call as deciding whether you need a custom app at all.

The mistake is skipping this decision, starting to write the Function, and discovering halfway in that the data is unreachable. Five minutes spent asking "where does every input to this logic live?" saves the rewrite.

The second decision: Rust or JavaScript

Once you have decided it genuinely is a Function, the language choice is less about taste than people expect, because it mostly comes down to those size and execution limits. Both Rust and JavaScript compile to WebAssembly and both are supported, so the question is which one keeps you comfortably inside the constraints for the logic you are writing.

Rust produces smaller and faster WebAssembly, which means it sits more comfortably within the module size limit and the execution limits, and it is the safer choice for Functions whose logic is complex or performance-sensitive or likely to grow. The cost is that Rust is harder to write if your team does not already know it, so there is a real ramp. JavaScript is far quicker to get going with, especially for a team that already lives in JavaScript, and it is perfectly fine for simpler Functions where the logic is modest. Its risk is that the compiled module is larger, so a JavaScript Function with genuinely complex logic can bump into the size limit in a way the equivalent Rust Function would not, and discovering that late means a rewrite in the other language.

The practical rule we use is to reach for JavaScript when the logic is simple and the team is JavaScript-native, and to reach for Rust when the Function is complex, performance-critical, or expected to grow, because that is where the limits start to bite and Rust's smaller, faster output buys headroom. If there is real doubt about whether the logic will fit, Rust is the lower-risk default, because hitting a wall in Rust is rarer than hitting one in JavaScript.

How to approach a Function from the start

The order that avoids the expensive surprises is to do the data check first and the language choice second. List every input the logic needs and confirm each one is either already in Shopify or can be precomputed into it; if any input requires a live external call, stop, because this is not a Function, it is a precompute step or an app. Once you know it is genuinely a Function, size up the logic honestly: simple and your team knows JavaScript, write it in JavaScript; complex, performance-critical, or growing, write it in Rust for the headroom. Then build it knowing the limits are fixed, so the logic has to fit the sandbox rather than the sandbox bending to the logic.

The reason to work in that order is that the language is the cheap decision to change and the architecture is the expensive one. A team that picks the language first and the architecture never is the team that ends up rewriting a half-built Function once it discovers the data it needs was on the wrong side of a wall the whole time. Decide what belongs in a Function before you decide what to write it in, and the rest follows cleanly.

Engineering certifications

Sapota engineers hold credentials on Shopify. Each badge links to the individual engineer's credly profile.

Browse Shopify certs

Need this on your team?

Sapota engineers ship the patterns you read here. Two-week paid trial, direct pricing from $1,800/ engineer/month, no agency markup.

Get a quote
Contact Us Now

Share Your Story

We build trust by delivering what we promise – the first time and every time!

We'd love to hear your vision. Our IT experts will reach out to you during business hours to discuss making it happen.

WHY CHOOSE US

"Collaborate, Elevate, Celebrate where Associates - Create Project Excellence"

SapotaCorp beyond the IT industry standard, we are

  • Certificated
  • Assured quality
  • Extra maintenance

Tell us about your project