Services
Store Setup UX/UI Redesign Migration Custom Development Shopify SEO Speed & Performance
Shopify Projects Corporate Websites Themes Apps
Tools
All Tools SEO Audit ROI Calculator Migration Cost Store Speed Test Theme Detector Migration Readiness Homepage CRO Review
Resources
Resource Library Blog Pricing
About Contact Free Audit ROI Calculator Migration Cost Store Speed Test
🇬🇧 EN 🇹🇷 TR 🇩🇪 DE
Shopify Tips

How Shopify Functions Actually Works: The Wasm-at-Edge Architecture Plus Merchants Are Still Sleeping On

Shopify Functions is not just a Scripts replacement, it is a new runtime layer: WebAssembly modules, deterministically executed, co-located at the edge with checkout and order processing, with hard 5ms budgets and no network access. This deep dive explains the runtime model, the sandbox guarantees, multi-function composability, production observability and the design patterns Plus merchants are landing cleanly in 2026.

Updated:
19 min read
23 views

How Shopify Functions Actually Works: The Wasm-at-Edge Architecture Plus Merchants Are Still Sleeping On

Two weeks ago our migration-oriented Functions post did surprisingly well on LinkedIn. The most-liked comment cut to the heart of it: the migration from Scripts to Functions is the obvious story, but the real story is the architecture. Shopify Functions is not just a Scripts replacement with different syntax. It is an entirely new runtime layer inside the platform — WebAssembly modules, deterministically executed, co-located at the edge with checkout and order processing, with hard latency budgets and sandbox guarantees Scripts never had.

This is the architectural follow-up to our Functions Developer Guide. The previous post was tactical — how to migrate, what the API surfaces are, what a discount looks like. This post is conceptual and aimed at senior engineers, CTOs and engineering managers at Plus stores who want to understand why the platform was built this way, how the runtime works internally, and which design patterns the model enables — and which it intentionally rules out.

No buzzwords. We will quote millisecond figures, memory limits and concrete tradeoffs. Where the model is weak, we will say so. Where Shopify made a better tradeoff than the competition, we will explain it technically.

The runtime model: a Wasm sandbox with hard determinism guarantees

Shopify Functions are WebAssembly modules executed inside a Shopify-controlled host environment. Four hard technical limits define the runtime and all four are deliberate.

  • 5 MB binary limit: The deployed Wasm module must be at most 5 MB. In practice a Rust-compiled discount function lands at 80-400 KB, a Javy-compiled JS function at 1.5-3 MB.
  • 5 ms execution budget per invocation: A hard ceiling. Anything beyond 5 ms aborts the function and the platform falls back to default behaviour (no discount, no customization). Production functions we see typically run at 0.8-2.5 ms.
  • Fuel-based instruction budget: On top of wall-clock time, Shopify uses a fuel system (similar to Wasmtime fuel). Each Wasm instruction consumes fuel. A function stuck in an infinite loop burns its fuel allocation and is terminated — even if wall-clock would have been lower. This protects against pathological inputs.
  • No I/O, no network calls, no shared state: The function has no fetch, no filesystem, no access to memory shared across invocations. Each run starts with empty memory, receives a GraphQL input snapshot and produces a GraphQL output snapshot. That is it.

These four limits are not bugs. They are the platform security and latency guarantee. If Shopify allowed 250 Plus merchants per POP to each run function logic that could issue a fetch with a three-second timeout, checkout latency would no longer be predictable. By locking every function into a deterministically bounded Wasm sandbox, the platform guarantees that no tenant can affect another tenant and that every checkout request has a computable worst-case latency profile.

Host functions and the imports/exports model

If you have written Wasm for Cloudflare Workers or Fastly Compute@Edge you know the host functions concept. The Wasm sandbox itself has no capabilities — it can compute, but it cannot observe. The host environment exports a limited set of imports into the sandbox that the function can call. Shopify exports very few: essentially shopify::function_input (returns the GraphQL snapshot) and shopify::function_output (accepts the operations as the result). No host import for network, no http_get, no storage_read. That is intentional.

// Pseudocode: host function interface (Shopify-internal)
extern "C" {
  fn shopify_function_input_read(buf: *mut u8, len: usize) -> i32;
  fn shopify_function_output_write(buf: *const u8, len: usize) -> i32;
  fn shopify_function_log(level: i32, buf: *const u8, len: usize);
}

What the function does inside Wasm linear memory — parsing, iteration, pattern matching, math — is not the platform concern. What it does outside the sandbox — only write output and emit logs — is limited to those two host calls.

Why Wasm rather than JavaScript isolates like Cloudflare Workers?

Cloudflare Workers run in V8 isolates. AWS Lambda runs in container VMs. Fastly Compute@Edge runs in Wasm via Lucet/Wasmtime. Shopify could have picked any of these. They picked Wasm and the choice is defensible.

  • Deterministic by default: Wasm has no built-in sources of non-determinism. No Math.random, no Date.now (unless explicitly provided as a host import), no filesystem time. Function runs are reproducible. The same input produces the same output. For a platform that has to be auditable for compliance, that is worth real money.
  • Sandboxed by default: Wasm modules can only do what the host environment explicitly allows. V8 isolates have to enforce sandboxing inside the JS API surface — historically a source of side-channel leaks (Spectre, Meltdown). Wasm is capability-secure.
  • Language agnostic: Rust, AssemblyScript, TinyGo, C, C++ and JS (via Javy) all compile to Wasm. Shopify does not force anyone into JS — teams with Rust DNA write Rust functions, teams that prefer JS write JS and ship via Javy.
  • Millisecond cold start: Wasm modules instantiate faster than V8 isolates and drastically faster than containers. Shopify can warm a freshly loaded function in under 5 ms — no pre-warming overhead like Lambda needs.
  • Security isolation at platform level: 250 Plus merchants can run functions on the same physical edge node without any tenant seeing another. Wasm linear memory is per-instance isolated.

The tradeoff: Wasm is harder to debug than JS because stack traces have to be read against Wasm module symbols, not against source files. Shopify mitigates this with source maps in the Functions CLI and the Function Run logs in the Partners dashboard.

Edge execution topology: where Functions actually run

Functions do not run in a centralized datacenter. They run in Shopify edge POPs — co-located with the checkout and order processing services. A cart update in Berlin hits the Frankfurt POP, the cart-transform function runs locally there in under 2 ms, and the result is written directly into the checkout state. No round trip to Toronto. No cross-region latency.

For DACH merchants this matters because Frankfurt is one of the larger EU POPs and the latency between a Berlin checkout user and the function run there is typically 8-25 ms at the first hop. In production at Plus stores we monitor we see a p50 function latency of 1.4 ms and p99 of 3.8 ms.

Why does that matter for checkout conversion? Years of research show that every 100 ms of additional checkout latency depresses conversion by roughly 0.5-1%. Running discount or delivery logic at the edge rather than in an app backend saves a real 200-600 ms per checkout page load.

The function API surfaces in 2026 — complete overview

Functions are not a single endpoint. They are multiple target APIs, each with its own GraphQL input schema and output operation set. As of 2026 GA:

  • product_discounts: discounts on individual product variants. Replaces classic line-item Scripts. Enables patterns Scripts never could — for example tag-based tier logic with cart-composition awareness.
  • order_discounts: discount on order total. Replaces order Scripts. Allows complex threshold rules (e.g. across all products in a category, conditional on customer tags).
  • shipping_discounts: discount on shipping. Free-shipping thresholds, customer-tier-based shipping rebates.
  • delivery_customization: filter, rename, reorder delivery options. Example: for orders above 50 kg, hide standard shipping and only show freight. Scripts had no comparable hook.
  • payment_customization: filter, rename, reorder payment methods. Example: for B2B customers hide Klarna and show invoice. For risk-tagged customers remove PayPal.
  • cart_transform: bundle logic, virtual variants, composite products. This API replaces an entire class of apps that used to be built with cart attributes and Liquid hacks.
  • cart_checkout_validation: validation rules at checkout. Example: allow only EU shipping addresses, block specific postal codes. Errors render directly at checkout.
  • fulfillment_constraints: controls how orders are split across locations. Example: ship everything from one warehouse if possible, otherwise multi-location split with defined priority.
  • order_routing: which location gets which order line. More complex logic than the old location-priority settings.

Each API has its own GraphQL schema, its own input types and its own output operations. Codegen via the Shopify CLI generates Rust or TypeScript types locally from the schema. That makes function development type-safe.

The input/output contract: snapshot in, operations out

This is the conceptually most important point of the entire architecture. Functions do not work on live state. They receive a snapshot of the cart (or the order, depending on the API), and they return operations — not a new cart object but a list of changes.

// product_discounts function — simplified
input FunctionInput {
  cart: Cart!
  shop: Shop!
  customer: Customer
  presentmentCurrencyRate: Decimal!
}

type FunctionResult {
  discounts: [Discount!]!
  discountApplicationStrategy: DiscountApplicationStrategy!
}

Why this design? Because it eliminates race conditions. Scripts ran synchronously in the Ruby VM but could mutate the cart during execution — leading to undefined behaviour when multiple Scripts ran. Functions see a frozen snapshot, return operations, and Shopify applies those operations to the master state in a well-defined order.

This also makes functions side-effect-free and therefore cacheable. Identical input guarantees identical output. That is the foundation of the function replay feature in the Partners dashboard — Shopify can replay any function run locally with the original input.

Composing multiple functions on one store

A Plus store can have multiple functions at once. Two discount functions, one delivery customization, one payment customization plus two cart validation functions is a normal production setup.

How is ordering determined? There are three levels.

  • API order: the platform runs cart_transform first (because it mutates cart structure), then the discount APIs (product, order, shipping), then delivery_customization and payment_customization, finally cart_checkout_validation. This order is deterministic and documented.
  • Within an API: the discount application strategy (FIRST, MAXIMUM, ALL) controls how multiple discount functions combine. FIRST picks the first matching discount, MAXIMUM the highest, ALL stacks them (use carefully).
  • Per function: inside one function, the code decides whether to return an operation or an empty result. A function that returns nothing has no effect.

In practice a clean Plus architecture in 2026 looks like this: one cart-transform function for bundles, two product-discount functions (one for loyalty tiers, one for promo codes), one order-discount function for spend thresholds, one delivery-customization for carrier logic, one payment-customization for B2B-vs-DTC splits, and one validation function for compliance rules (e.g. EU-only shipping).

State management without state: the standard patterns

Functions cannot hold persistent state between invocations. How do Plus teams in 2026 still ship complex logic that depends on configuration and external data?

  • Metafields for configuration: tier thresholds, discount percentages, carrier rules, excluded postal codes live in shop, product or customer metafields. They are delivered in the function input. Changing a metafield value affects the next function run immediately.
  • App backend for external data enrichment via webhooks: if the function needs a value that does not live in Shopify — e.g. current stock from an external ERP — an app backend periodically syncs the external data into Shopify metafields. The function only reads the metafield. No live API call needed.
  • Cart attributes for function-to-function communication: a cart_transform function can set a cart attribute that a later discount function reads. That is the only path for inter-function state sharing.
  • customer.metafield for personalization: loyalty tier, lifetime value, preferred categories are stored as customer metafields and read by the function.
  • Function settings UI via App Extensions: a function can ship with a configuration UI in the Shopify Admin. Settings are stored as metafields in the function-owner namespace.

Observability: monitoring functions in production

Functions ship with a built-in observability layer that goes beyond what Scripts had.

  • Function Run logs in the Partners dashboard: every function run logs input, output, wall-clock time, fuel consumption and any logs from the shopify.functionLog call. Searchable per function in the Partners dashboard.
  • shopify app function-runs CLI: shopify app function-runs returns the latest runs in JSON. Useful for debugging and snapshot replay.
  • Replay feature: any function run can be replayed locally with the original input — shopify app function run --input run-2026-05-15.json. By far the most important debugging workflow.
  • Performance budgets: p95 latency per function is visible in the dashboard. A function drifting toward 4 ms is a warning signal before it breaches the 5 ms ceiling.
  • Output validation: if a function returns output that does not match the GraphQL schema, the run fails and is logged. Helpful against type drift on API updates.
// example function_log output
{
  "run_id": "fn_run_01HABCDE",
  "function_id": "tier-discount-v3",
  "wall_ms": 1.42,
  "fuel_consumed": 14823,
  "input_bytes": 4192,
  "output_bytes": 312,
  "status": "success",
  "logs": ["customer tier: gold", "applied 15% discount"]
}

We recommend Plus teams in 2026 pipe the shopify app function-runs CLI output into an external observability platform like Datadog or Honeycomb. That enables p99 alerting and cross-service correlation.

Security implications of the Wasm sandbox

When a Plus merchant installs a function from a third-party app — say a discount-stacking app from an App Store partner — that third party is given code execution rights inside the checkout flow. With Scripts this was a real supply-chain risk. With Functions it is considerably less problematic because the function runs inside a hard sandbox.

  • No data exfiltration: the function cannot send cart data to a third-party server. It has no network. Even if the app developer had bad intent, they could not exfiltrate cart data.
  • No lateral movement: the function only sees the GraphQL input the API allows. No access to other customers, other orders, other shops on the same edge node.
  • Hard limits against denial of service: a function in an infinite loop burns its fuel and is terminated. It cannot hang checkout.
  • Auditable code surface: function bundles are visible in the App Store and can be reviewed by security before install. With Scripts that was practically impossible.

For Plus compliance teams this is a real improvement. With enterprise Plus customers we advise we see security sign-off time for a function-based app drop from four to six weeks (Scripts era) down to about two weeks.

Three architectural patterns Plus merchants ship cleanly in 2026

Pattern A: tiered discount stacking with cart-snapshot awareness

Problem: loyalty-tier customers get 10% off at tier 1, 15% at tier 2, 20% at tier 3. There is also a 5% promo code. Rule: the higher of tier-vs-promo applies, combined with free shipping above 80 EUR cart total.

Naive Scripts approach: an order Script with all conditions in one block. After three changes per quarter it becomes a layer of conditionals no one understands.

Functions architecture: three separate functions. Function 1 (product_discounts) applies tier discount on each line, reads tier from customer.metafield. Function 2 (order_discounts) checks promo code from cart attribute, compares with tier discount, returns the higher variant via discountApplicationStrategy MAXIMUM. Function 3 (shipping_discounts) sets shipping to zero when cart total above 80 EUR.

Why this is better: each function is independently testable, independently versioned, independently deployable. A tier-threshold change is a metafield edit. A promo-code behaviour change is a function deploy. No shared code path, no regression surface.

Pattern B: carrier-aware delivery rules for DACH logistics

Problem: DHL for packages under 30 kg, DPD between 30 and 150 kg, freight forwarding above 150 kg, express options only in Germany and Austria, no shipping to Switzerland for lithium-containing products.

Naive approach with shipping profiles: three shipping zones with three shipping profiles in the Shopify shipping settings — scales to about five product categories, then becomes unmaintainable.

Functions architecture: one delivery_customization function. Input is the cart including variant metafields (weight category, lithium flag). The function filters available shipping options based on cart composition and destination country. Result: a clean list of allowed carriers per cart.

Why better: the logic is in code, not in a UI. Code is versioned, testable, reviewable. A new rule (e.g. SwissPost exclusion for lithium) is a two-line PR rather than a ten-click config change.

Pattern C: compliance-driven payment routing for DACH

Problem: B2B business customers see invoice via Billie, B2C customers see Klarna, Swiss customers see TWINT, baskets above 1,500 EUR hide Klarna (risk compliance), customers with a risk-tagged customer.tag see no pay-later options.

Naive Scripts approach: a payment Script with five if blocks. Maintainability after three quarters: poor.

Functions architecture: one payment_customization function. Reads customer.tags, customer.metafield (risk score synced by app backend), cart.totalAmount, deliveryAddress.country. Reordering and filtering payment options is a pure function over the input. Tests are unit-testable with snapshot files.

Comparison to BigCommerce, Salesforce Commerce Cloud, commercetools

What do other commerce platforms offer in this layer?

  • BigCommerce: ships GraphQL Storefront API and webhooks but no comparable edge-Wasm layer. Custom discount logic is typically implemented in the frontend (headless) or in an app backend. Faster to start with, but latency and security isolation are not in the same range.
  • Salesforce Commerce Cloud: has OCAPI hooks and SFRA, but execution happens inside a JVM container in the Salesforce datacenter — cold-start latencies are higher and the language (SFCC ISML, JS) is less flexible than Wasm-with-many-languages.
  • commercetools: ships API Extensions and Subscriptions that trigger external functions — usually AWS Lambda. More flexible on network access, but slower (cold start 100-500 ms) and harder to audit.
  • Adobe Commerce: traditional PHP-plugin world, no comparable sandbox model.

Shopifys tradeoff is clear: hard limits (5 ms, no network) in exchange for deterministic, secure, cheap edge execution. If a merchant truly needs a live API call (e.g. external tax calculation), the app backend on the Admin API remains the right layer. Functions are not the last word for everything.

What Functions still cannot do in 2026

Honest limitations:

  • No network: no fetch, no external API call, no live data from a PIM, ERP or tax service. If the logic needs live external data, it has to happen via webhook-driven metafield sync from the app backend.
  • No checkout UI changes: functions can transform data but cannot mutate the checkout UI. Checkout UI Extensions are a separate primitive for that.
  • The 5 ms cap is hard: complex algorithms with hundredfold cart iterations or big data structures can blow the budget. We recommend profiling with the Function Runner before deploy.
  • No real-time pricing API: dynamic pricing from an external price engine (e.g. currency-hedging service) is not direct — it has to be prepared as a metafield snapshot.
  • No async operations: functions are synchronous, single-threaded, no Promise.all, no worker pool.
  • Bounded input depth: the GraphQL input is limited in depth and number of lines. Very large B2B carts with 500+ lines can trigger edge cases.

Where Shopify is heading: 2026 and beyond

Three directions we see in the Function roadmap.

  • More API surfaces: fulfillment_constraints and order_routing are maturing, new APIs for subscription pricing and bundle constraints are in discussion. We expect 2-3 more target APIs in 2026/2027.
  • Convergence with Hydrogen and OXP: Functions are already integrated with the Hydrogen storefront world (discount logic from Functions is respected in the Hydrogen cart). We expect tighter tooling for joint debugging of Hydrogen plus Functions. See our Berlin Tech Startup Shopify Guide for the Hydrogen context.
  • SHOP App integration: functions that run in checkout will plausibly affect the SHOP App cart as well — cross-surface consistency will become a theme.
  • Better observability tools: external observability integration (Datadog, Honeycomb) via official connectors rather than only via CLI pipes.

FAQ for senior engineers

Should we still write Scripts in 2026?

No. Shopify Scripts were fully shut down in August 2025. If you still have Scripts in production today, the migration is overdue. See our Functions Developer Guide for the tactical path.

Can Functions replace our middleware?

Not entirely. Functions replace the logic that has to run at checkout or on the cart. Back-office logic (order sync to ERP, inventory enrichment, external tax calculation) stays in the app backend. Rule of thumb: anything synchronous, deterministic and checkout-relevant belongs in Functions, anything async, external or data-heavy stays in the app backend.

How do we test Functions in CI?

Shopify CLI has shopify app function run --input fixture.json. We recommend per function a folder with 5-15 input fixtures (edge cases, empty cart, very large cart, international customers) and snapshot tests against output JSON. The CI job runs on every PR.

What does Function hosting cost?

Functions are included in Shopify Plus with no separate pricing. There is no per-invocation pricing as with Lambda. That is a significant advantage — a function that runs 50 million times per month costs nothing extra.

Do Functions count toward the 25-app limit on Plus?

Functions are app extensions. One app can ship multiple Functions. The limit applies at app level, not Function level. An app with twelve Functions counts as one app.

Can we mix Rust and AssemblyScript in the same Function?

Inside a single Function: no. A Function is one Wasm module compiled from one source language. But you can have multiple Functions in the same app, each in a different language. We see teams writing perf-critical Functions in Rust and simple configuration Functions in JS.

How do Functions interact with Checkout UI Extensions?

They are separate primitives. Functions transform cart data, UI Extensions render UI in checkout. A UI Extension can set cart attributes that a Function reads — that is the main communication path.

Are there cases where a private app server with Admin API is still better?

Yes, three: (1) when the logic needs a live API call to an external system (tax, inventory, currency hedge), (2) when the computation needs significantly more than 5 ms (complex routing optimization), (3) when the operation is not synchronous-checkout-relevant (order sync, notifications, re-indexing).

What does a Function repo layout typically look like in 2026?

A Shopify app in a monorepo, in extensions/ a subfolder per Function with shopify.extension.toml, src/run.ts (or src/main.rs), schema.graphql and tests/fixtures/*.json. Codegen via shopify app generate schema. Deploy via shopify app deploy.

How do we debug a Function that sporadically fails in production?

Function Run logging in the Partners dashboard allows filtering by status=failed. The corresponding input snapshots can be downloaded and replayed locally with shopify app function run --input failed-run.json. Fastest path to repro.

Conclusion and next steps

Shopify Functions is not simply a better version of Scripts. It is an architectural change — deterministically bounded, sandboxed, edge-co-located and language-agnostic. The 5 ms limit is hard, the absence of network is a feature choice not a limitation. Plus merchants who accept these constraints get back a platform layer in which discount, shipping, payment and cart logic run as fast, safely and auditably as storefront CDN cache hits.

The patterns we have described — tiered discount stacking, carrier-aware delivery, compliance payment routing — are real and run in production at DACH and international Plus merchants in 2026. What Functions cannot do (live external calls, UI changes, long computations) belongs in adjacent layers: Checkout UI Extensions for UI, app backend on the Admin API for external integration, webhook sync for pre-computation.

If you are rethinking your Plus architecture or your Scripts migration is not yet final — we help with architecture reviews, function-patterns workshops and concrete Plus migrations. Talk to us via Contact or read what we build in Custom Development. For Berlin teams combining Hydrogen with Functions our Shopify Agency Berlin page is a good starting point.

Further reading: the tactical Functions Developer Guide, the Berlin Tech Startup Playbook with Hydrogen context, and our Migration Services for stores moving off Scripts or off other platforms.

34Devs is based in Korschenbroich, 20 minutes from Duesseldorf, and has been working on Shopify Plus architecture since 2022 — from first Functions MVPs through multi-function composability setups with Hydrogen storefronts. Architecture questions welcome.

Share this article
Back to Blog

Related Posts

34Devs Chat Assistant
34Devs Chat Assistant
34Devs Assistant
Online
Hey! What would you like to improve on your website?
Need a human? Just ask.