Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix Now×
Skip to content
concurrency

Making Concurrent Requests in Node.js: Scheduling, Sockets, Queues, and Cleanup

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How do I make multiple HTTP requests at the same time in Node.js? Start each independent request without awaiting it immediately, then await the group. For the built-in HTTP client, use an http.Agent (or https.Agent) to reuse connections and set a per-host socket ceiling with maxSockets. These are separate controls: your code decides how many tasks are scheduled, while the Agent decides how many connections to a host can be active and queues the rest.

What “concurrent” means in Node.js

Node.js can have many network operations in flight while one JavaScript thread continues processing events. A request is concurrent when independent work is started before earlier work has finished. It is not the same as opening one socket per promise, nor does a socket limit automatically cap every promise or application task.

The low-level Node.js HTTP API documentation (v26.10.0) describes HTTP handling in terms of messages and streams; it does not parse your application payloads for you. You read response data, assemble it, and decide whether the result is valid.

Two controls you must keep separate

Control What it governs Typical effect
Application scheduling How many URL-processing tasks your code starts A task limiter can keep a large input list from creating thousands of promises at once.
http.Agent/https.Agent Connection persistence, reuse, and sockets per host maxSockets caps active sockets for each host; additional requests wait in the Agent queue.
Remote server behavior Whether an existing connection remains reusable An idle connection can be closed or reuse can be refused, forcing a new connection.

For example, you might schedule 20 URL tasks but configure maxSockets: 4. The first four eligible requests can use sockets to a host; the remainder are pending until a socket becomes available. If your tasks target several hosts, the Agent applies its socket limit per host rather than as one universal promise limit.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A complete concurrent HTTPS example

The following ES module uses Node.js’s built-in https client. It reuses connections, limits each host to four active sockets, collects response bodies, and cleans up the Agent when the batch is complete.

import https from 'node:https';

const agent = new https.Agent({
  keepAlive: true,
  maxSockets: 4
});

function getText(url) {
  return new Promise((resolve, reject) => {
    const req = https.get(url, { agent }, (res) => {
      const chunks = [];
      res.setEncoding('utf8');
      res.on('data', (chunk) => chunks.push(chunk));
      res.on('end', () => {
        const body = chunks.join('');
        if (res.statusCode >= 200 && res.statusCode < 300) {
          resolve({ url, statusCode: res.statusCode, body });
        } else {
          reject(new Error(`${url} returned HTTP ${res.statusCode}`));
        }
      });
    });

    req.on('error', reject);
    req.setTimeout(15_000, () => {
      req.destroy(new Error(`Timed out: ${url}`));
    });
  });
}

const urls = [
  'https://example.com/',
  'https://nodejs.org/',
  'https://www.iana.org/domains/example'
];

try {
  const results = await Promise.all(urls.map(getText));
  for (const result of results) {
    console.log(result.statusCode, result.url, result.body.length);
  }
} catch (error) {
  console.error('At least one request failed:', error);
} finally {
  agent.destroy();
}

Why this starts requests concurrently

urls.map(getText) invokes getText for every URL before Promise.all waits for results. Each invocation enters the HTTP client and returns a promise while the response stream is still pending. Promise.all resolves when every operation succeeds and rejects as soon as one rejects, so the example treats the batch as a unit.

What the Agent is doing

The Agent keeps eligible connections alive for reuse and allows up to four active sockets per host. When all four are occupied, later requests are queued by the Agent and become active when a socket is free. This is connection-level back-pressure; it is not a general scheduler for unrelated asynchronous work.

Always release the Agent

Call agent.destroy() when the batch or service no longer needs it. Unused sockets consume operating-system resources. In a long-running process that makes repeated batches, keep one Agent for the intended lifetime and destroy it during shutdown rather than creating a new Agent for every request.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When a large list needs an application-level limit

Promise.all(urls.map(getText)) is convenient for a short list, but a list containing thousands of items schedules all tasks immediately. Use a small worker pool when you need to control task creation independently of socket reuse.

async function mapConcurrent(items, limit, worker) {
  if (!Number.isInteger(limit) || limit < 1) {
    throw new RangeError('limit must be a positive integer');
  }

  const output = new Array(items.length);
  let next = 0;

  async function run() {
    while (true) {
      const index = next++;
      if (index >= items.length) return;
      output[index] = await worker(items[index], index);
    }
  }

  const workerCount = Math.min(limit, items.length);
  await Promise.all(Array.from({ length: workerCount }, run));
  return output;
}

const results = await mapConcurrent(urls, 8, getText);

Here, at most eight worker tasks are processing items at once. The Agent can still impose a lower per-host socket ceiling, so eight scheduled tasks might result in four active sockets to one host and four queued requests. Conversely, a high maxSockets does not force your worker pool to start more tasks.

Choosing maxSockets responsibly

Use the default when you have no measured reason to change it

The Agent’s per-host limit is a safety and resource setting, not a promise of faster completion. Raising it can increase simultaneous connections, local file-descriptor use, TLS work, and pressure on the remote service. Lowering it increases queuing and can reduce burst pressure.

Match the limit to the target and workload

  • For a small interactive batch, a modest value keeps latency predictable without creating a connection burst.
  • For a crawler or ingestion job, combine a task limiter with a per-host socket limit and respect the target service’s policies.
  • For multiple hosts, remember that the Agent limit is evaluated per host; your total open sockets can therefore exceed the value for any one host.

Do not assume every connection will be reused

Reuse depends on the server. A server may close an idle connection or decline to keep it reusable. The next request then needs a new connection even though your Agent is configured for persistence.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Handling partial failures instead of losing the whole batch

When each URL is independent, use a settled-result pattern so one failure does not hide successful responses.

const settled = await Promise.allSettled(urls.map(getText));

for (const [index, result] of settled.entries()) {
  if (result.status === 'fulfilled') {
    console.log('OK', urls[index], result.value.statusCode);
  } else {
    console.error('FAILED', urls[index], result.reason.message);
  }
}

Keep the request function responsible for transport and status handling, and let the caller decide whether one failure should abort the operation, be logged, or be retried under an explicit policy. Do not treat an HTTP error status as a successful payload merely because a TCP connection completed.

Timeouts, streams, and response size

Set a bounded wait

A request can remain pending if a peer stops responding. The example uses req.setTimeout and destroys the request, causing its promise to reject. Choose a timeout appropriate to the endpoint and workload; a single value is not universally correct.

Consume the response stream

Reading data and waiting for end allows the response to complete and the connection to become eligible for reuse. If you only inspect headers and abandon the stream, you can interfere with reuse and leave work unfinished.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Protect memory for untrusted sizes

The example buffers the complete body for clarity. For large or untrusted responses, count bytes while streaming and destroy the response when a configured limit is exceeded, or process chunks incrementally. Buffering every response from a large concurrent batch can exhaust memory even when socket counts are modest.

Common failure modes and fixes

“Everything is still sequential”

Check for an await inside a loop that starts and finishes one request before creating the next. Build the promises first (for example, with map) or use a worker pool.

More requests are queued than expected

Inspect the Agent configuration and the destination host. maxSockets is per host, and requests above that ceiling wait in the Agent’s pending queue. Increase it only when the remote service and your process can handle the additional connections.

Connections are not reused

The server may close idle connections or refuse reuse. Confirm that the same Agent is passed to each request and that its lifetime spans the batch. Even then, server behavior can require new connections.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The process keeps resources after the batch

Destroy an Agent that is no longer needed. In a service, perform that cleanup in your shutdown path after stopping new work.

One bad URL rejects the whole result

Use Promise.allSettled for independent work, or catch errors per item in your worker function. Keep the rejected reason associated with its URL so operators can diagnose it.

Memory rises during a large batch

Reduce the application worker limit, cap response sizes, and process streams incrementally. A low socket limit alone does not prevent many queued promises from retaining input and result data.

Observability and safe tuning

Record the URL (or a redacted host and path), start and finish times, status code, response bytes, timeout or socket errors, and whether the item was queued by your own worker pool. Compare completion time and error rate before changing limits. A faster batch that triggers more remote failures or local resource exhaustion is not an improvement.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep credentials out of URLs and logs. Use HTTPS for sensitive requests, validate redirects and response content for your application, and avoid sending a concurrency burst to a service that has not authorized it.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your concurrent jobs are website screenshots rather than API payloads, ScreenshotNeo provides a single HTTP endpoint and an MCP server for AI clients. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers.

Use the API directly from Node.js (see the ScreenshotNeo API documentation):

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

The same request with cURL is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

And with Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

ScreenshotNeo supports PNG, JPEG, WebP, and PDF output; full-page captures with lazy images, CSS-selector element captures, dark mode, device presets, arbitrary viewports, retina scale, PDF paper and page options, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable-TTL caching, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. Its parameter names are compatible with those used by other screenshot APIs, which can simplify migration.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Frequently asked questions

Does maxSockets limit all Node.js promises?

No. It limits concurrent sockets per host for requests using that Agent. Promises for other work require their own scheduling control.

Should I create one Agent per request?

No. Reuse an Agent for the lifetime of a related batch or service so connections can persist, then destroy it when that lifetime ends.

Why can a request wait even when my code started it?

The Agent may have reached its per-host socket ceiling, placing the request in a pending queue until a socket is available.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Can I use this pattern with HTTP as well as HTTPS?

Yes. Use Node’s http module and an http.Agent for HTTP URLs; use https and https.Agent for TLS URLs.

Is a higher socket limit always faster?

No. It can increase local and remote resource pressure, while server connection policies may prevent reuse. Measure the actual workload and tune conservatively.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read next

Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.