Je ERP stuurt 500 productwijzigingen tegelijk naar Shopify. De helft komt niet aan. De andere helft gooit een 429 error. Dit is geen edge case — dit is wat er gebeurt zonder retry strategie en kennis van het leaky bucket algoritme.
Shopify API rate limits — hoe je integraties betrouwbaar houdt
Een ERP stuurt 500 productwijzigingen tegelijk naar Shopify. De eerste 40 slagen. Dan beginnen de 429-errors. De integratie logt fouten en stopt. De helft van je assortiment heeft nu verkeerde prijzen.
Dit is geen hypothetisch scenario. Wij zien het bij elke nieuwe Shopify-integratie die zonder rate limit-strategie is gebouwd. Het leaky bucket algoritme, GraphQL cost points en retry-strategieën zijn geen optionele kennis. Ze zijn de basis van een betrouwbare integratie.
Wat je leert in dit artikel
- Hoe het leaky bucket algoritme werkt
- Wat het verschil is tussen REST en GraphQL rate limits
- Hoe je retry-logica correct implementeert
- Wanneer je bulk operations gebruikt
- Een referentie-implementatie in PHP en JavaScript
Het leaky bucket algoritme
Shopify's rate limiter werkt als een emmer met een gat. De emmer heeft een maximale inhoud. Elke request vult de emmer. Het gat loopt constant leeg.
REST API:- Emmergrootte: 40 requests
- Leksnelheid: 2 requests per seconde
- Maximale burst: 40 requests tegelijk, daarna 2 per seconde
- Emmergrootte: 1000 cost points
- Leksnelheid: 50 points per seconde
- Elke query heeft een cost op basis van de data die je opvraagt
De fout die wij het vaakst zien: developers behandelen Shopify als een API zonder limieten en sturen alles tegelijk. De emmer overstroomt, Shopify geeft 429 terug, de integratie crasht.
REST response headers lezen
Shopify vertelt je hoeveel ruimte er nog is via response headers:
HTTP/1.1 200 OK
X-Shopify-Shop-Api-Call-Limit: 32/40
Retry-After: 2.0
X-Shopify-Shop-Api-Call-Limit: 32/40— 32 van 40 slots gebruiktRetry-After— aanwezig bij 429, geeft aan hoeveel seconden je moet wachten
Lees deze headers. Niet na elke request, maar wel als je een 429 ontvangt.
GraphQL cost points begrijpen
GraphQL-queries hebben een cost die afhankelijk is van de complexiteit. Shopify berekent de cost vooraf en geeft hem terug in de response:
{
"data": { ... },
"extensions": {
"cost": {
"requestedQueryCost": 122,
"actualQueryCost": 98,
"throttleStatus": {
"maximumAvailable": 1000,
"currentlyAvailable": 902,
"restoreRate": 50
}
}
}
}
requestedQueryCost— geschatte cost voor de queryactualQueryCost— werkelijke cost na uitvoeringcurrentlyAvailable— hoeveel points er nu beschikbaar zijnrestoreRate— 50 points per seconde worden hersteld
Een eenvoudige productenquery kost ~8 points. Een query met geneste varianten, metafields en images kan 200+ points kosten. Controleer de cost van je queries in development.
Retry-logica implementeren
De meeste integraties crashen bij een 429. De correcte aanpak is exponential backoff met jitter.
Implementatie in PHP
<?php
namespace App\Services\Shopify;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class ShopifyApiClient
{
private string $shopDomain;
private string $accessToken;
private int $maxPogingen = 5;
private int $basisVertraging = 1000; // milliseconden
public function __construct(string $shopDomain, string $accessToken)
{
$this->shopDomain = $shopDomain;
$this->accessToken = $accessToken;
}
/**
* Voer een API-verzoek uit met automatische retry bij rate limiting.
*/
public function verzoek(string $methode, string $endpoint, array $data = []): array
{
$url = "https://{$this->shopDomain}/admin/api/2024-01/{$endpoint}";
for ($poging = 1; $poging <= $this->maxPogingen; $poging++) {
try {
$response = Http::withHeaders([
'X-Shopify-Access-Token' => $this->accessToken,
'Content-Type' => 'application/json',
])->{$methode}($url, $data);
if ($response->status() === 429) {
// Lees de Retry-After header
$wachtTijd = (float) ($response->header('Retry-After') ?? 2.0);
Log::warning("Shopify rate limit bereikt. Wachten {$wachtTijd}s. Poging {$poging}/{$this->maxPogingen}.");
// Wacht de aangegeven tijd + jitter
$jitter = rand(0, 500); // 0-500ms willekeurige extra wachttijd
usleep(($wachtTijd * 1000 + $jitter) * 1000);
continue;
}
$response->throw(); // Gooit exception bij 4xx/5xx (behalve 429)
return $response->json();
} catch (RequestException $e) {
// Herstelbaar? Probeer opnieuw met exponential backoff
if ($poging === $this->maxPogingen) {
throw $e;
}
$vertraging = $this->berekenVertraging($poging);
Log::warning("Shopify API fout: {$e->getMessage()}. Wachten {$vertraging}ms. Poging {$poging}/{$this->maxPogingen}.");
usleep($vertraging * 1000);
}
}
throw new \RuntimeException("Shopify API: maximale pogingen bereikt voor {$endpoint}");
}
/**
* Bereken exponential backoff met jitter.
*/
private function berekenVertraging(int $poging): int
{
// Exponential backoff: 1s, 2s, 4s, 8s...
$basisMs = $this->basisVertraging * (2 ** ($poging - 1));
// Jitter: ±25% willekeurige variatie
$jitterMs = (int) ($basisMs * 0.25 * (rand(0, 100) / 100));
return min($basisMs + $jitterMs, 30000); // Maximum 30 seconden
}
/**
* GraphQL-verzoek met cost-tracking.
*/
public function graphql(string $query, array $variabelen = []): array
{
$url = "https://{$this->shopDomain}/admin/api/2024-01/graphql.json";
for ($poging = 1; $poging <= $this->maxPogingen; $poging++) {
$response = Http::withHeaders([
'X-Shopify-Access-Token' => $this->accessToken,
'Content-Type' => 'application/json',
])->post($url, [
'query' => $query,
'variables' => $variabelen,
]);
$body = $response->json();
// Shopify geeft throttling-errors terug in de response body, niet als 429
if (isset($body['errors'])) {
foreach ($body['errors'] as $error) {
if (($error['extensions']['code'] ?? '') === 'THROTTLED') {
$wachtTijd = $this->berekenVertraging($poging) / 1000;
Log::warning("GraphQL throttled. Wachten {$wachtTijd}s. Poging {$poging}.");
sleep((int) ceil($wachtTijd));
continue 2;
}
}
throw new \RuntimeException('Shopify GraphQL fout: ' . json_encode($body['errors']));
}
// Log de resterende cost voor monitoring
$cost = $body['extensions']['cost'] ?? null;
if ($cost) {
Log::debug("GraphQL cost: {$cost['actualQueryCost']} points. Beschikbaar: {$cost['throttleStatus']['currentlyAvailable']}.");
}
return $body['data'];
}
throw new \RuntimeException('GraphQL: maximale pogingen bereikt');
}
}
Implementatie in JavaScript (Node.js)
// shopify-client.js
const MAX_POGINGEN = 5;
const BASIS_VERTRAGING_MS = 1000;
/**
* Bereken exponential backoff met jitter.
*/
function berekenVertraging(poging) {
const basisMs = BASIS_VERTRAGING_MS * Math.pow(2, poging - 1);
const jitterMs = Math.random() * basisMs * 0.25;
return Math.min(basisMs + jitterMs, 30000);
}
/**
* Wacht een bepaald aantal milliseconden.
*/
const wacht = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* Shopify REST API verzoek met retry.
*/
async function shopifyVerzoek(shopDomain, accessToken, methode, endpoint, data = null) {
const url = `https://${shopDomain}/admin/api/2024-01/${endpoint}`;
for (let poging = 1; poging <= MAX_POGINGEN; poging++) {
const opties = {
method: methode.toUpperCase(),
headers: {
'X-Shopify-Access-Token': accessToken,
'Content-Type': 'application/json',
},
};
if (data) {
opties.body = JSON.stringify(data);
}
const response = await fetch(url, opties);
if (response.status === 429) {
const retryAfter = parseFloat(response.headers.get('Retry-After') ?? '2');
const jitter = Math.random() * 500;
const wachtTijd = retryAfter * 1000 + jitter;
console.warn(`Rate limit bereikt. Wachten ${wachtTijd.toFixed(0)}ms. Poging ${poging}/${MAX_POGINGEN}.`);
await wacht(wachtTijd);
continue;
}
if (!response.ok) {
if (poging === MAX_POGINGEN) {
throw new Error(`Shopify API fout ${response.status}: ${await response.text()}`);
}
const vertraging = berekenVertraging(poging);
await wacht(vertraging);
continue;
}
return response.json();
}
throw new Error(`Shopify API: maximale pogingen bereikt voor ${endpoint}`);
}
Bulk operations: voor grote datasets
Wil je 10.000 producten bijwerken? Gebruik geen loop van 10.000 REST-calls. Gebruik de Bulk Operations API.
Bulk operations zijn asynchrone GraphQL-mutaties die Shopify op de achtergrond uitvoert. Je stuurt één grote JSONL-file, Shopify verwerkt hem en je pollt op de status.
Stap 1: Bulk mutation starten
mutation {
bulkOperationRunMutation(
mutation: """
mutation productUpdate($input: ProductInput!) {
productUpdate(input: $input) {
product { id title }
userErrors { field message }
}
}
""",
stagedUploadPath: "tmp/bulk_products.jsonl"
) {
bulkOperation {
id
status
}
userErrors {
field
message
}
}
}
Stap 2: JSONL-file samenstellen
Elke regel in de JSONL-file is één mutation-input:
{"input": {"id": "gid://shopify/Product/1", "title": "Nieuw product 1", "variants": [{"id": "gid://shopify/ProductVariant/10", "price": "29.99"}]}}
{"input": {"id": "gid://shopify/Product/2", "title": "Nieuw product 2", "variants": [{"id": "gid://shopify/ProductVariant/20", "price": "49.99"}]}}
Stap 3: Status pollen
query {
currentBulkOperation {
id
status
errorCode
completedAt
objectCount
fileSize
url
}
}
Statussen: CREATED, RUNNING, COMPLETED, FAILED, CANCELING, CANCELED.
Poll maximaal één keer per 5 seconden. Bij COMPLETED download je het resultaat van de url.
Prioriteitswachtrij voor gecombineerde integraties
Als je meerdere systemen hebt die gelijktijdig de Shopify API aanroepen (ERP + PIM + orderverwerking), verdeel de 40 REST-slots dan via een wachtrij.
// Gebruik Laravel Queues met rate limiting via middleware
// In App\Jobs\ShopifyProductUpdateJob:
public function middleware(): array
{
return [
// Maximaal 2 jobs per seconde (40 per 20 seconden = 2/s)
new RateLimited('shopify-api'),
];
}
// In AppServiceProvider::boot():
RateLimiter::for('shopify-api', function (object $job) {
return Limit::perSecond(2);
});
Monitoring instellen
Een betrouwbare integratie zonder monitoring is een toevalstreffer. Meet:
- Aantal 429-responses per uur per integratie
- Gemiddeld aantal retry-pogingen
- Bulk operation success rate
- GraphQL cost per endpoint over tijd
Een plotse stijging in 429-responses betekent dat een systeem meer requests stuurt dan gepland. Dat is eerder te detecteren met metrics dan met een boze klant die belt omdat zijn prijzen niet kloppen.
Conclusie
Shopify API rate limits zijn geen belemmering. Ze zijn een ontwerpregel waarmee je je integratie betrouwbaar maakt.
Het leaky bucket algoritme is voorspelbaar. Met retry-logica, exponential backoff en bulk operations voor grote datasets bouw je integraties die ook onder druk blijven werken.
Het leaky bucket algoritme is een veelgebruikt patroon voor rate limiting. Shopify documenteert de exacte limieten in de Shopify API rate limits documentatie.
Gerelateerde artikelen:- Shopify integraties met ERP, WMS en PIM — de complete integratiegids
- Shopify + Alumio: iPaaS integraties — middleware voor complexe koppelingen
- Laravel als e-commerce middleware — custom integraties bouwen
Meer weten? Bekijk onze Shopify-diensten of neem contact op voor een technisch gesprek over je integratiearchitectuur.

Geschreven door Ruthger Idema
15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.
Meer over ons team →