DEV Community

ProxySmart
ProxySmart

Posted on

Building a Safe Proxy Health Check in Node.js: Timeouts, Retries, and Secret-Safe Logs

Proxy checks often start as a one-line HTTP request. That is enough to prove that something responded, but it is not enough to operate a proxy reliably.

A useful health check should answer four questions:

  1. Did the request leave through the expected proxy?
  2. How long did the full request take?
  3. Is the failure temporary or does it require a configuration change?
  4. Can the result be logged without exposing credentials?

This article builds a small Node.js check that returns structured data, retries only transient failures, and keeps secrets out of logs.

What a proxy health check should measure

For an HTTP or HTTPS proxy, a practical minimum is:

  • Connectivity: the TCP/TLS path can complete.
  • Exit IP: the response contains a valid public-facing IP.
  • Latency: the total request stays inside your operational threshold.
  • Failure type: authentication, temporary network failure, or permanent validation error.
  • Attempt count: operators can see whether a successful result required retries.

Do not treat one successful request as a complete service-level indicator. A production monitor should run periodically, use more than one destination endpoint, and alert on a sustained error rate instead of a single timeout.

Keep the proxy URL out of source control

A proxy URL can contain a username and password:

http://username:password@proxy.example:3128
Enter fullscreen mode Exit fullscreen mode

Store it in an environment variable or secret manager. The value should never appear in a repository, exception report, screenshot, or log message.

When an operator needs to know which proxy was checked, strip the user information first:

export function safeProxyLabel(rawUrl) {
  const url = new URL(rawUrl);
  return `${url.protocol}//${url.hostname}:${url.port}`;
}
Enter fullscreen mode Exit fullscreen mode

The label is useful for diagnostics while the credentials remain private.

Create one client with a hard timeout

The example uses Axios and https-proxy-agent:

import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';

export function createProxyClient(proxyUrl) {
  const agent = new HttpsProxyAgent(proxyUrl);

  return axios.create({
    httpAgent: agent,
    httpsAgent: agent,
    proxy: false,
    timeout: 12_000,
    maxRedirects: 3,
    validateStatus: status => status >= 200 && status < 500,
  });
}
Enter fullscreen mode Exit fullscreen mode

The explicit timeout matters. Without it, a failed proxy can leave work waiting far longer than the caller expects. Setting proxy: false also prevents Axios from applying a second proxy configuration on top of the custom agent.

Choose a timeout that matches the job. A health monitor may tolerate several seconds, while a user-facing request usually needs a much tighter limit.

Retry only failures that may recover

Blindly retrying every error creates load and hides configuration problems. HTTP 401 and 407 normally mean credentials or permissions must be fixed; repeating the same request will not help.

Temporary gateway errors, rate limits, timeouts, and connection resets may recover:

const TRANSIENT_CODES = new Set([
  'ECONNRESET',
  'ETIMEDOUT',
  'ECONNABORTED',
  'EAI_AGAIN',
]);

const TRANSIENT_STATUS = new Set([429, 502, 503, 504]);

export function classifyFailure(error) {
  const status = error.response?.status;

  if (status === 401 || status === 407) {
    return { kind: 'authentication', retryable: false };
  }

  if (TRANSIENT_STATUS.has(status) || TRANSIENT_CODES.has(error.code)) {
    return { kind: 'transient', retryable: true };
  }

  return { kind: 'permanent', retryable: false };
}
Enter fullscreen mode Exit fullscreen mode

Use a small maximum attempt count with exponential backoff and jitter. Jitter prevents many workers from retrying at exactly the same moment:

const sleep = milliseconds =>
  new Promise(resolve => setTimeout(resolve, milliseconds));

export async function withRetry(operation, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
    try {
      return { value: await operation(), attempts: attempt };
    } catch (error) {
      const failure = classifyFailure(error);

      if (!failure.retryable || attempt === maxAttempts) {
        error.failureKind = failure.kind;
        error.attempts = attempt;
        throw error;
      }

      const exponential = 400 * (2 ** (attempt - 1));
      const jitter = Math.floor(Math.random() * 200);
      await sleep(exponential + jitter);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

If an API returns Retry-After, prefer that value when it is valid instead of inventing a shorter delay.

Validate the exit IP and return structured output

The check below calls an IP endpoint through the proxy, records total latency, and verifies that the expected response field exists:

import { performance } from 'node:perf_hooks';

export async function checkProxy(client) {
  const startedAt = performance.now();

  try {
    const { value: response, attempts } = await withRetry(() =>
      client.get('https://api.ipify.org?format=json')
    );

    const latencyMs = Math.round(performance.now() - startedAt);
    const exitIp = response.data?.ip;

    if (!exitIp) throw new Error('The IP endpoint returned no exit IP');

    return {
      checkedAt: new Date().toISOString(),
      status: latencyMs < 5_000 ? 'healthy' : 'degraded',
      latencyMs,
      exitIp,
      attempts,
    };
  } catch (error) {
    return {
      checkedAt: new Date().toISOString(),
      status: 'unhealthy',
      latencyMs: Math.round(performance.now() - startedAt),
      failureKind: error.failureKind ?? 'validation',
      attempts: error.attempts ?? 1,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Structured results are easier to send to a metrics system than free-form log strings. They also let you alert differently: authentication failures should create an immediate configuration ticket, while one transient timeout may only increment a counter.

Production checklist

  • Use only proxies and destination systems you are authorised to access.
  • Run health checks on a schedule, not on every customer request.
  • Limit concurrency so monitoring cannot overload the proxy pool.
  • Use at least two independent IP-check endpoints.
  • Never log proxy passwords, API keys, or complete URLs containing credentials.
  • Keep read-only monitoring separate from purchasing, renewal, and rotation operations.
  • Test retry classification and credential redaction.
  • Track percentiles and sustained failure rates rather than only averages.

Runnable example

The complete MIT-licensed implementation, environment template, lockfile, and Node.js tests are available in the ProxySmart Node.js examples repository.

For API integration examples, authentication details, and documented rate limits, see the ProxySmart developer portal and its public OpenAPI 3.1 specification.

The most important design choice is not the HTTP library. It is making failures observable without turning credentials into telemetry—and retrying only when another attempt has a reasonable chance of succeeding.

Top comments (0)