Fixes · Apps and the cart
Requests to cart.js every second that never stop
The fix
Open the browser’s Network tab, filter by cart.js, put something in the cart and leave the page alone. A quiet page sends nothing new. If reads keep coming, hover the Initiator column: it names the script that sent each one, usually an app.
When two apps set each other off, break the loop at one end. On a store we look after, a free gift app (Kite) took every Storefront API call for a cart change. Adding Kite’s own opt-out, skipListener=true, to the calls that don’t change the cart stopped it, and neither app was removed.
01Why an idle page keeps reading the cart
Shopify’s Ajax Cart API gives a theme GET /cart.js to read the cart as JSON, and endpoints to add to it and change it (Shopify, Cart API reference). It doesn’t announce a change to anyone else on the page. Shopify’s Web Pixels API does report cart events such as product_added_to_cart, but to tracking code loaded in a sandbox (Shopify, Web Pixels API), so it suits a tracking app, not one that changes what’s on the page. So an app that needs to know when the cart changes either asks again and again, or watches the requests the page sends. Both can run away.
- 01
An app that asks on a timer
In this Shopify Community thread (Feb 2022), a store owner wrote: “There are constant requests to cart.js, every second, I can’t figure out what this is about.” A reply traced it to code that runs on a timer and calls
/cart.js, from a tracking app the owner had installed. A reply in Oct 2026 says the same app’s team fixed it for them by switching to Shopify Web Pixels. - 02
Two apps that set each other off
This one is harder to see, because each app does something sensible on its own. App A reads the cart whenever it sees a request it takes for a cart change. App B sends a request every time it sees the cart read. If B’s request looks like a cart change to A, neither ever stops. Turn either app off and the page goes quiet, so each looks fine when tested alone.
The pair we found
On the store we fixed, the pair was Kite (free gifts) and Intelligems (A/B tests), and only with something in the cart. Both scripts wrap the page’s fetch, so each sees the other’s requests.
- Intelligems hears every
/cart.jsresponse. In the version on the store, when its cart metafields option is on and the cart has items, it reads/cart.jsagain and then re-checks its own cart metafield with a Storefront API query: a POST to/api/<version>/graphql.json. - Kite treats any request whose address contains
graphql.jsonas a cart change, and re-reads/cart.jsafter it. Its check is below. - That read is a
/cart.jsresponse, which Intelligems hears, and round it goes: about two cart reads and one Storefront API call a second, per open tab.
window.fetch = function(url, options) {
// Check if the URL contains "/cart"
if (url && typeof url == "string" && ((url.includes("graphql.json") && !url.includes("skipListener=true")) || url.includes('/cart/change') || url.includes('/cart/add') || url.includes('/cart/update') || url.includes('/cart/clear'))) {
return originalFetch.apply(this, arguments).then(async (response) => {
// ... callchecksAfterApiCalls(url, data), which reads /cart.js againEvery Storefront API call goes to the same graphql.json endpoint, whether it changes the cart or reads a product, so Kite can’t tell them apart by the address. The second half of that check is the way out: a URL carrying skipListener=true is ignored.
Why it matters: every open tab with something in the cart keeps hitting the store’s cart. In a thread on Shopify’s developer forum about carts failing with 429 errors (Jan to Feb 2026), Shopify staff wrote: “It’s expected for bot detection to kick in when the exact same request is sent multiple times in a short period.”
02Find the script, then break the loop at one end
For store owners
See it for yourself
In Chrome, right-click the page, choose Inspect, then open the Network tab. Type
cart.jsin the Filter box. Add a product to the cart, then leave the page alone for 20 seconds. A quiet page shows nothing new; the store we fixed showed 40 reads.Find out who sends them
Hover the Initiator column on one of the reads to see the stack trace that sent it (Chrome DevTools, Network features reference). The script addresses name the source: the theme’s own files load from the store’s
/cdn/shop/t/path, files packed into an app’s extension fromcdn.shopify.com/extensions/, and many apps from their own domain, even when an app embed adds them. In the page source, each app embed is marked with aBEGIN app blockcomment naming the app. If two apps show up taking turns, it’s a loop.Test without changing the store
Right-click one app’s script in the Network tab and choose Block request, then reload (Chrome DevTools, Request conditions). If the reads stop, you have the cause. This only affects your own browser.
Send it to the right people
If only one app sends the reads, on a steady timer, the fix is the app maker’s: send its support team a screen recording of the Network tab and the script name. If two apps are setting each other off, send both script names to your developer, with this page. Don’t remove an app the store relies on to fix this: a loop can usually be broken without it.
For developers
Read both listeners
Find what each app treats as a cart change and what it sends in reply. Log the stack for each
cart.jsandgraphql.jsonrequest from afetchwrapper of your own, then search each app’s script for the function names. Look for an opt-out already built in, like Kite’sskipListener=true.Tag the calls that don’t change the cart
Wrap
fetch, and add the opt-out to Storefront API calls whose body isn’t a cart mutation. Leave real cart changes alone, so the app still hears them. The Storefront API answered our tagged calls as before; Intelligems still got its metafield back.Be the outermost wrapper
Apps wrap
fetchwhen they start, and Kite checks the URL it’s handed. Your wrapper has to be the last one added, so it changes the URL before Kite sees it. We re-wrap whenever something has wrapped after us, for 20 seconds after load.Prefer the app’s own switch
Use a switch the vendor’s own code already honours rather than patching its script, and keep the change in one file you can remove. We found no mention of
skipListener=truein Kite’s public docs, but Kite adds it to its own Storefront API reads, with the comment “skipListener=true: these are READS, and the cart-change interceptor must not treat them as cart mutations.” Intelligems’ script also reads awindow.igSetup.suppressCartUpdateflag. It isn’t in Intelligems’ public docs either, and in the version we read it also stops Intelligems writing its own cart attributes, so we left it alone.
(function () {
'use strict';
var STOREFRONT_API = /\/api\/[^/?#]+\/graphql\.json/;
var SKIP = 'skipListener=true';
var CART_MUTATION = /\bcart(Create|Lines\w*|DiscountCodesUpdate|GiftCardCodes\w*|BuyerIdentityUpdate|AttributesUpdate|NoteUpdate|SelectedDeliveryOptionsUpdate)\b/;
function tag(url, options) {
if (typeof url !== 'string' || !STOREFRONT_API.test(url) || url.indexOf(SKIP) !== -1) return url;
var body = options && typeof options.body === 'string' ? options.body : '';
if (CART_MUTATION.test(body)) return url; // a real cart change: let Kite hear it
return url + (url.indexOf('?') === -1 ? '?' : '&') + SKIP;
}
var guard = null;
function wrap() {
if (typeof window.fetch !== 'function' || window.fetch === guard) return;
var inner = window.fetch;
guard = function (url, options) {
var args = Array.prototype.slice.call(arguments);
args[0] = tag(url, options);
return inner.apply(this, args);
};
window.fetch = guard;
}
// Kite and Intelligems wrap fetch as they start: wrap again whenever
// something has wrapped after us, for 20 seconds after load.
wrap();
document.addEventListener('DOMContentLoaded', wrap);
window.addEventListener('load', function () {
wrap();
var ticks = 0;
var timer = setInterval(function () {
wrap();
if (++ticks >= 20) clearInterval(timer);
}, 1000);
});
window.CartFetchGuard = { isOutermost: function () { return window.fetch === guard; } };
})();- Load it from
layout/theme.liquidwith<script src="{{ 'kite-fetch-guard.js' | asset_url }}" defer></script>. - Anything missing from the mutation list is hidden from Kite, so check it against the cart mutations your own scripts send.
cartMetafieldsSetis left off on purpose: that’s Intelligems writing its own tag, which isn’t a cart change Kite needs to hear (Shopify, cartMetafieldsSet). - A request sent as a
Requestobject rather than a string isn’t tagged. Kite’sfetchlistener only checks string URLs, so those don’t set it off. - The flag only covers Kite’s
fetchlistener. In the same file, its listener forXMLHttpRequesttreats everygraphql.jsonaddress as a cart change, flag or not, so this won’t help if the other app sends its calls that way. - If either app is removed, or Kite ships a new script, count the reads again: without Kite the file does nothing, and a new listener may not honour the flag.
- The same idea works for any app whose listener matches by address: tag what it shouldn’t hear, at the outermost wrapper, and nothing else.
03Count the reads again
The page goes quiet
With something in the cart, leave a home, product and collection page alone for 20 seconds each. There should be no new
cart.jsreads.Your wrapper is outermost
In the console,
window.CartFetchGuard.isOutermost()should returntrueafter load. If it’sfalseonce the page has settled, a script wrappedfetchafter the guard stopped re-wrapping, and Kite may see untagged calls again.Real cart changes still count
Add a product that takes the cart over a free gift threshold. The gift should still unlock in the cart.
The other app still works
Filter by
graphql.json: the tagged calls carryskipListener=trueand still get a 200 with their data.
From our work
04Two apps on a beauty brand’s store
On a beauty brand’s store we look after, we were sent a screenshot of the Network tab on a product page, full of repeated cart and Intelligems requests.
The cause (26 Oct 2026). With one item in the cart, an idle home page made 40 reads of /cart.js and 20 Storefront API calls in 20 seconds (measured in Chrome on the live store). With an empty cart there was no loop. The stack traces showed Intelligems’ cart read, its metafield query, and Kite’s re-read, taking turns.
The fix (26 Oct 2026). One new theme file, the guard above, loaded from the theme layout. Neither app’s settings changed, and removing the file undoes it. Before it went live, with the file loaded into the live page in our own browser, the free gift still unlocked over its threshold.
Checked after it went live (26 Oct 2026). With three items in the cart, the home page, a product page and a collection page each made 0 cart reads and 0 Intelligems calls in 20 seconds. Intelligems’ calls still returned its tag. We tested in Chrome.
- In our live log“Basket no longer stuck in a constant refresh loop between two apps”
The bigger picture
05Apps that listen to each other
Every app that watches the cart is listening to the same page as every other. Two can be fine alone and loop together, and nothing on the storefront looks wrong. If your store runs a free gift, upsell, test or tracking app on the cart, check the Network tab after each new one goes in.
Finding what each app on a store is doing, and making them work together without removing one, is what our support work covers.
06Sources
Every step and claim above comes from one of these, or from the fix on the store it describes.
- Shopify developers: Cart API reference
- Shopify developers: Storefront API, cartMetafieldsSet
- Chrome DevTools: Network features reference (Initiator, filters)
- Chrome DevTools: Request conditions, block or throttle a request
- Shopify developers: Web Pixels API
- Shopify App Store: Kite Discount, Free Gift, BOGO
- Shopify Community: XHR requests every second (Feb 2022, reply Oct 2026)
- Shopify developer community: Aggressive bot detection with 429 errors, AJAX Cart API (Jan to Feb 2026)
Sources checked on .
