Shopify uses the leaky bucket algorithm to perform rate limiting on their (Admin) GraphQL API. Yet they do not provide a standard for retrying failed API requests because of these rate limits. They do provide all necessary information to implement rate limiting in their GraphQL responses. Their GraphQL API implements rate limiting with an algorithm called the leaky bucket.
A quick refresher about the leaky bucket algorithm can be found on Shopify’s explainer page about their rate limiting on the Admin GraphQL API: https://shopify.dev/concepts/about-apis/rate-limits#graphql-admin-api-rate-limits.
The rate limit information is stored in the response as an extension
. The cost extension to be precise. We can act upon this information using an ErrorLink
from the @apollo/client
package.
Given a standard ApolloClient
setup to query your Shopify Admin API:
// client.js
import {
ApolloClient,
HttpLink,
InMemoryCache,
} from "@apollo/client";
export const createClient = (shop, accessToken) => {
const http = new HttpLink({
uri: `https://${shop}/admin/api/2021-04/graphql.json`,
});
return new ApolloClient({
cache: new InMemoryCache(),
link: http,
});
};
We can implement an ErrorLink
like this:
import { onError } from "@apollo/client/link/error";
function isThrottledError(error) {
return error.extensions.code === "THROTTLED";
}
const rateLimit = onError(
({ graphQLErrors, networkError, forward, operation, response }) => {
if (networkError) {
// A non 429 connection error.
// Fallback to ApolloClient's own error handler.
return;
}
if (!graphQLErrors) {
// An error we cannot respond to with rate limit handling. We require a specific error extension.
// Fallback to ApolloClient's own error handler.
return;
}
if (!graphQLErrors.some(isThrottledError)) {
// There was no throttling for this request.
// Fallback to ApolloClient's own error handler.
return;
}
const cost = response.extensions.cost;
if (!cost) {
// We require the cost extension to calculate the delay.
// Fallback to ApolloClient's own error handler.
return;
}
if (exceedsMaximumCost(cost)) {
// Your query costs more than the maximum allowed cost.
// Fallback to ApolloClient's own error handler.
return;
}
const msToWait = calculateDelayCost(cost);
operation.setContext({ retry: true, msToWait });
return delay(msToWait).concat(forward(operation));
}
);
To implement the ErrorLink
above, these functions are needed:
exceedsMaximumCost
: will check whether the GraphQL operation failed due to a request that exceeds the maximum cost of1000
calculateDelayCost
: returns the required wait time in milliseconds to be able to perform the GraphQL operationdelay
: Apollo links can be composed usingObservables
from thezen-observable
package.delay
will be used to make the operation wait until it can be retried
Lets define these functions:
export function exceedsMaximumCost(cost) {
const {
requestedQueryCost,
actualQueryCost,
throttleStatus: { maximumAvailable },
} = cost;
const requested = actualQueryCost || requestedQueryCost;
return requested > maximumAvailable;
}
export function calculateDelayCost(cost) {
const {
requestedQueryCost,
actualQueryCost,
throttleStatus: { currentlyAvailable, restoreRate },
} = cost;
const requested = actualQueryCost || requestedQueryCost;
const restoreAmount = Math.max(0, requested - currentlyAvailable);
const msToWait = Math.ceil(restoreAmount / restoreRate) * 1000;
return msToWait;
}
function delay(msToWait) {
return new Observable((observer) => {
let timer = setTimeout(() => {
observer.complete();
}, msToWait);
return () => clearTimeout(timer);
});
}
And with this rateLimit
link that we just created, we can compose our ApolloClient
as following:
// client.js
import {
ApolloClient,
HttpLink,
ApolloLink,
InMemoryCache,
} from "@apollo/client";
const rateLimit = onError(/*...code above...*/);
export const createClient = (shop, accessToken) => {
const http = new HttpLink({
uri: `https://${shop}/admin/api/2021-04/graphql.json`,
});
// Compose the rateLimit ErrorLink with the HttpLink into a single link
const link = new ApolloLink.from([
rateLimit,
http
]);
return new ApolloClient({
cache: new InMemoryCache(),
link,
});
};
And you got yourself a retry mechanism for your rate limited requests in Shopify Admin GraphQL API! This rate limit ErrorLink
has also been packaged into a handy NPM library: apollo-link-shopify-retry
. Just simply run npm install apollo-link-shopify-retry
and set it up by following the readme.