Inicio
Ubicaciones
Blog
Productos
Proxies residenciales Proxies residenciales ilimitados Proxies estáticos residenciales Proxies de centro de datos estáticos Proxies estáticos compartidos
Recursos
Precios Ubicaciones Blog Centro de ayuda Preguntas frecuentes
Registrarse

Building a Rotating Proxy Middleware in Node.js

Building a Rotating Proxy Middleware in Node.js

A rotating proxy middleware in Node.js should do three jobs well: choose a proxy for each outbound request, stop sending traffic to failing routes for a cooldown period, and give you enough visibility to see which proxy is helping or hurting the workload. That's the difference between "I have a list of proxies" and "I have an outbound network layer I can actually operate."

Method note: this guide is built around Undici's documented ProxyAgent support, the fact that the built-in Node.js fetch does not expose proxy APIs like ProxyAgent, documented Axios proxy configuration, and Proxy001's public residential product pages; it does not claim a benchmark or a proprietary anti-block technique.

What are you building?

The most reliable default in modern Node.js is an Undici-based outbound middleware, because Undici exposes ProxyAgent while the built-in fetch does not expose those proxy-specific APIs directly. That makes Undici the cleaner foundation for a rotating proxy layer where each outbound request may need a different proxy dispatcher.

You are not building an Express middleware in the narrow "mutate req and res" sense. You are building an outbound request middleware that sits between your app logic and the remote destination, selects a proxy, sends the request through that proxy, records the outcome, and applies retry or cooldown rules when a route degrades.

That design matters because rotation by itself is not enough. If you keep reusing a bad proxy after failures, or keep hammering one proxy because it happened to be first in the array, the rotation layer becomes noise instead of protection.

For most teams, the right architecture is:

  • A proxy pool with one object per proxy route.

  • A selector that picks the next healthy proxy.

  • A request wrapper that injects the proxy dispatcher.

  • Outcome tracking for success, failure, and latency.

  • Cooldown logic so broken routes temporarily leave the pool.

  • A small status snapshot so you can verify the middleware is behaving.

If your stack is already Axios-heavy, Axios does support proxy configuration through a proxy object with protocol, host, port, and optional auth, and recent proxy guides also document environment-variable approaches for global proxy behavior. That said, for a new rotating middleware, Undici is the better first path because proxy dispatching is the core problem here, not convenience wrappers.

This is also where provider fit matters. Proxy001's public residential pages list HTTP(S) and SOCKS5 support, 100M+ residential IPs across 200+ locations, rotation options, and public onboarding steps based on credentials or IP whitelisting, which fits the kind of middleware you're building here.

How do you build it?

The shortest good path is: use Undici, keep the pool state in memory, pick the least-busy healthy proxy, send the request through a ProxyAgent, and cool the proxy down after a connection or upstream failure. That gives you a working rotating proxy middleware without dragging in a full scheduler before you even know your traffic pattern.

Prerequisites

Use a standard Node.js project with npm, then install the packages you actually need:

npm init -y
npm install undici express

undici is the important dependency here because that package exposes ProxyAgent, Socks5Agent, and related proxy APIs that are not available through the built-in fetch surface alone. If your app already routes everything through environment variables, Undici also documents EnvHttpProxyAgent, which reads http_proxy, https_proxy, and no_proxy, but that agent is better for global proxy behavior than for per-request rotation.

You also need proxy routes in URI form. For authenticated HTTP proxies, that usually means a URI like:

http://username:password@proxy-host:proxy-port

That format matches how proxy URLs are typically passed into agent-based Node.js proxy flows and keeps the middleware code simpler than splitting host, port, and auth at every call site.

Step 1: Create the proxy pool module

Create a file named proxy-middleware.js:

import { fetch, ProxyAgent } from "undici";

export class RotatingProxyMiddleware {
  constructor(proxyUris, options = {}) {
    if (!Array.isArray(proxyUris) || proxyUris.length === 0) {
      throw new Error("proxyUris must be a non-empty array");
    }

    this.cooldownMs = options.cooldownMs ?? 30000;
    this.maxRetries = options.maxRetries ?? 1;
    this.requestTimeoutMs = options.requestTimeoutMs ?? 15000;

    this.pool = proxyUris.map((uri, index) => ({
      id: `proxy-${index + 1}`,
      uri,
      agent: new ProxyAgent({ uri }),
      inFlight: 0,
      successCount: 0,
      failureCount: 0,
      lastLatencyMs: null,
      unhealthyUntil: 0,
      lastError: null,
      lastUsedAt: 0,
    }));

    this.pointer = 0;
  }

  healthyPool() {
    const now = Date.now();
    return this.pool.filter((p) => p.unhealthyUntil <= now);
  }

  pickProxy() {
    const candidates = this.healthyPool();

    if (candidates.length === 0) {
      throw new Error("No healthy proxies available");
    }

    candidates.sort((a, b) => {
      if (a.inFlight !== b.inFlight) return a.inFlight - b.inFlight;
      return a.lastUsedAt - b.lastUsedAt;
    });

    const proxy = candidates[0];
    proxy.lastUsedAt = Date.now();
    return proxy;
  }

  markSuccess(proxy, latencyMs) {
    proxy.successCount += 1;
    proxy.lastLatencyMs = latencyMs;
    proxy.lastError = null;
  }

  markFailure(proxy, error) {
    proxy.failureCount += 1;
    proxy.lastError = error?.message ?? String(error);
    proxy.unhealthyUntil = Date.now() + this.cooldownMs;
  }

  async request(url, init = {}) {
    let attempt = 0;
    let lastError;

    while (attempt <= this.maxRetries) {
      const proxy = this.pickProxy();
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
      const startedAt = Date.now();

      proxy.inFlight += 1;

      try {
        const response = await fetch(url, {
          ...init,
          dispatcher: proxy.agent,
          signal: controller.signal,
        });

        if (response.status >= 500) {
          throw new Error(`Upstream returned ${response.status}`);
        }

        this.markSuccess(proxy, Date.now() - startedAt);

        return {
          proxyId: proxy.id,
          proxyUri: proxy.uri,
          response,
        };
      } catch (error) {
        this.markFailure(proxy, error);
        lastError = error;
        attempt += 1;

        if (attempt > this.maxRetries) {
          throw lastError;
        }
      } finally {
        clearTimeout(timeout);
        proxy.inFlight -= 1;
      }
    }

    throw lastError;
  }

  snapshot() {
    return this.pool.map((p) => ({
      id: p.id,
      inFlight: p.inFlight,
      successCount: p.successCount,
      failureCount: p.failureCount,
      lastLatencyMs: p.lastLatencyMs,
      unhealthyUntil: p.unhealthyUntil,
      lastError: p.lastError,
    }));
  }
}

Why this shape works:

  • It uses ProxyAgent, which is the correct Undici primitive for routing through a proxy.

  • It rotates per outbound request instead of forcing one process-wide proxy.

  • It applies cooldown after failure instead of immediately throwing the same proxy back into live traffic.

  • It exposes snapshot() so you can verify real behavior instead of guessing.

Step 2: Add a small Express integration

Create server.js:

import express from "express";
import { RotatingProxyMiddleware } from "./proxy-middleware.js";

const app = express();

const proxyPool = new RotatingProxyMiddleware(
  [
    process.env.PROXY_1_URI,
    process.env.PROXY_2_URI,
    process.env.PROXY_3_URI,
  ].filter(Boolean),
  {
    cooldownMs: 30000,
    maxRetries: 1,
    requestTimeoutMs: 15000,
  }
);

app.get("/proxy-status", (req, res) => {
  res.json(proxyPool.snapshot());
});

app.get("/debug/ip", async (req, res, next) => {
  try {
    const { proxyId, response } = await proxyPool.request(
      process.env.IP_CHECK_URL,
      { method: "GET" }
    );

    const body = await response.text();

    res.json({
      proxyId,
      upstream: body,
    });
  } catch (error) {
    next(error);
  }
});

app.get("/fetch-page", async (req, res, next) => {
  try {
    const targetUrl = req.query.url;
    if (!targetUrl) {
      return res.status(400).json({ error: "Missing ?url=" });
    }

    const { proxyId, response } = await proxyPool.request(targetUrl, {
      method: "GET",
      headers: {
        "user-agent": "node-rotating-proxy-middleware/1.0",
      },
    });

    const html = await response.text();

    res.json({
      proxyId,
      status: response.status,
      bytes: Buffer.byteLength(html),
    });
  } catch (error) {
    next(error);
  }
});

app.use((error, req, res, next) => {
  res.status(502).json({
    error: error.message,
  });
});

app.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});

This is enough to solve the real problem for most teams. You now have a Node.js service that rotates outbound requests across a proxy pool, cools down failing proxies, retries once on failure, and gives you a status endpoint to inspect pool health.

Step 3: Set your environment variables

Export your proxy URIs and a verification endpoint before you start the server:

export PROXY_1_URI="http://user:pass@proxy-host-1:port"
export PROXY_2_URI="http://user:pass@proxy-host-2:port"
export PROXY_3_URI="http://user:pass@proxy-host-3:port"
export IP_CHECK_URL="https://your-ip-check-endpoint.example"
node server.js

If your provider issues SOCKS5 residential routes instead of HTTP(S), Undici's public docs note separate SOCKS support through Socks5Agent, which is why you should match the agent type to the endpoint type instead of forcing everything into one proxy shape.

Step 4: Add an Axios adapter only if your app already depends on Axios

If your service is already standardized on Axios, keep the same selector logic and feed its result into Axios's documented proxy config rather than rewriting the whole app at once.

A minimal adapter looks like this:

import axios from "axios";

function axiosProxyFromUri(uri) {
  const url = new URL(uri);

  return {
    protocol: url.protocol.replace(":", ""),
    host: url.hostname,
    port: Number(url.port),
    auth: url.username
      ? {
          username: decodeURIComponent(url.username),
          password: decodeURIComponent(url.password),
        }
      : undefined,
  };
}

export async function axiosRequestWithRotation(proxyPool, url, config = {}) {
  const proxy = proxyPool.pickProxy();

  try {
    proxy.inFlight += 1;

    const response = await axios.get(url, {
      ...config,
      proxy: axiosProxyFromUri(proxy.uri),
      timeout: 15000,
    });

    proxyPool.markSuccess(proxy, null);
    return { proxyId: proxy.id, response };
  } catch (error) {
    proxyPool.markFailure(proxy, error);
    throw error;
  } finally {
    proxy.inFlight -= 1;
  }
}

Axios supports a native proxy object for common HTTP proxy scenarios, and current proxy setup guides also document environment-variable patterns when the whole process should share one proxy policy. For a real rotating middleware, though, Undici still makes the cleaner main implementation.

How do you verify and operate it?

The first test is not your target site. The first test is whether repeated requests actually move through different proxy IDs in your own middleware and whether a failing proxy disappears from selection long enough for the rest of the pool to keep working.

Run these checks in order:

  1. Hit /proxy-status before any traffic and confirm the pool loaded correctly.

  2. Hit /debug/ip several times and confirm the returned proxyId changes across requests.

  3. Break one proxy URI on purpose, restart, and confirm that proxy starts collecting failures and enters cooldown.

  4. Watch the remaining proxies continue to serve traffic while the failed route is excluded.

  5. Restore the broken URI and confirm the pool uses it again after cooldown expires.

A good verify result looks like this:

  • inFlight rises and falls instead of getting stuck.

  • failureCount increases only on the degraded proxy.

  • unhealthyUntil gets set after a failure and prevents immediate reuse.

  • Healthy proxies continue receiving requests while the bad route cools down.

The three most common failures are predictable:

  • Symptom: every request uses the same proxy.Cause: you selected randomly once at startup instead of per request, or your selector never updates state.Fix: move proxy selection into the request wrapper and update lastUsedAt and inFlight on each call.

  • Symptom: failed proxies never leave rotation.Cause: you record the error but never set a cooldown window.Fix: set unhealthyUntil immediately on failure and filter unhealthy proxies before every selection.

  • Symptom: the whole pool stops on one bad route.Cause: retries are bound to the same proxy instead of reselecting from the healthy pool.Fix: retry by calling the selector again, not by resending through the same broken dispatcher.

One honest limitation: this middleware gives you controlled rotation, but it does not replace target-aware rate control. The same proxy reused too often can still become a problem, which is exactly why current Axios proxy guidance recommends rotating proxies instead of reusing one route indefinitely.

Compliance note and CTA

Use rotating residential proxy middleware for approved workflows such as regional QA, ad verification, localized content validation, fraud analysis, and authorized data-quality testing. Keep retries, concurrency, and request volume within the service's terms or your approved testing scope, and do not turn proxy rotation into a playbook for defeating access controls.

If you want to pair this middleware with a provider that already exposes the knobs you need on the supply side, Proxy001 is a practical option because its public residential pages list 100M+ residential IPs in 200+ locations, HTTP(S) and SOCKS5 support, rotation options, IP whitelisting, and credential-based integration, plus public onboarding steps that map cleanly to the URI-based setup shown above. Start small: load a few routes into the pool, verify cooldown and retry behavior with your own status endpoint, and only then widen concurrency or geography.

Inicie su servicio de proxy global
seguro y estable
Comience en solo unos minutos y libere completamente el potencial de los proxies.
Empezar