# API Limits

Learn how to handle API rate limits.

API limits are technical safeguards that ensure your GraphQL API performs optimally and remains available for all users. These limits guard against common problems, such as big requests, inefficient queries, or high traffic, that could otherwise impact performance.

**Note:**
We do not enforce any limits on requests that hit our [CDN cache](https://hygraph.com/docs/api-reference/basics/caching). API limits are enforced on all uncached GraphQL queries for shared regions. The rate limiting depends on the current load of the shared region and the subscription plan. These limits can be lifted on dedicated clusters and enterprise plans. You can [contact sales](https://hygraph.com/contact) to request a custom plan.

## Request size

The maximum size of GraphQL queries and mutations, including the text and variables, as it reaches our API. This helps prevent oversized requests that could slow down your API. When you exceed this limit, you'll get a [`413 error`](https://hygraph.com/docs/api-reference/basics/errors#413-payload-too-large).

| Plan  | Limit                  |
| ----- | ---------------------- |
| Hobby | <ul><li>Queries: 10 KB </li><li>Mutations: 30 KB </li></ul>|
| Growth | <ul><li>Queries: 15 KB </li><li>Mutations: 70 KB </li></ul>|
| Enterprise | <ul><li>Queries: 20 KB </li><li>Mutations: 80 KB </li></ul> |

Follow these steps to check your query/ mutation request size.
1. In your browser, go to **Developer Tools > Network**.
2. Run the GraphQL request in the [API Playground](https://hygraph.com/docs/api-reference/basics/api-playground).
3. Click on the request and check the **Request Payload** or **Size** column.
4. Compare the size against our limits.

We recommend that you:
- Test your queries against limits during development.
- Break large queries into smaller, focused requests.
  For example: Instead of a query like this:

      **Before - A large query**

    ```graphql
    query DashboardData($userId: ID!) {
      user(id: $userId) {
        id
        name
        email
        avatar { url width height }
        settings {
          locale
          timezone
          notifications { marketing product updates security }
        }
        followers(first: 200) {
          nodes { id name avatar { url } }
        }
        posts(first: 50) {
          nodes {
            id
            title
            slug
            excerpt
            body
            coverImage { url width height }
            tags { id name }
            author { id name avatar { url } }
            comments(first: 200) {
              nodes {
                id
                body
                createdAt
                author { id name }
              }
            }
          }
        }
        notifications(first: 100) {
          nodes { id type message createdAt readAt }
        }
      }
    }
    ```


    You can split it into three separate queries:

      **Query 1**

    ```graphql
    # Request 1: lightweight user profile
    query UserProfile($userId: ID!) {
      user(id: $userId) {
        id
        name
        avatar { url }
        settings { locale timezone }
      }
    }
    ```

      
      **Query 2**

    ```graphql
    # Request 2: concise posts list for the dashboard
    query UserPostsList($userId: ID!) {
      user(id: $userId) {
        id
        posts(first: 20) {
          nodes { id title slug excerpt coverImage { url } }
        }
      }
    }
    ```

      
      **Query 3**

    ```graphql
    # Request 3: fetch comments on-demand (example, when opening a post)
    query PostComments($postId: ID!) {
      post(id: $postId) {
        id
        comments(first: 20) {
          nodes {
            id
            body
            createdAt
            author { id name }
          }
        }
      }
    }
    ```


- Use GraphQL fragments to avoid repetition.
  For example: Instead of a query like this:

      **Before**

    ```graphql
    query ArticlePage($id: ID!) {
      post(id: $id) {
        id
        title
        author {
          id
          name
          username
          avatar { url width height }
        }
        lastEditor {
          id
          name
          username
          avatar { url width height }
        }
        comments(first: 20) {
          nodes {
            id
            body
            author {
              id
              name
              username
              avatar { url width height }
            }
          }
        }
      }
      me {
        id
        name
        username
        avatar { url width height }
        followers(first: 10) {
          nodes {
            id
            name
            username
            avatar { url width height }
          }
        }
        following(first: 10) {
          nodes {
            id
            name
            username
            avatar { url width height }
          }
        }
      }
    }
    ```

      
      **Recommended - With fragments**

    ```graphql
    fragment UserSummary on User {
      id
      name
      username
      avatar { url }
    }

    query ArticlePage($id: ID!) {
      post(id: $id) {
        id
        title
        author { ...UserSummary }
        lastEditor { ...UserSummary }
        comments(first: 20) {
          nodes {
            id
            body
            author { ...UserSummary }
          }
        }
      }
      me {
        ...UserSummary
        followers(first: 10) { nodes { ...UserSummary } }
        following(first: 10) { nodes { ...UserSummary } }
      }
    }
    ```
      

- Instead of relying on long and complex variable filters, use pagination to handle large data sets.

  For example: Instead of a query like this:

      **Before-Query**

    ```graphql
    query BulkPosts($ids: [ID!]!) {
      posts(where: { id_in: $ids }, first: 5000) {
        nodes {
          id
          title
          excerpt
          author { id name }
          comments(first: 500) { nodes { id body author { id name } } }
        }
      }
    }
    ```

      
      **Before-Variables**

    ```json
    {
      "ids": [
        "post_000001", "post_000002", "post_000003", "post_000004", "post_000005",
        "... thousands more ids ...",
        "post_004999", "post_005000"
      ]
      // ≈ hundreds of kilobytes, just for variables → likely 413 (Payload Too Large)
    }
    ```


    You can split it into:

      **Query 1**

    ```graphql
    # Request 1 ≈ a few bytes; repeat with {"after": "<endCursor>"} until hasNextPage=false
    query PagedPosts($first: Int! = 50, $after: String) {
      posts(first: $first, after: $after) {
        nodes {
          id
          title
          excerpt
          author { id name }
        }
        pageInfo { endCursor hasNextPage }
      }
    }
    ```

      
      **Query 1-Variables**

    ```json
    { "first": 50, "after": null }
    ```


      **Query 2**

    ```
    # Request N ≈ Still tiny; fetch next pages on demand
    # Only when a user opens a post, also paginated
    query PagedComments($postId: ID!, $first: Int! = 25, $after: String) {
      post(id: $postId) {
        id
        comments(first: $first, after: $after) {
          nodes { id body author { id name } }
          pageInfo { endCursor hasNextPage }
        }
      }
    }
    ```

      
      **Query 2-Variables**

    ```json
    { "postId": "post_000123", "first": 25, "after": null }
    ```


      **Query 3**

    ```graphql
    # If you must target specific IDs, batch them.
    query BatchByIds($ids: [ID!]!) {
      posts(where: { id_in: $ids }) {
        nodes { id title excerpt }
      }
    }
    ```

      
      **Query 3-Variables**

    ```json
    { "ids": ["post_000001", "... up to ~100 per batch ..."] }
    // Send multiple small requests instead of one huge list
    ```


We recommend that you do not:
- Fetch unnecessary fields in your queries.
- Create overly complex nested queries.
- Ignore limit violation errors without addressing root causes.

## Requests per second

The number of uncached requests you can send to the Content API per second. A single request can contain multiple queries and mutations. When you exceed this limit, you'll get a [`429 error`](https://hygraph.com/docs/api-reference/basics/errors#429-too-many-requests).

**Note:**

This limit measures how many **requests start per second**. It is different from **concurrency operations**, which is measured by the number of operations running at the same time.

| Plan  | Limit                   |
| ----- | ----------------------- |
| Hobby | 5 req/sec               |
| Growth | 25 req/sec             |
| Enterprise | Up to 500 req/sec  |

## Concurrent operations

The number of uncached GraphQL operations (queries / mutations) that run simultaneously per environment. Multiple operations bundled in a single request count toward the concurrency limit. This helps prevent resource exhaustion during traffic spikes. When you exceed this limit, you'll get a [`429 error`](https://hygraph.com/docs/api-reference/basics/errors#429-too-many-requests).

**Note:** This limit measures **in-flight queries and mutations**. Even if your requests per second (RPS) rate is low, you can still exceed concurrency if many operations run at once. 

| Plan  | Limit per environment                  |
| ----- | ---------------------- |
| Hobby | <ul><li>Queries: 10</li><li>Mutations: 5</li></ul>|
| Growth | <ul><li>Queries: 30</li><li>Mutations: 10</li></ul>|
| Enterprise | <ul><li>Queries: 60</li><li>Mutations: 20</li></ul> |

To stay below the limit, count the total number of queries and mutations sent simultaneously, not just the number of fired requests. If a request contains five queries, it counts as five concurrent queries.
You can also measure the total in-flight operations at any moment, by using a counter or concurrency control library in your script or code.

We recommend that you:
- Implement proper retry logic with [exponential backoff](#exponential-backoff) to handle temporary errors.
- Distribute requests over time rather than sending large bursts at once.
- Apply connection pooling and request queuing to manage load efficiently.
- Monitor your application's concurrent request patterns.

We recommend that you do not:
- Retry immediately after receiving a 429 error.
- Send large batches of requests simultaneously.
- Ignore concurrent limit violations in your error handling.

## Requests per second vs. Concurrent operations

These two limits measure different things:
- Requests per second (RPS): How many new requests you start each second.
- Concurrent operations: How many queries or mutations are running at the same time, even if they came from the same request.

It’s possible to stay within one limit while exceeding the other. Always design your queries and requests to remain under both the RPS and concurrency limits.

| Scenario | Request per second | Concurrent operations | Result|
| ----- | ---------------------- |--------------- | -----------|
|20 requests per second, each finishes in ~50 ms| <ul><li>RPS = 20</li><li>Within Growth plan limit.</li></ul>  | <ul><li>Concurrency ≈ 1 at any time, since requests complete quickly.</li><li>Within Growth plan limit.</li></ul>  | Both within limits.|
|5 requests per second, each contains 10 queries. Each query takes ~2 seconds to finish.| <ul><li>RPS = 5</li><li>Within Growth plan limit.</li></ul> | <ul><li>Concurrency = 50 (5 requests × 10 queries still running)</li><li>Exceeds Growth plan limit.</li></ul> | Exceeds concurrency limit.|
|40 requests sent at once, each with 1 query.| <ul><li>RPS = 40</li><li>Exceeds Growth plan limit.</li></ul>| <ul><li>Concurrency = 40</li><li>Exceeds Growth plan limit.</li></ul> | Exceeds both limits.|

## Handling API rate limits

In this section, learn how to handle API rate limits with **Next.js**, **Gatsby**, and **Nuxt**.

### Next.js

#### Thread limiting

You can use this experimental setting in **Next.js** for disabling multithreading:

```js
// Your Next.js config file (next.config.js)
...
experimental: {
  workerThreads: false,
  cpus: 1
},
...
```

This setting will force the build to be single-threaded, which limits the speed at which requests are made within the `getStaticProps`.

As a result, the build runs slower but completes without errors.

#### Throttling

The following **Next.js** example uses `pThrottle`, and allows you to control the limit of API calls per interval.

```js
import React from 'react';
import { allProducts } from '../../utils/getProducts';
import { gql } from '../../utils/hygraph-client';
import { throttledFetch } from '../../utils/throttle';

// Singular query used in getStaticProps
const query = gql`
  query GetSingleItem($slug: String!) {
    product(where: { slug: $slug }) {
      name
      slug
    }
  }
`;
export async function getStaticPaths() {
  // One call to get all paths
  // No need to throttle this
  // Unless you have a LOT of these calls
  const products = await allProducts();
  const paths = products.map((product) => ({
    params: { slug: product?.slug },
  }));
  return { paths, fallback: false };
}

export async function getStaticProps({ params }) {
  // For each path, there will be an API call
  // We need to throttle this
  // We need to throttle it on a global throttle, so we need to set that externally

  // throttleFetch comes from a utility area and is shared among all dynamic route files
  /*
    import pThrottle from 'p-throttle'
    import hygraphClient from './hygraph-client'

    // Set the limit of # of calls per interval in ms (5 per second)
    const throttle = pThrottle({limit: 5, interval: 1000})
    export const throttledFetch = throttle(async (...args) => {
        const [query, vars] = args

        const data = await hygraphClient.request(query, vars)

        return data
    })
  */
  const product = await throttledFetch(query, { slug: params.slug });
  return {
    props: product,
  };
}

export default function Page({ product }) {
  // Each page produced by paths and props
  return (
    <>
      <h1>{product.name}</h1>
    </>
  );
}
```

#### Exponential backoff

Combining query execution with exponential backoff retries ensures applications remain reliable, even when encountering concurrent operation limits.

1. The `graphqlFetchWithRetry` function provides error handling and a retry strategy with exponential backoff, ensuring that errors such as concurrent operation limits or temporary rate limiting are retried effectively.

    ```js
    // lib/graphql.ts
    type GraphQLRequest = {
      endpoint: string;
      token?: string;
      query: string;
      variables?: Record<string, unknown>;
      maxRetries?: number;
      baseDelayMs?: number; // starting delay (e.g., 250ms)
      maxDelayMs?: number;  // cap delay (e.g., 5000ms)
      signal?: AbortSignal;
    };

    type GraphQLResponse<T> = {
      data?: T;
      errors?: Array<{ message: string; [key: string]: unknown }>;
    };

    function sleep(ms: number, signal?: AbortSignal): Promise<void> {
      return new Promise((resolve, reject) => {
        if (signal?.aborted) return reject(new DOMException('Aborted', 'AbortError'));
        const timer = setTimeout(resolve, ms);
        signal?.addEventListener('abort', () => {
          clearTimeout(timer);
          reject(new DOMException('Aborted', 'AbortError'));
        });
      });
    }

    // Exponential backoff with jitter and a hard minimum of 1000ms per retry.
    function computeBackoffMs(
      attempt: number,
      baseDelayMs: number,
      maxDelayMs: number,
      minimumMs = 1000
    ): number {
      const exp = Math.max(minimumMs, baseDelayMs * Math.pow(2, attempt));
      const jitter = Math.floor(Math.random() * Math.min(250, exp));
      return Math.min(exp + jitter, maxDelayMs);
    }

    function containsConcurrencyError(errors?: Array<{ message: string }>): boolean {
      if (!errors) return false;
      return errors.some(
        (e) =>
          typeof e.message === 'string' &&
          e.message.toLowerCase().includes('concurrent operations limit exceeded')
      );
    }

    export async function graphqlFetchWithRetry<T = unknown>({
      endpoint,
      token,
      query,
      variables,
      maxRetries = 5,
      baseDelayMs = 250,
      maxDelayMs = 5000,
      signal,
    }: GraphQLRequest): Promise<T> {
      let lastError: unknown;

      for (let attempt = 0; attempt <= maxRetries; attempt++) {
        try {
          const res = await fetch(endpoint, {
            method: 'POST',
            headers: {
              'content-type': 'application/json',
              ...(token ? { authorization: `Bearer ${token}` } : {}),
            },
            body: JSON.stringify({ query, variables }),
            signal,
            cache: 'no-store',
          });

          const is429 = res.status === 429;

          if (res.ok) {
            const json = (await res.json()) as GraphQLResponse<T>;
            const concurrencyHit = containsConcurrencyError(json.errors);

            if (!concurrencyHit) {
              if (json.errors) {
                const messages = json.errors.map((e) => e.message).join('; ');
                throw new Error(`GraphQL error: ${messages}`);
              }
              return json.data as T;
            }

            // Concurrency limit via GraphQL errors – backoff and retry
            if (attempt < maxRetries) {
              const delay = computeBackoffMs(attempt, baseDelayMs, maxDelayMs, 1000);
              await sleep(delay, signal);
              continue;
            }

            throw new Error('Exceeded retries due to concurrent operations limit.');
          }

          // HTTP 429 – backoff and retry (minimum 1s)
          if (is429 && attempt < maxRetries) {
            const delay = computeBackoffMs(attempt, baseDelayMs, maxDelayMs, 1000);
            await sleep(delay, signal);
            continue;
          }

          // Other non-OK statuses – surface
          const body = await res.text().catch(() => '');
          throw new Error(`HTTP ${res.status}: ${body || res.statusText}`);
        } catch (err) {
          lastError = err;
          const isAbort = err instanceof DOMException && err.name === 'AbortError';
          if (isAbort) throw err;

          if (attempt < maxRetries) {
            // Network or transient error – backoff with at least 1s
            const delay = computeBackoffMs(attempt, baseDelayMs, maxDelayMs, 1000);
            await sleep(delay, signal);
            continue;
          }
          break;
        }
      }

      throw lastError instanceof Error ? lastError : new Error('Request failed.');
    }
    ```

2. This example demonstrates how to execute a GraphQL query using the `graphqlFetchWithRetry` function defined in the previous step.

    ```
    // app/api/products/route.ts
    import { NextResponse } from 'next/server';
    import { graphqlFetchWithRetry } from '@/lib/graphql';

    const GRAPHQL_ENDPOINT = process.env.GRAPHQL_ENDPOINT!;
    const GRAPHQL_TOKEN = process.env.GRAPHQL_TOKEN!; // Store server-side only

    export async function GET() {
      try {
        const query = `
          query Products($first: Int!) {
            products(first: $first) {
              id
              name
            }
          }
        `;

        const data = await graphqlFetchWithRetry<{
          products: Array<{ id: string; name: string }>;
        }>({
          endpoint: GRAPHQL_ENDPOINT,
          token: GRAPHQL_TOKEN,
          query,
          variables: { first: 10 },
          maxRetries: 5,
          baseDelayMs: 300,
          maxDelayMs: 5000,
        });

        return NextResponse.json({ ok: true, data });
      } catch (error) {
        return NextResponse.json(
          { ok: false, message: (error as Error).message },
          { status: 429 }
        );
      }
    }
    ```

### Gatsby

#### Concurrency override

You can use `queryConcurrency` with our official [Gatsby source plugin for Hygraph projects](https://github.com/hygraph/gatsby-source-graphcms#options).

This key indicates the number of promises ran at once when executing queries. Its default value is set to 10.

**Warning:**
This plugin has been deprecated and no longer receives support. However, if you are already using it and have encountered a problem with rate limits, then we suggest you use the `queryConcurrency` solution.

#### Throttling

The following **Gatsby** example uses `pThrottle`, and allows you to fetch 1 concurrent request maximum, with a minimum delay of 0.5 seconds.

```js
import { createHttpLink } from "apollo-link-http";
 import pThrottle from "p-throttle";
// Throttle fetches to max 1 concurrent request and
  //  min. delay of 0.5 seconds.
const throttledFetch = pThrottle( (...args) => {
    return fetch(...args);
  }, 1, 500);
const link = createHttpLink({ uri: "/graphql" fetch: throttledFetch });
```

### Nuxt

#### Thread limiting

You can add the following to your `nuxt.config.js` to avoid getting a [429 error](https://hygraph.com/docs/api-reference/basics/errors#429-too-many-requests). It will stop GraphQL requests from overloading Hygraph's API limits when building.

```js
// Your Nuxt config file (nuxt.config.js)
generate: {
concurrency: 250, //maximum number of requests per thread. This will only build 250 at a time based on the api rate limit
interval: 200, //delay by 0.2s. You can adjust this to be higher if you still run into issues
},
```
