Shopify GraphQL Admin API : 8 patterns pour ne pas se faire throttler
Comment ne pas exploser ton rate limit Shopify quand tu pousses 50 K produits. Bulk operations, cost predictor, batch chunks, retry exponentiel. Patterns que j'utilise en prod.
Shopify GraphQL Admin API tourne sur un système de “calculated query cost”. Tu n’as pas un rate limit en req/s, mais un budget en points. Tu en consommes selon la complexité de ta requête, pas selon le volume.
Si tu pousses 50 000 produits sans connaître les patterns, tu te fais throttler en 30 secondes et tu attends 4h pour reprendre. Voici les 8 patterns pour ne pas y passer.
1. Comprendre le système de cost
Shopify alloue 2 000 points au bucket de ton app, qui se recharge à 100 points/seconde. Chaque query a un coût calculé :
- Field simple (id, title) : 1 point
- Connection (products, orders) :
count * cost_per_node - Mutation : 10 points minimum + cost des champs retournés
{
products(first: 50) { # ← 50 nodes × cost = 50 minimum
edges {
node {
id # 1 pt
title # 1 pt
variants(first: 10) { # 10 nodes × cost = 10 pts par produit = 500 pts
edges {
node { id sku }
}
}
}
}
}
}Cette query coûte environ 600 points. Tu peux la lancer 3 fois avant de saturer.
2. Toujours interroger extensions.cost
Toute réponse GraphQL inclut le cost réel :
{
"data": { ... },
"extensions": {
"cost": {
"requestedQueryCost": 600,
"actualQueryCost": 587,
"throttleStatus": {
"maximumAvailable": 2000,
"currentlyAvailable": 1413,
"restoreRate": 100
}
}
}
}Avant de lancer la query suivante, vérifie currentlyAvailable. Si < 200, attends que le bucket se recharge.
3. Pattern : token bucket côté client
class ShopifyRateLimiter {
available = 2000;
restoreRate = 100;
lastUpdate = Date.now();
async wait(needed: number) {
this.refresh();
while (this.available < needed) {
const wait = Math.ceil((needed - this.available) / this.restoreRate * 1000);
await sleep(wait);
this.refresh();
}
this.available -= needed;
}
refresh() {
const elapsed = (Date.now() - this.lastUpdate) / 1000;
this.available = Math.min(2000, this.available + elapsed * this.restoreRate);
this.lastUpdate = Date.now();
}
update(cost: number, current: number) {
this.available = current;
this.lastUpdate = Date.now();
}
}Tu attends d’avoir le budget avant de lancer. Tu mets à jour avec le currentlyAvailable retourné par Shopify.
4. Pattern : pagination par cursor (jamais offset)
query Products($cursor: String) {
products(first: 250, after: $cursor) {
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
node { id title }
}
}
}Tu paginates page par page, en passant le endCursor à la requête suivante. Jamais de last + before sauf cas exceptionnel (lourd en cost).
5. Pattern : Bulk Operations (le secret pour > 10 000 items)
Pour > 10 000 produits, n’utilise pas la pagination classique. Use Bulk Operations :
mutation {
bulkOperationRunQuery(
query: """
{
products {
edges {
node {
id
title
variants {
edges {
node { id sku price inventoryQuantity }
}
}
}
}
}
}
"""
) {
bulkOperation { id status }
userErrors { field message }
}
}Shopify exécute la query en background, génère un fichier JSONL. Tu polles le statut avec currentBulkOperation. Quand status: COMPLETED, tu télécharges le fichier (jusqu’à plusieurs Go).
Coût : 10 points fixe pour lancer le bulk, peu importe le volume retourné. C’est imbattable pour les gros exports.
Limite : 1 bulk operation simultanée par store.
6. Pattern : Bulk Mutations pour les imports (Plus only)
Pour pousser 50 K produits, on utilise bulkOperationRunMutation (Shopify Plus uniquement) :
- Générer un fichier JSONL avec 1 mutation par ligne
- Upload sur Shopify via
stagedUploadsCreate - Lancer
bulkOperationRunMutationavec l’URL du fichier - Poller le résultat
mutation {
bulkOperationRunMutation(
mutation: "mutation call($input: ProductInput!) { productCreate(input: $input) { product { id } } }",
stagedUploadPath: "tmp/12345/abc.jsonl"
) {
bulkOperation { id }
userErrors { field message }
}
}50 000 produits passent en 30-60 minutes vs 8-12 heures en API REST classique.
7. Pattern : retry exponentiel sur 429 et 5xx
Tu vas en prendre. Toujours retry avec backoff :
async function withRetry<T>(fn: () => Promise<T>, max = 5): Promise<T> {
let lastErr;
for (let i = 0; i < max; i++) {
try {
return await fn();
} catch (err: any) {
lastErr = err;
if (err.status === 429 || err.status >= 500) {
const wait = Math.pow(2, i) * 1000 + Math.random() * 500;
await sleep(wait);
continue;
}
throw err; // erreurs 4xx hors 429 → ne pas retry
}
}
throw lastErr;
}Backoff exponentiel : 1s, 2s, 4s, 8s, 16s. Plus du jitter pour éviter les retry simultanés.
8. Pattern : split queries trop chères en plusieurs requêtes
Si une query coûte > 1000 points, split-la. Plutôt que products(first: 250) { variants(first: 100) } (cost > 25 000), tu fais :
- 1 query :
products(first: 250) { id }(cost ~250) - 250 queries :
product(id: $id) { variants(first: 100) }en parallèle limité à 10 (cost 100 each)
Total : 250 + 250×100 = 25 250 points sur une fenêtre de temps plus longue. Tu prends 25 min mais tu ne te fais jamais throttler.
9. Bonus : le cost predictor
Avant de lancer une query inconnue, demande son cost via costPredict (en alpha) ou estimate-le avec extensions.cost.requestedQueryCost sur dryRun. Tu sais combien ça va coûter avant de payer.
La toolchain qui marche
J’utilise une stack simple en TypeScript :
@shopify/shopify-apipour l’auth + base GraphQL clientp-limitpour la concurrence (max 5 requêtes simultanées)p-retrypour les retry- Mon propre token bucket
- Zod pour valider les responses (Shopify retourne parfois des structures inattendues sur les anciennes APIs)
Code typique pour un push de 50K produits :
const limiter = pLimit(5);
const tokenBucket = new ShopifyRateLimiter();
await Promise.all(products.map(p => limiter(async () => {
await tokenBucket.wait(50); // estimation cost mutation
const res = await withRetry(() => shopify.graphql(MUTATION, { input: p }));
tokenBucket.update(res.extensions.cost.actualQueryCost,
res.extensions.cost.throttleStatus.currentlyAvailable);
})));Les pièges que j’ai vus
- Pas de validation de la response : tu push 10 K produits, tu reçois 10 K success, mais 30 % des SKU ont été silencieusement renommés par Shopify. Toujours validate.
- Webhook qui hammer ton listener : un webhook
inventory_levels/updatepeut firer 50 fois/sec sur un drop. Buffer-le côté client (n8n queue ou simple Redis stream). - Mutations sans
userErrorschecké : siuserErrorsest non-vide, la mutation a échoué malgré un HTTP 200. Toujours vérifier.
Le quota Plus
En Shopify Plus, tu passes à 10 000 points bucket + 500/s restore rate (5× le standard). C’est suffisant pour pratiquement tout. Si tu satures encore, c’est que ton archi a un problème pas le rate limit.
Audit
Si tu pousses des données en gros volume vers Shopify et que ça plante (retry loops infinis, throttling, données silencieusement perdues), l’audit gratuit que je fais inclut un test de charge sur ta toolchain actuelle + recommandations chiffrées sous 72 h.
Bâtissons votre architecture ensemble.
Audit gratuit sous 24 h ouvrées. Devis chiffré sous 72 h. Aucun engagement derrière.
Réserver un audit