Shopify GraphQL Bulk Operations for Large Catalog Sync: 2026 Playbook

By Milan Dhameliya · · 5 min read

Scale Shopify catalog sync beyond count/query limits with bulk operations, JSONL pipelines, throttle-safe writes, and failure recovery.

Large catalog teams break sync pipelines by relying on synchronous queries and naive count checks. At scale, the API limit model forces asynchronous extraction and controlled write batching.

Failure pattern in high-SKU operations

What usually fails first:

  1. Sync jobs depend on productsCount and treat 10000+ as a real count.
  2. Pagination loops are interrupted by throttle or timeout.
  3. Delta logic is based on stale snapshots and misses writes.
  4. Mutation batches overload query cost budget and fail mid-run.

Core architecture for stable sync

Use a four-stage flow:

  1. Bulk extract source snapshot.
  2. Build deterministic diff offline.
  3. Apply writes in bounded batches.
  4. Verify post-write state with audit queries.

Run bulk extraction instead of synchronous scans

mutation BulkCatalogSnapshot {
  bulkOperationRunQuery(
    query: """
    {
      products {
        edges {
          node {
            id
            handle
            title
            updatedAt
            variants {
              edges {
                node {
                  id
                  sku
                  price
                  compareAtPrice
                  inventoryItem {
                    id
                  }
                }
              }
            }
          }
        }
      }
    }
    """
  ) {
    bulkOperation {
      id
      status
    }
    userErrors {
      field
      message
    }
  }
}

Poll status and download URL when completed.

query CurrentBulk {
  currentBulkOperation(type: QUERY) {
    id
    status
    errorCode
    createdAt
    completedAt
    objectCount
    fileSize
    url
  }
}

Shopify supports one running bulk operation per type per shop, so orchestrate jobs accordingly.

Parse JSONL snapshot deterministically

import fs from "node:fs";
import readline from "node:readline";

type ProductNode = {
  id: string;
  handle: string;
  updatedAt: string;
};

export async function parseBulkJsonl(filePath: string) {
  const stream = fs.createReadStream(filePath, "utf8");
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });

  const products = new Map<string, ProductNode>();

  for await (const line of rl) {
    if (!line) continue;
    const row = JSON.parse(line);

    if (row.id?.startsWith("gid://shopify/Product/")) {
      products.set(row.id, {
        id: row.id,
        handle: row.handle,
        updatedAt: row.updatedAt,
      });
    }
  }

  return products;
}

Diff against your source ERP/PIM snapshot and produce explicit action lists.

Write updates in bounded mutation batches

Keep write payloads small and deterministic. If one batch fails, re-run only that chunk.

function chunkArray<T>(items: T[], size: number) {
  const out: T[][] = [];
  for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
  return out;
}

async function applyPriceUpdates(updates: Array<{ variantId: string; amount: string }>) {
  for (const batch of chunkArray(updates, 50)) {
    await adminGraphql(PRICE_UPDATE_MUTATION, { updates: batch });
    await sleep(120);
  }
}

Track userErrors and request cost metrics for every batch.

Don’t trust count-only control logic

Community reports confirm productsCount can cap at 10000+, which breaks job progress logic. Use bulk objectCount and your own processed row counters instead.

Failure recovery design

Store run checkpoints

Persist:

  1. bulk operation ID,
  2. downloaded snapshot checksum,
  3. last successful write batch index,
  4. failed entity list.

Re-run from checkpoint

Never restart full catalog sync if only batch 47/300 failed.

Technical Checklist

SEO Checklist