> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zenrows.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Concurrency with the Node.js SDK

> Scrape multiple URLs in parallel with the ZenRows Node.js SDK's built-in concurrency queue.

Every call to `client.get()` or `client.post()` already returns a `Promise`, so there's no separate "async" method to learn. The SDK queues every request from a client instance internally and only runs up to `concurrency` of them at the same time.

## Basic usage

<Steps>
  <Step title="Set a concurrency limit">
    Pass `concurrency` when creating the client to cap how many requests run at the same time:

    ```javascript theme={null} theme={null}
    const { ZenRows } = require("zenrows");

    const client = new ZenRows("YOUR_ZENROWS_API_KEY", { concurrency: 5, retries: 1 });
    ```

    <Note>
      Set `concurrency` to match your ZenRows plan's concurrent request limit. Each client instance enforces its own limit. Two scripts (or two client instances) don't share it, so running both at once can still trigger `429 Too Many Requests` errors. See [Concurrency](/universal-scraper-api/features/concurrency) for how plan limits work.
    </Note>
  </Step>

  <Step title="Scrape a list of URLs in parallel">
    Map your URLs to `client.get()` calls and use `Promise.allSettled()` to wait for all of them, even if some fail:

    ```javascript theme={null} theme={null}
    const { ZenRows } = require("zenrows");

    const client = new ZenRows("YOUR_ZENROWS_API_KEY", { concurrency: 5, retries: 1 });

    const urls = [
      "https://www.scrapingcourse.com/ecommerce/",
      // ... more URLs
    ];

    (async () => {
      const promises = urls.map((url) => client.get(url));

      const results = await Promise.allSettled(promises);
      console.log(results);
    })();
    ```

    `Promise.allSettled()` waits for every call to finish, even if some of them fail. Unlike `Promise.all()`, it doesn't reject as soon as one promise fails, so the rest of the batch still completes.
  </Step>

  <Step title="Split fulfilled and rejected results">
    ```javascript theme={null} theme={null}
    const rejected = results.filter(({ status }) => status === "rejected");
    const fulfilled = results.filter(({ status }) => status === "fulfilled");
    ```

    Each fulfilled result's `.value` is a standard `Response`, read the same way as `client.get()`'s return value.
  </Step>
</Steps>

## When to think about concurrency

You don't need to do anything special to run requests concurrently, calling `.get()`/`.post()` in a `.map()` already benefits from the internal queue. Set `concurrency` explicitly when you want to match a specific plan limit, or lower it if you're seeing `429` errors.

## POST in parallel

The same pattern works with `client.post()`:

```javascript theme={null} theme={null}
const promises = urls.map((url) =>
  client.post(url, {}, { data: new URLSearchParams({ key1: "value1" }).toString() }),
);

const results = await Promise.allSettled(promises);
```

## How it works under the hood

<Info>
  The SDK uses <a href="https://github.com/mcollina/fastq" target="_blank" rel="noopener noreferrer nofollow">fastq</a> internally to queue every `get()` and `post()` call from the same client instance and run at most `concurrency` of them at once (default `5`). This applies uniformly, there's no separate queue for GET versus POST. Retry behavior configured via `retries` applies the same way regardless of how many requests run in parallel. See [Error Handling and Retries](/universal-scraper-api/sdk/nodejs/error-handling-and-retries) for details.
</Info>

## Troubleshooting

* **`429 Too Many Requests` even though `concurrency` is set correctly**: Another script or process is using the same API key at the same time. Concurrency limits are per plan, not per client instance, so simultaneous scripts share the same real ceiling even though each `ZenRows` client only tracks its own local limit.
* **Some responses in the list are much slower than others**: This is expected; total throughput depends on the slowest requests in each concurrent batch. See the [Concurrency](/universal-scraper-api/features/concurrency#impact-of-request-duration-on-throughput) guide for the underlying math.
* **A `Promise.all()` call fails as soon as one request errors**: Use `Promise.allSettled()` instead, as shown above, so a single failure doesn't abort the whole batch.
* **Cancelling a request doesn't free a concurrency slot right away**: ZenRows keeps the slot occupied for up to a few minutes after a client-side cancellation while it finishes processing server-side.

## Pricing

Requests are billed identically whether they run one at a time or concurrently. Cost depends on the config you send per request, not on how many run in parallel:

| Configuration                             | Cost multiplier vs. basic |
| ----------------------------------------- | ------------------------- |
| Basic request                             | 1x                        |
| `js_render: true`                         | 5x                        |
| `premium_proxy: true`                     | 10x                       |
| `js_render: true` + `premium_proxy: true` | 25x                       |

Running requests concurrently doesn't add cost by itself. It only changes how fast the same requests complete. See [ZenRows Pricing](/first-steps/pricing).

## FAQ (Frequently Asked Questions)

<AccordionGroup>
  <Accordion title="What's the default concurrency if I don't set one?">
    5. You can set it to any number that matches your ZenRows plan's concurrent request limit.
  </Accordion>

  <Accordion title="Do GET and POST share the same concurrency limit?">
    Yes. Every call from the same `ZenRows` instance, GET or POST, goes through one shared internal queue.
  </Accordion>

  <Accordion title="Is there a get_async or similar method?">
    No. Every method already returns a `Promise`, so there's no separate async variant to call, unlike SDKs for languages without built-in async I/O.
  </Accordion>
</AccordionGroup>
