Critical Performance Degradation: Cart Transform Latency (8s+) on POS during Peak Hours for High-Volume Merchants

Issue Summary

Our POS extension experiences severe performance degradation during peak hours. Add-to-cart operations that normally take 1-2 seconds increase to 5-8 seconds during busy periods. Force-closing and restarting the Shopify POS app temporarily restores normal performance.

Performance Comparison

  • Off-peak: 1-2 seconds per add-to-cart
  • Peak hours: 5-8 seconds per add-to-cart (and sometimes up to 15 seconds)
  • After POS app restart: Returns to 1-2 seconds (then degrades again)

Supporting Evidence

Please see the following screen recordings demonstrating:

Normal performance

(1-2s, 3 API calls completing quickly) during off-peak hours

Degraded performance

(5-8s, each API call taking longer) during peak hours - these are from two different merchants:

Example 01:

Example 02:


Current Implementation: Sequential API Calls

We currently use the standard sequential approach for adding items to the cart:

// Step 1: Add the line item
const uuid = await shopify.cart.addLineItem(
  Number(variantId),
  quantity
);

// Step 2: Add line item properties
await shopify.cart.addLineItemProperties(
  uuid,
  lineProperties // includes _modifierVariants, _order_type, etc.
);

// Step 3: Update cart properties
await shopify.cart.addCartProperties(mergedCartProperties);

Current Approach Characteristics

  • 3 sequential API calls per add-to-cart operation
  • 2-3 Cart Transform executions (triggered after each cart mutation)
  • Race condition potential between calls (properties added after initial line item)
  • Each API call must complete before the next begins

Optimized Cart Transform Function

Our Rust Cart Transform function is optimized with:

  • Early exits to skip unnecessary processing
  • Pre-allocated vectors with capacity hints
  • Cached attribute lookups to avoid repeated HashMap access
  • Minimal string allocations using Cow<str>
// Early exit if no processing needed
fn should_process(cart: &input::InputCart) -> bool {
    let referrer = cart.referrer.as_ref().and_then(|a| a.value.as_ref());
    referrer.is_some()
}

// Parse modifiers with early returns
fn parse_modifier_variants(line: &input::InputCartLines) -> Vec<ModifierVariant> {
    let value = match line.modifier_variants
        .as_ref()
        .and_then(|attr| attr.value.as_ref()) {
        Some(v) => v,
        None => return vec![],
    };
    
    if value.is_empty() {
        return vec![];
    }
    
    serde_json::from_str::<Vec<ModifierVariant>>(value).unwrap_or_default()
}

Key Observations

  1. Time-based pattern: Fast in morning, degrades throughout the day during peak hours
  2. Affects all locations and devices (multiple iPads at different sites)
  3. App restart fixes it temporarily: Suggests memory/state accumulation in POS app
  4. General Shopify POS sluggishness: Users report overall Shopify POS slowness during same periods (not just our extension)
  5. Same code, different performance: Identical code runs fast off-peak, slow during peak
  6. Multiple Transform executions: Each of our 3 API calls may trigger a Cart Transform, compounding latency

Performance Breakdown

Off-Peak (Normal)

  • Total: 1-2 seconds
  • addLineItem: ~400-600ms
  • addLineItemProperties: ~400-600ms
  • addCartProperties: ~200-400ms
  • Cart Transform (2-3x): Unknown execution time

Peak Hours (Degraded)

  • Total: 5-8 seconds
  • Each API call appears significantly slower
  • Cart Transform executions may also be slower
  • Compounding effect with sequential calls

Business Impact

  • Peak hours = 60-70% of daily revenue
  • Staff must wait 5-8 seconds per item during lunch/dinner rush
  • Each add-to-cart requires 3 sequential API calls + multiple Cart Transform executions
  • Reduced order throughput during critical business periods
  • Staff resorting to app restarts multiple times per shift
  • Customer satisfaction impacted by slow service

What We Need

Immediate Questions

  1. Is this a platform-level performance issue during peak load?
  2. Is this a POS app memory/state management issue?

Guidance Requested

  • Recommendations for improving performance with our current approach
  • Best practices for Cart API usage during high-load scenarios
  • Whether atomic cart updates would be more resilient during peak periods

Context: Why We Use This Approach

We currently use the sequential approach because:

  1. It’s the documented standard pattern for adding line items with properties
  2. We need to attach custom properties (including _modifierVariants JSON for Cart Transform)

We’re open to refactoring if a different approach would solve the peak-hour performance issue.


Technical Details

  • Platform: Shopify POS (iOS/iPad)
  • Extension Type: POS UI Extension (Preact) + Cart Transform Function (Rust)
  • API Version: 2025.10.x
  • Cart Transform: Expands line items based on _modifierVariants property
  • Current Pattern: addLineItem → addLineItemProperties → addCartProperties

thanks @adamwooding for the detailed post

i can also +1 for our extensions we have received reports from our baristas:

  • long waiting times for simple fetchProductWithId (or just outright failing)
  • long waiting time for addLineItem and/or applyCartDiscount, setCustomer

Hey folks - thanks for flagging, we’re looking into this now.

Hi @adamwooding & @Gunes

Is this performance issue something you’ve only recently started experiencing, or has it been observed for a while? Have you considered using the bulkCartUpdate API?

hey @Liam-Shopify, my calls are not chained, just single calls.

the first report from the baristas came in at Jan 16th, but the initial report was it happens 1-2 times per hour where the product details do not load, so they’d have to close the app and open it again.

but again, i don’t have any chained calls.

Hey @Liam-Shopify, thank you for your time and help with this.

With regards to your questions:

Timeline: Past 3-4 weeks have been significantly worse (similar to @Gunes’s Jan 16th timeline), though we’ve actually had intermittent issues for several months. We have unfortunately lost some merchants to other platforms due to the intermittent slow issues.

bulkCartUpdate: Yes, we’ve already implemented it! Single atomic call with client-generated UUIDs. Works great off-peak (1-2s), but still degrades to 5-8s during peak hours.

I think that since @Gunes is also seeing issues with single, non-chained calls (fetchProductWithId, etc.), could suggest that this might be a platform-wide performance issue rather than something specific to our call patterns.

Thank you very much for your help and support @Liam-Shopify :folded_hands:t5:

Thanks for the detailed breakdown including the Shopify Functions code — that was very helpful for our investigation.

What we found: The primary issue is the sequential API call pattern. Each call to addLineItem, addLineItemProperties, and addCartProperties triggers a separate server round-trip that includes cart transform execution. So for 5 items with 3 calls each, that’s 15 sequential round-trips — each re-running your cart transform on the full cart. That’s where the 5-8s+ is most likely coming from.

Could you try the following and report back? Use bulkCartUpdate to consolidate everything into a single call. It supports line item properties and cart-level properties:

await cart.bulkCartUpdate({
  lineItems: [
    { variantId: 11223344, quantity: 1, properties: { size: "large", milk: "oat" } },
    { variantId: 55667788, quantity: 2, properties: { size: "medium", milk: "whole" } },
  ],
  properties: { order_source: "mobile_kiosk", barista_station: "2" },
});

This should eliminate the redundant round-trips during cart building. The trade-off is that server-computed values (taxes, transform results) won’t be reflected until checkout, but for property-based workflows that should be fine.

If you need to update properties on items already in the cart, bulkAddLineItemProperties batches those into a single operation as well.

We’d like to know:

  • What latency you see with this approach (both off-peak and peak)
  • Whether the “degradation over time” behavior persists
  • Any blockers that prevent you from using this pattern

This will help us determine what platform-level improvements are still needed.

Hey @Victor_Chu ,

Thank you very much for taking the time to investigate this issue and provide this feedback and code suggestions. We have implemented your recommendations and we are just waiting a couple of days to investigate the impact of the changes, and then we will provide feedback here.

Thank you again Victor, your help is very much appreciated :folded_hands:t5:

Hi @Victor_Chu,

Thanks for the guidance on bulkCartUpdate – we’ve implemented your recommendations and have some results to share. Unfortunately, we’re still experiencing significant delays for some merchants, so we’d like to walk through what we’ve done and where we think the remaining bottleneck is.


What We Implemented

Following your recommendation, we migrated our add-to-cart flow from 3 sequential calls to a single atomic bulkCartUpdate. Here’s our current implementation:


// Build new line item with all properties included in a single call

const newLineItem = {

variantId: Number(selectedMainProduct),

quantity: state.quantity,

properties: lineProperties, // all properties included atomically

};

// SINGLE API CALL - one round-trip, one Cart Transform execution

await shopify.cart.bulkCartUpdate({

lineItems: [...(freshCart?.lineItems || []), newLineItem],

properties: mergedCartProperties,

cartDiscounts: freshCart?.cartDiscounts || [],

});

Line item properties and cart-level properties are all included in the single bulkCartUpdate call – exactly as you recommended. We reduced from 3 sequential API calls (and 2-3 Cart Transform executions) down to 1 API call and 1 Cart Transform execution.


Results

Off-Peak Performance

The atomic approach is working ok for some merchants during off-peak hours. We’re seeing 1-2 second add-to-cart times for some merchants.

Peak-Hour / Slow-Connection Performance

We’re still receiving reports from merchants of 5-10+ second add-to-cart times during peak hours. Some merchants are experiencing this consistently throughout the day.

New Observation: Network Connection Impact

We were able to replicate the slow performance when connected to a slower internet access point. On our high-speed connection, the same code performs at 1-2 seconds. On a slower connection, it jumps to 5-10+ seconds.

This suggests the issue could be influenced by network latency, which makes sense given that each bulkCartUpdate call still involves a full server round-trip including Cart Transform execution. On a slow or congested connection (common in busy cafes with many devices on one network), that single round-trip becomes the bottleneck.


Screen Recordings

The following screen recordings were sent to us from two separate merchants:

Slow Performance (peak hours / slower connection) – Example 1

Slow Performance (peak hours / slower connection) – Example 2

Slow Performance Reproduced on Slow WiFi

This screen recording is one that we recorded recently when we happened to be on a slower wifi network:


Summary of Where We Are

API calls per add-to-cart

  • Previous Implementation: 3 sequential
  • New implementation: 1 atomic

Cart Transform executions

  • Previous Implementation: 2-3 per add-to-cart
  • New implementation: 1 per add-to-cart

Off-peak performance

  • Previous Implementation: 1-2s
  • New implementation: 1-2s

Peak-hour / slow-connection performance

  • Previous Implementation: 5-8s
  • New implementation: Still 5-10s for some merchants

“Degradation over time” behavior

  • Previous Implementation: Yes
  • New implementation: Still present

The atomic approach eliminated the compounding effect of multiple sequential round-trips, but the underlying single round-trip is still very slow under certain conditions.


Technical Details

  • Platform: Shopify POS (iOS/iPad)

  • Extension Type: POS UI Extension (Preact) + Cart Transform Function (Rust)

  • API Version: 2025.10.x

  • Pattern: Single bulkCartUpdate with inline line item properties and cart-level properties

  • Cart Transform: Expands line items based on _modifierVariants JSON property

  • Locations affected: Multiple locations, multiple devices

Thanks again for your help investigating this. We’ve implemented everything you recommended and would appreciate any further guidance on reducing the latency of the single bulkCartUpdate round-trip, especially under slower network conditions.

@adamwooding are most of your merchants in Australia? In our experience these types of POS server latency issues are worse for non-North America based merchants. Could be a detail worth mentioning here.

Hey @derrick ,

Thank you for your reply. Most of our merchants are actually based in the US & Canada. It is a good point that you raise though - some of the merchants experiencing this issue are based outside the US, for example: Germany, Australia, etc. But some are also US based too.

Thank you very much for taking the time to reply,
Adam

Got it - that’s good to know. We unfortunately have also recently been experiencing our own POS cart issues which I believe stem from the same root cause as what you’re reporting: Shopify POS - intermittent "Not found" error in POS cart for newly-created items using addLineItem

Hey @derrick , I’m so sorry to hear that you’re experiencing issues, but it’s also reassuring to me that we’re not the only ones! Hopefully the Shopify POS dev team can help us resolve these issues :folded_hands:t5:

Hey @Rune-Shopify , I’m sorry to tag you directly but I’m wondering if anyone on the team is investigating this issue or if you have any updates? Thank you so much :folded_hands:t5:

Hey @Victor_Chu , I’m wondering if you have had a chance to take another look at this? We are continuing to get complaints about the add to cart process being very slow, and losing merchants to other platforms (such as Square). Any help would be very much appreciated :folded_hands:t5:

@adamwooding Thanks for your patience.

Have you tested your POS UI extension without the Cart Transform function? Is it still slow? I just want to eliminate that factor before diving more into whats happening on the UI extension side.

Victor

Hi @Victor_Chu - thanks for your patience.

Before testing without the cart transform, we set up controlled instrumentation around the atomic bulkCartUpdate call to measure its round-trip latency precisely. In doing so we discovered something significant that I think changes the picture, and I want to share it before we report any performance numbers from the cart-transform-off test.

tl;dr

shopify.cart.bulkCartUpdate has been throwing GuardError: Invalid prop at position 0 on every single call in our extension since we migrated to the atomic pattern, on two consecutive API versions (2025-10 and 2026-01). Our previous reply was wrong - the cart was being updated, but only because our fallback catch block was silently dropping back to the legacy 3-call sequential path (addLineItem → addLineItemProperties → addCartProperties). The 1–2s and 5–10s numbers we shared with you were therefore measured on the legacy pattern, not the atomic one. We were unknowingly still on the original implementation the entire time.

This explains a lot of what we previously reported:

  • Why “migrating to atomic” didn’t reduce peak-hour latency - there was no migration in practice.
  • Why the “degradation over time” behaviour persisted - the underlying mechanism never changed.
  • Why slow-network performance was so bad - every add is still 3 sequential round-trips.

What we tested

Four progressively-simpler payload shapes, on api_version = "2025-10". All fail with the identical error:

# Payload Result
1 Full payload as in our previous reply: { lineItems: [...existing, newItem], properties, cartDiscounts } GuardError: Invalid prop at position 0
2 Exactly the shape from your example: { lineItems: [{ variantId, quantity, properties }], properties } Same error
3 Bare minimum to add a new item: { lineItems: [{ variantId, quantity }] } (no extras of any kind) Same error
4 No-op round-trip of existing item: { lineItems: [{ uuid, variantId, quantity }] } Same error

Variant 4 is the strongest signal - it’s literally “send the cart state back unchanged”, which should be the simplest possible bulk operation. Cart had one line item with a valid uuid + numeric variantId + positive integer quantity. Even that fails.

We then bumped to api_version = "2026-01" and tested variant 4 again

To rule out a 2025-10-specific bug, we upgraded @shopify/ui-extensions to 2026.1.3 and api_version to 2026-01, redeployed, force-quit and reopened POS on the test device, and confirmed the new bundle was loaded (custom version banner on the Settings screen). We then ran variant 4 (the no-op probe) again.

Identical failure, same errorName, same errorMessage, same payload shape:

{
  "build": "A",
  "op": "atomic_failed",
  "ts": 1777363595752,
  "errorName": "GuardError",
  "errorMessage": "Invalid prop at position 0 for cart.bulkCartUpdate",
  "payloadShape": {
    "payloadVariant": "noop_existing_items_only",
    "payloadTopLevelKeys": ["lineItems"],
    "lineItemsLength": 1,
    "firstExistingLineItemKeys": ["uuid", "variantId", "quantity"],
    "firstExistingLineItemTypes": {
      "uuid": "string",
      "variantId": "number",
      "quantity": "number"
    },
    "firstExistingVariantIdValid": true,
    "firstExistingQuantityValid": true,
    "cartDiscountsLength": 0,
    "propertiesKeyCount": 0
  }
}

So the problem isn’t quarter-specific. Either two consecutive API versions ship the same bug, or the runtime guard against bulkCartUpdate is rejecting on something we genuinely cannot inspect from JS.

Diagnostic - what the payload looks like at the moment of failure

Variant 4 (api_version = "2026-01"):

  • Top-level keys sent: ["lineItems"] only — no properties, no cartDiscounts, no customer, no note
  • lineItems.length === 1
  • The single line item has exactly uuid, variantId, quantity
  • typeof uuid === "string"
  • Number.isFinite(variantId) && variantId > 0
  • Number.isFinite(quantity) && quantity > 0
  • uuid was read from shopify.cart.current.value.lineItems[0].uuid immediately before the call (so it is a uuid POS itself just gave us)
  • variantId and quantity were also read from that same lineItems[0]

Nothing was hand-constructed. It’s the cart we just received from the platform, sent back verbatim with output-only fields stripped.

Our environment

  • API versions tested: 2025-10 and 2026-01 (same failure on both)
  • @shopify/ui-extensions: 2025.10.x and 2026.1.3 respectively
  • Extension target: pos.home.modal.render
  • Cart API docs at /2025-10/target-apis/contextual-apis/cart-api and /2026-01/…/cart-api both list bulkCartUpdate(cartState: CartUpdateInput) => Promise<Cart> as required
  • Device: Shopify POS iOS app on iPad, current public release
  • Tested across multiple iPads, multiple stores

What we’d like to know

  1. Can you reproduce await shopify.cart.bulkCartUpdate({ lineItems: [{ variantId: <any_valid_variant_id>, quantity: 1 }] }) from a pos.home.modal.render extension on either 2025-10 or 2026-01? If it works in your environment, what version difference (POS app build, internal SDK build, store-level flag, account flag) might explain the divergence?
  2. Is there a different payload shape we should be sending that the published LineItem[] / CartUpdateInput types don’t reflect? The runtime guard appears stricter than the documented type, on both versions.
  3. The error message “Invalid prop at position 0” gives us no information about which prop or which position. Is there any way to surface the actual rejection reason from the runtime guard, even just for our development store? Even a “did you mean…?” hint would unblock us.

What we still plan to do

We’re going to continue with the test you originally asked about - running our extension with the cart transform unregistered - but on the legacy 3-call path, since that’s what’s actually executing in production. We’ll measure the A/B/C differential (cart transform on / no-op / unregistered) on add-to-cart latency under both fast and throttled wifi, n=20 reps each cell. Will share results once collected.

But the more pressing question is whether bulkCartUpdate can be made to work for us at all on a supported API version. If we can fix that, the peak-hour problem may resolve as a downstream effect, since we’d genuinely be down to 1 round-trip + 1 cart-transform invocation per add for the first time.

Happy to share full [PERF_TEST] log entries (we have them piped to a backend route via fetch so we have them on Windows too), the relevant slice of CustomiseScreen.tsx, the redacted payload diagnostic at any of the four failure variants, or a minimal reproduction extension if useful. Just let us know what would help.

Thanks again,
@adamwooding

@adamwooding I don’t believe we are seeing any guard error, but we are on 2025-07 and bulkCartUpdate works for us with the following type of payload. Not sure if the below helps?

            const newVariant = {
              discounts: [],
              isGiftCard: false,
              quantity: 1,
              taxable: false,
              taxLines: [],
              properties: {},
              variantId: variantId,
              uuid: generateNewUUID()
            }
            const updatedCartData = {
              ...cart.current,
              'lineItems': [
                ...cart.current.lineItems,
                newVariant
              ]
            }

           api.cart.bulkCartUpdate(updatedCartData)

Thanks @derrick for sharing the spread-everything payload - it gave us a concrete shape to test against, so we built a brand-new minimal POS UI extension to isolate bulkCartUpdate from any of our app’s complexity and run five payload variants side-by-side, including yours and the one from the docs.

The new evidence is stronger than expected, and it changes our understanding of the bug. Posting it here so others on this thread (and @Victor_Chu ) has everything in one place.

Setup

  • Brand-new extension in the same Shopify app, single target pos.home.modal.render, api_version = "2026-01", @shopify/ui-extensions@2026.1.3
  • One Preact modal with five <s-button> elements - each calls shopify.cart.bulkCartUpdate with a different payload, no helpers, no normalization, no shared state
  • Each call wrapped in try { await ... } catch (e) { ... }, timed with performance.now(), payload shape captured before the await so the diagnostic survives a rejection
  • Same dev store, same iPad, same physical environment as our production extension
  • Test variant: simple Default Title product, inventory_management: null, no oversell guard

Results

# Variant Payload Result
A @Victor_Chu 's documentation example { lineItems: [{ variantId, quantity, properties }], properties } GuardError: Invalid prop at position 0
B Spread-everything (your shape, @derrick ) { ...cart.current, lineItems: [...existing, fullNewItem] } - 10 top-level keys, 8-key line item with all required fields and a generated uuid No error. ~75ms. But the cart did not actually update.
C Bare minimum { lineItems: [{ variantId, quantity }] } GuardError: Invalid prop at position 0
D No-op existing item { lineItems: [{ uuid, variantId, quantity }] }, where uuid is read directly from cart.current.value.lineItems[0] GuardError: Invalid prop at position 0
E Empty {} GuardError: Invalid prop at position 0

Two distinct failure modes:

  • Hard reject with GuardError: Invalid prop at position 0 (A, C, D, E)
  • Silent accept and drop - call resolves cleanly with no error and no cart update (B)

Across all five shapes, we have not found one that both passes the guard and actually persists.

The two findings worth highlighting

1. Victor’s own documentation example fails. Variant A is the exact shape from the Cart API docs and from your earlier reply in this thread, @Victor_Chu. Hard GuardError on this device, on this API version. The published CartUpdateInput type accepts the partial shape, but the runtime guard does not.

{
  "errorName": "GuardError",
  "errorMessage": "Invalid prop at position 0 for cart.bulkCartUpdate",
  "payloadShape": {
    "payloadTopLevelKeys": ["lineItems", "properties"],
    "lineItemsLength": 1,
    "firstLineItemKeys": ["variantId", "quantity", "properties"],
    "firstLineItemTypes": { "variantId": "number", "quantity": "number", "properties": "object" }
  }
}

2. The “working” spread-everything payload silently does not persist. Variant B returns from await bulkCartUpdate(...) in 73-80ms with no error thrown. But cart.current.value.lineItems.length is unchanged before vs after, and the cart visibly stays empty on the iPad. Confirmed multiple times, both via diagnostic readout and visual inspection.

{
  "success": true,
  "ms": 73,
  "cartLineCountBefore": 0,
  "cartLineCountAfter": 0,
  "payloadShape": {
    "payloadTopLevelKeys": [
      "lineItems", "cartDiscount", "cartDiscounts", "subtotal",
      "taxTotal", "note", "grandTotal", "customer", "properties", "editable"
    ],
    "lineItemsLength": 1,
    "firstLineItemKeys": [
      "discounts", "isGiftCard", "quantity", "taxable",
      "taxLines", "properties", "variantId", "uuid"
    ]
  }
}

The uuid we generate for the new line item is a fresh UUID (via crypto.randomUUID()); we suspect the platform may silently reject line items with UUIDs it didn’t issue, but the spec doesn’t say either way.

What we’d love help with

  1. ** @derrick ** - the visual cart check is the key one. After a successful bulkCartUpdate on 2025-07, does the new line item visibly appear in the cart on the device, and does cart.current.value.lineItems.length increment? We were also seeing 73ms “successes” on this shape until we looked at the cart afterwards.
  2. @Victor_Chu - can your team reproduce variant A on a recent POS iOS build, against any dev store, on 2025-10, 2026-01, or 2026-04? If it works in your environment but fails in ours, we need to find the diff.
  3. The error message - “Invalid prop at position 0” gives nothing actionable. Is there any developer mode, store-level flag, or POS build that surfaces which prop, what the guard expected, or why validation failed? Even a one-line additional clue would unblock us in minutes.
  4. Variant B silent failure - is bulkCartUpdate documented to silently drop a new line item if some downstream validation step fails after the synchronous guard passes? Where would we look in platform logs to see why?

In the meantime

Our production extension stays on the legacy 3-call path (addLineItem → addLineItemProperties → addCartProperties), which is what’s actually been running all along. We’re stripping out the dead try { bulkCartUpdate } catch { legacy } so we stop wasting ~50-150ms per add on a doomed atomic call.

Happy to share the full repro extension as a gist - it’s roughly 400 lines of Preact + TypeScript total.

Thanks again,
@adamwooding

@adamwooding yes, this type of payload for bulkCartUpdate works in production for our apps and does add the product to the cart. Not sure what you might be missing there.