Shopify POS UI Extension: `shopify.cart.addLineItem` Performance Bottleneck

Hi @Victor_Chu, @Rune-Shopify & Shopify team,

I hope it’s ok to start a new thread about this issue. I still want to keep my other thread going to investigate cart transform issues, but I wanted to open a new issue related to shopify.cart.addLineItem(...), which both @Gunes and @derrick have both reported on.

We have been investigating add-to-cart performance in a Shopify POS UI extension and have found that shopify.cart.addLineItem(...) appears to be a major baseline bottleneck. We would appreciate guidance from Shopify on whether this latency is expected, and whether the POS cart APIs can be optimized for faster item creation.

Context

We have a POS UI extension for a cafe ordering workflow. The extension lets staff configure a product with modifiers, then adds the configured item to the Shopify POS cart with line item properties and cart properties.

The relevant APIs we tested are:


shopify.cart.addLineItem(variantId, quantity)

shopify.cart.addLineItemProperties(uuid, lineProperties)

shopify.cart.addCartProperties(cartProperties)

shopify.cart.bulkCartUpdate(cartUpdateInput)

Our original goal was to improve add-to-cart speed by moving from multiple sequential cart calls to bulkCartUpdate. After testing, we found that bulkCartUpdate is not faster for simple add-with-properties flows, but more importantly, the initial addLineItem(...) call itself consistently takes multiple seconds.

Test Environment

  • Surface: Shopify POS UI Extension

  • POS device: iPad running Shopify POS

  • Extension API package: @shopify/ui-extensions 2026.1.3

  • App type: Shopify app with embedded POS extension

  • Store/network/device kept consistent during the test runs

  • Timing captured inside the extension using performance.now()

  • Logs posted to a Gadget endpoint and streamed with ggt logs

Tested Add-To-Cart Strategies

We tested three strategies for adding a configured product with line item properties and cart properties.

1. Legacy Sequential


const uuid = await shopify.cart.addLineItem(variantId, quantity);

await shopify.cart.addLineItemProperties(uuid, lineProperties);

await shopify.cart.addCartProperties(cartProperties);

2. Bulk Cart Update


const uuid = await shopify.cart.addLineItem(variantId, quantity);

await waitForCartLineItem(uuid);

await shopify.cart.bulkCartUpdate({

lineItems,

properties: cartProperties,

cartDiscounts,

});

3. Parallel Property Calls


const uuid = await shopify.cart.addLineItem(variantId, quantity);

await waitForCartLineItem(uuid);

await Promise.all([

shopify.cart.addLineItemProperties(uuid, lineProperties),

shopify.cart.addCartProperties(cartProperties),

]);

Timing Results

Each strategy was tested repeatedly on the same device/store/network.


=== legacySequential ===

samples: 5

p50: 3049ms

p90: 3893ms

max: 3893ms

mean: 3244ms

=== bulkCartUpdate ===

samples: 5

p50: 4934ms

p90: 5728ms

max: 5728ms

mean: 4878.2ms

=== parallelProperties ===

samples: 4

p50: 2766ms

p90: 3431ms

max: 3431ms

mean: 2963.8ms

The fastest approach for our simple add-with-properties case was parallelProperties, but the total time was still usually around 2.7-3.4 seconds.

addLineItem Is a Major Baseline Cost

In a representative bulkCartUpdate run, the timing breakdown was:


addLineItemMs: 2623

waitForCartLineItemMs: 0

bulkCartUpdateMs: 2742

totalBlockingMs: 5367

strategy: bulk

This suggests that shopify.cart.addLineItem(...) alone took about 2.6 seconds.

The waitForCartLineItemMs value was 0ms in that run, which suggests the delay was not from our polling/wait helper. Instead, addLineItem(...) did not resolve until the POS cart line was already created and visible in shopify.cart.current.value.

So even after optimizing the property attachment step, the initial POS line creation remains a significant unavoidable delay.

Additional Observations

We found that bulkCartUpdate is useful for some existing-line mutation cases, but it is not a good fit for our simple add-with-properties flow.

For simple new-item adds:

  • bulkCartUpdate was slower than dedicated property APIs.

  • Resending the full cart payload is expensive.

  • Using bulkCartUpdate for duplicate quantity increments caused a serious issue where cart line prices changed to $0.00 on-device. We avoided this by no longer using bulkCartUpdate for duplicate adds.

Our current fastest safe path is:


const uuid = await shopify.cart.addLineItem(variantId, quantity);

await Promise.all([

shopify.cart.addLineItemProperties(uuid, lineProperties),

shopify.cart.addCartProperties(cartProperties),

]);

Even with that optimized path, addLineItem(...) itself remains a multi-second bottleneck.

Questions For Shopify

  1. Is a 2-3 second duration for shopify.cart.addLineItem(...) expected in Shopify POS UI extensions?

  2. Are there known performance limitations in POS cart line creation that extension developers should account for?

  3. Is Shopify planning any optimization work for addLineItem(...) latency?

  4. Is there a faster supported API for adding a variant line with initial line item properties in one operation?

  5. Could addLineItem(...) accept optional initial line item properties/cart properties in the future, so POS does not require separate calls after UUID creation?

  6. Are there recommended profiling steps or diagnostic data Shopify would like us to capture to help investigate?

Ideal API Shape

For POS ordering workflows, the ideal API would allow a configured line to be created with properties in one call:


await shopify.cart.addLineItem({

variantId,

quantity,

properties: lineProperties,

cartProperties,

});

This would avoid requiring developers to:

  • wait for a UUID before attaching properties

  • issue separate line-property/cart-property calls

  • use bulkCartUpdate as a workaround

  • risk full-cart rewrite side effects

Request

Could the Shopify POS team please investigate the performance of shopify.cart.addLineItem(...) in POS UI extensions, especially for workflows where staff need to add configured products quickly during checkout?

For cafe/restaurant POS use cases, an add-to-cart operation taking 2-3+ seconds creates a noticeable delay for staff during order entry. Any improvement to the baseline addLineItem(...) latency, or a new API for creating a line with initial properties, would make a significant difference.

Happy to provide more logs, timing breakdowns, or reproduction details if helpful.

Hey Adam,

Thanks for the writeup. The timings, the three strategies side by side, and the $0.00 repro on duplicate adds give us a lot to work with.

I’ll follow up here once we have a concrete read on root cause, what’s fixable short term, and whether the right longer-term answer needs an API change. No ETA yet, but we’ll keep you posted.

Victor

Hey @Victor_Chu , thank you very, very much for your time and help with this, it is very much appreciated :folded_hands:t5:

I noticed this as well. When adding a line item before closing my modal there was a significant delay. As a workaround I’ve handed that off to Tile, so the Modal closes quickly and the addLineItem runs whilst that is happening, so by the time the user returns to the cart it’s done - but without any noticeable delay.

Obviously a faster insert would be great, but this is not a bad workaround.

As @adamwooding says, being able to insert a line item and add a property in a single call would be very useful. That said, ideally this should not be implemented under the hood as an insert followed by the properties since that generates 2 cart updates which can fire off a lot of additional functions.

What would be great is a transaction. ie. something like this:

startCartTransaction()
insertLineItem()
addCart Property()
removeCart Property()
commitTransaction()

The end result being all changes are applied before a single cartUpdate event is generated. This could avoid a lot of churn due to subscribers processing partial changes.

Hey @Ian_Bale and @Victor_Chu,

Ian, thank you for your reply and the suggestion about handing the cart mutation off to the Tile. I think that is a really good workaround for improving perceived performance, especially when the delay is only a couple of seconds.

In our case, though, we have had a large number of merchants report that the addLineItem process can take up to 10-15 seconds. In those situations, even handing the work off to the Tile could still cause issues, because the cart mutation is still running for a long time in the background and we need to manage pending state, duplicate taps, failures, and cart reconciliation.

We have tried to find a common thread among those affected merchants, and so far it seems to be either older iPad hardware, weaker internet/Wi-Fi, or both. That makes me think there is an underlying issue with the current addLineItem flow, especially because extension developers have to wait for the POS-issued UUID before we can safely attach required line item properties.

For configured-product workflows, the current flow can require several separate cart mutations:

  1. Add the base product line with addLineItem.
  2. Shopify POS creates, validates, prices, and materializes that line before returning the POS-issued UUID.
  3. Use that UUID to attach the line item properties that describe the configured item, creating another cart update.
  4. Add/update cart properties, creating another cart update.
  5. Optionally apply discounts, creating another cart update.

In our app, we also use Cart Transform. The Cart Transform logic depends on the line item properties added after addLineItem, so the meaningful transform work likely happens after the property mutation rather than during the initial bare line insert.

That means the current API flow can create an intermediate cart state where the product line exists, but the metadata required to interpret it has not been attached yet. If Cart Transform functions, subscribers, or other cart update processing run after each mutation, a single configured item can cause multiple rounds of cart processing before the final cart state is ready.

When step 1 takes several seconds, everything else is blocked behind it.

@Ian_Bale - I also really like your transaction suggestion. I think that would be the ideal API shape, rather than Shopify simply adding a convenience wrapper that internally performs multiple cart mutations. Something like:


const transaction = await shopify.cart.startTransaction();

transaction.addLineItem({

variantId,

quantity,

properties: lineProperties,

});

transaction.addCartProperties(cartProperties);

await transaction.commit();

The important part would be that Shopify POS applies all changes as one atomic cart operation and emits one final cart update event, rather than exposing intermediate states or firing multiple cart updates/functions/subscribers.

That would help with a few things:

  • Avoid waiting for a UUID before attaching required properties.
  • Reduce partial cart states.
  • Avoid multiple cart update events for a single configured item.
  • Reduce cart transform / subscriber churn.
  • Improve reliability on older devices or weaker networks.
  • Give extension developers a safer way to create fully configured cart lines.

So I think there are two related asks here:

  1. Improve the baseline performance of shopify.cart.addLineItem(...), especially on older iPads or slower networks.
  2. Consider a transaction/batched cart API so extensions can create a configured cart line with properties and cart attributes in one atomic operation.

Happy to provide timing logs or reproduction details if useful.

Thank you so much,
Adam

Hi everyone,

Adding some more info here from my own testing (originally posted here).

We have been using the bulkCartUpdate method for some time now along with a cart transform and whilst it does provide a more consistent delay, it still adds a decent amount of time to the operation. Which in some contexts is undesirable, some merchants are not tolerant of this amount of delay.

I did some testing some time ago which I provided in an issue on the Shopify/ui-extensions repo, looks like issues have since been disabled so I can’t reference it anymore but here are the results I had:

Numbers for the bulkCartUpdate I got today:

Screenshot 2026-05-13 at 08.47.24

It seems to take around 2.5s, these were with a varying number of line items and a cart transform.

addLineItem (also from today) appears to take a similar amount of time:

Screenshot 2026-05-13 at 09.00.29

Screenshot 2026-05-13 at 09.00.29710×220 11.3 KB

Ignore the 5s+ one that one was out of stock :slightly_smiling_face:

If nothing has changed since my initial tests over a year ago then it looks like the cart transform adds another second to the latency.

It would be nice to be able to kick the bulkCartUpdate or addLineItem off from within an extension and have it queued (FIFO) to mutate the cart asynchronously rather than have us wait for the return result.

That way we could carry on doing whatever it is we want to do, e.g. dismiss the extension or navigate elsewhere, and be assured that the cart update was happening in the background. The cart update mutation could be indicated to the user via a loading spinner or skeleton text?

Would also be nice if you could add line item properties at the same time as adding a line item via addLineItem :slightly_smiling_face:

Hey @Victor_Chu ,

I just wanted to check in to see if you’ve had a chance to have a look at this issue?

Thank you so much,
Adam

Hi,

I’d like to point out another point in this discussion.

In my case, I created a POS app to customize a product using the lineItem properties. While testing this feature, I noticed that if I try to add the same variant of the product to the cart more than once, the cart merges it with the existing lineItem and returns its ID.
So, using addCartProperties, I’m overwriting the properties of the old one, making a mess.

We should be able to add a product to the cart with certain properties atomically to avoid this behavior.

Thanks!
Francesco

I have a problem with stampcard redemption.

Customer buys 3 coffees for £2.99 each.
But redeems a full stampcard to get one FREE.

So I apply a “FixedDiscount” to the coffee line item of £2.99.

POS promptly changes this to £3. It takes the £2.99, divides that by the quantity (3) and gets £0.996666 per item. It then rounds that to the nearest £0.01 and gets £1.00 per item. It then multiples that back by up 3 to get £3.00 discount. Depending upon the numbers it can also offer too little discount - so customer is charged 1p for their FREE coffee.

If I then add an additional coffee, so I now have 4, it decides that the new discount is £4 ( 4 * it’s calculated £1 each). So now my customer is getting their FREE coffee plus £1.01 extra discount!

If I could split the discounted coffees out into a separate line item then I could apply a 100% discount to that one, and no discount to the other one.

But, as you pointed out, POS combines the new line item before you can add properties in order to differentiate them…

Hi,

I wanted to thank @adamwooding for his work on this issue and for all the details he provides in his question and answers. We are one of the merchants that are experiencing the 10-15 second delay in the addLineItem process.

@Victor_Chu - We are eager to hear any news about this issue and look forward to any updates from the Shopify team. The delay can be crippling for our cashiers on busy days so any improvements are greatly appreciated.

Thank you,

Luke Erlandson

Hi @tod97 and @Ian_Bale,

Thanks for your comments on this thread.

The focus here has mostly been the performance bottleneck with shopify.cart.addLineItem, but on your specific question - separating line items that share a variant - yes, this is doable today using line item properties to keep the lines distinct. There are two approaches.

One important framing point first: this works because POS merges line items that have the same variant and identical properties - so differing properties are what keep two lines apart. The thing to get right is ordering: the property write on the first line has to finish before you add the variant again. If both adds happen before any property is attached, POS sees two identical bare lines and merges them into one. Awaiting each step in turn is all it takes to avoid that.

Method 1: Sequential adding (await each step)

shopify.cart.addLineItem returns a Promise<string> that resolves with the UUID of the new line item - so you don’t have to guess it. The key is to await each step: the property write on the first line must finish before the second add runs.

// 1. Add the base variant - the promise resolves with the new line item's UUID.
//    (Resolves to '' if an oversell guard modal was dismissed; throws on a real error.)
const uuidA = await shopify.cart.addLineItem(55443322, 1);

// 2. Attach a property to that UUID.
//    This MUST complete before the next add, otherwise POS merges the two
//    bare line items into a single line with quantity 2.
await shopify.cart.addLineItemProperties(uuidA, { Monogram: 'JS' });

// 3. Add the same variant again - it now stays separate because its
//    properties differ from line A.
const uuidB = await shopify.cart.addLineItem(55443322, 1);

Resulting state

Line Item UUID Variant ID Qty Properties
uuid-A 55443322 1 Monogram: JS
uuid-B 55443322 1 None

Note: if you later add { Monogram: 'JS' } to uuid-B, POS will merge the two lines back into a single line of quantity 2, since the variant and properties would then match.

Method 2: Splitting an existing line via bulkCartUpdate

If you already have a line item with quantity > 1 and need to peel one unit off, you can rebuild the line item array with shopify.cart.bulkCartUpdate.

Two things to be careful of:

  1. bulkCartUpdate takes a CartUpdateInput, not a full Cart - don’t just spread the current cart into it. Construct the input explicitly with the fields you intend to set.
  2. The lineItems array completely replaces the cart’s contents. Include every line, and be aware that rebuilding lines this way can drop existing line-item discounts.
  // Read the current cart synchronously from the signal.
  const currentCart = shopify.cart.current.value;

  const targetUuid = 'uuid-original-A'; // the line we want to split

  const newLineItems = currentCart.lineItems.reduce((list, row) => {
    if (row.uuid === targetUuid) {
      return [
        ...list,
        // Keep the original line, decremented by 1.
        { ...row, quantity: row.quantity - 1 },
        // The split-off unit with its new property.
        // Omit `uuid` — let POS assign one for the new line.
        { variantId: row.variantId, quantity: 1, properties: { Monogram: 'JS' } },
      ];
    }
    return [...list, row];
  }, []);

  // Pass an explicit CartUpdateInput — only the fields you want to set.
  const updatedCart = await shopify.cart.bulkCartUpdate({
    lineItems: newLineItems,
    note: currentCart.note,
    customer: currentCart.customer,
  });

Final cart result

Line Item UUID Variant ID Qty Properties
uuid-original-A 55443322 2 None
(POS-assigned) 55443322 1 Monogram: JS

For the stampcard rounding issue specifically

@Ian_Bale - I think that the rounding bug has one root cause: when a FixedAmount discount sits on a line with quantity > 1, POS divides the discount by the quantity, rounds per unit, then multiplies back. That’s where your £2.99 becomes £3.00.

I was thinking that perhaps another approach for you, could be to make sure the free coffee is the only thing on its line - a quantity-1 line. On a quantity-1 line there’s nothing to divide, so the rounding never happens. A Percentage 100% discount is cleanest (100% of £2.99 = £2.99 exactly, and it stays correct if the coffee price ever changes), though a £2.99 FixedAmount works equally well once the line is quantity 1.

The catch is that 4 coffees of the same variant added with no properties are merged into a single line of quantity 4 - so “pick one” actually means splitting one unit off. A line item property does exactly that, and does double duty: it forces the split and stops the free line from re-merging into the paid one.

  const cart = shopify.cart.current.value;
  const targetUuid = '<uuid of the coffee line>';

  // Split one unit off the coffee line onto its own quantity-1 line.
  const newLineItems = cart.lineItems.reduce((list, row) => {
    if (row.uuid === targetUuid && row.quantity > 1) {
      return [
        ...list,
        // the paid coffees stay together
        { ...row, quantity: row.quantity - 1 },
        // the free one — the property both splits it off and prevents a re-merge
        { variantId: row.variantId, quantity: 1, properties: { Stampcard: 'Redeemed' } },
      ];
    }
    return [...list, row];
  }, []);

  const updatedCart = await shopify.cart.bulkCartUpdate({
    lineItems: newLineItems,
    note: cart.note,
    customer: cart.customer,
  });

  // Find the freshly split line by its property and zero it out.
  const freeLine = updatedCart.lineItems.find(
    (li) => li.properties?.Stampcard === 'Redeemed',
  );

  await shopify.cart.setLineItemDiscount(
    freeLine.uuid,
    'Percentage',
    'Stampcard reward',
    '100',
  );

This also fixes your second bug - adding a 5th coffee now merges into the paid line, not the discounted one, so the reward can’t creep up to £4. (If you’d rather have this happen automatically server-side, or need identical behaviour across online + POS, a Cart Transform LineExpand function can do the split for you - but for a staff-driven redemption at the till, the above should be all you need.)

The underlying issue

Both methods work, but they multiply the number of cart round-trips - an addLineItem, then an addLineItemProperties, then another addLineItem, each one a separate await. And that’s the real pain point we keep hearing from merchants: in a busy cafe, adding items to the cart and applying line item properties is slow. Each call has noticeable latency, and stacking three or four of them per drink adds up to a sluggish experience at exactly the moment the queue is longest.

So while property-based splitting is the right tool for differentiating line items, the thing that genuinely needs to improve is the raw speed of addLineItem and addLineItemProperties. Until those individual calls are faster, any workflow built on top of them inherits that lag - and for high-throughput retail like cafes, that’s the bottleneck that matters most. Hopefully @Victor_Chu may have some updates for us soon :slight_smile:

Hope that’s helpful. Thanks so much,
Adam

Thanks @adamwooding

I had tried setting the property of a line item before adding an additional one. I guess my timing must have been off because it did not work. I’ll give it another try. However, using split rows does add another complication - I now have two rows the cashier can interact with and change quantities. I’d still need to be wary of quantity being changed in the discounted row without there being full stamp cards remaining that can be redeemed.

However I found another workaround. Rather than use FixedAmount, I’ve calculated a Percentage. This avoids the conversion and rounding and seems to generate the correct discount. It’s fairly trivial to adjust the percentage if quantities change.

I do think the “FixedAmount” is rather misleading, and given the rounding problem, is clearly not what could be reasonably expected. A true, “fixed amount” would certainly be useful and would reduce cart change churn since no further changes would need to be applied when quantities changed. On a related note, it would be helpful if line items with percentage discounts had a property of the percentage amount rather than just the calculated discount!

The bulk update looks worth investigating. I have done my best to limit cart updates by merging property / discount updates into as few mutations as possible. This has certainly helped, but applying a line item property, a cart property, and cart discount still results in 3 cart change events.

I presume the bulk update would result in just one change event? So whilst your figures suggest time to process might be slightly higher than the combined sequential updates I am doing, the reduced churn from cast updates that need to be processed will be reduced.

You said: “be aware that rebuilding lines this way can drop existing line-item discounts.” Can you re-apply the discounts in the bulk update?

Hey @Ian_Bale ,

I just replied on your other thread here.

If it’s ok, I’d love to keep this thread really focussed on the shopify.cart.addLineItem performance bottleneck, as it’s a major, major issue for our merchants.

Thanks so much,
Adam :slight_smile:

Hey @Victor_Chu ,

How are you? I’m so sorry to bother you - I just wanted to check in to see if you had any updates on this issue?

We are continuing to have merchants reach out about this and I just wanted to give them an update if possible.

Thank you so much Victor,
Adam

Hey @adamwooding

We’ve fixed the issue where using bulkCartUpdate for duplicate quantity increments could cause cart line prices to show as $0.00 on-device. We’re now avoiding bulkCartUpdate for duplicate adds, which prevents that from happening. This will be in POS 11.9.

The broader cart speed improvements will take a little longer, but the next improvement we’re working on is adding the ability to include cart properties in the same call as adding a line item. This will be apart of the 2026-07 ui extensions release.

Hope that helps give merchants a clearer update for now. Thanks!

Adding properties in the addLineItem will be a great improvement! Thanks @Victor_Chu

I’ve come across a new lineItemBug this morning - wondering whether other work on addLineItem has caused it? It is definitely new - wasn’t there when I was last working on POS (27th May) but is there today.

Essentially the bug is that addLineItem can return a different UUID to the to assigned to the added item! So adding a lineItem, then adding properties using the returned UUID results in the properties being added to the wrong item.

It is easily replicated on both Android and iOS by manually adding an item (by tapping a tile) at roughly the same time as the App is adding a line item in the background.

I’ve reported it as a bug in the POS app already. But figured it was worth mentioning it here too.

Hey @Victor_Chu ,

Thank you so much for your incredible work on the addLineItem method on the Cart API in the 2026-07 release. The new AddLineItemOptions is very, very helpful.

Just a question Victor - can you please let me know the minimum Shopify POS app version that includes the new AddLineItem changes?

Thank you Victor,
Adam

@adamwooding

Hey Adam,

Thanks for your patience. The new addLineItem changes will be available in Shopify POS version 11.11, which is expected to roll out in the coming weeks.

Victor