Using Proxies in Deno and Bun: A Code-First Guide

Learn how to route fetch() through residential proxies in Deno and Bun, with runnable examples for geo-targeting, sticky sessions, retries, and production scraping.

Using Proxies in Deno and Bun: A Code-First Guide
In this article

Why Using Proxies in Deno and Bun Isn't Built Into fetch()

If you've ever tried fetch('https://example.com') through a proxy in Deno or Bun and watched the request sail straight to the target with no proxy in sight, you're not alone. Using Proxies in Deno and Bun requires runtime-specific configuration because the modern JavaScript fetch() API — standardized by the WHATWG Fetch specification — intentionally omits proxy parameters. The spec delegates proxy handling to the environment, not the caller, which is why browser fetch respects HTTP_PROXY but server-side runtimes each pick their own approach.

Deno solves this with Deno.createHttpClient({ proxy }), a configurable HTTP client you pass into fetch(url, { client }). Bun takes the ergonomic route: fetch(url, { proxy }) accepts a proxy URL directly. Both support HTTP_PROXY/HTTPS_PROXY environment variables as a fallback. This guide walks through each path with runnable code, production patterns, and the pitfalls that bite at scale.

The Core Problem: fetch() Ignores Your Proxy

The fetch() signature has no proxy option in the spec. Runtimes that want to honor proxies must extend it. Node.js historically required undici's ProxyAgent; Deno and Bun each ship their own mechanism. If you forget to configure it, your request uses the local IP — which is fine for development and fatal for scraping.

Deno: Deno.createHttpClient and the { client } Option

Deno exposes a low-level HTTP client factory, Deno.createHttpClient, that accepts a proxy object with a url and optional basicAuth. You then pass that client as the client field of the fetch options bag. This keeps the fetch API spec-compliant while adding proxy support out-of-band.

// deno run --allow-net --allow-env proxy_deno.ts
// Basic Deno proxy via Deno.createHttpClient

const proxyUrl = 'http://user-country-US:pass@gate.proxyhat.com:8080';

const client = Deno.createHttpClient({
  proxy: {
    url: proxyUrl,
    // basicAuth is optional when creds are in the URL
  },
});

try {
  const res = await fetch('https://api.ipify.org?format=json', { client });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const json = await res.json();
  console.log('Egress IP:', json.ip);
} catch (err) {
  console.error('Request failed:', err);
} finally {
  client.close(); // free resources
}

Note the --allow-net permission flag — Deno's sandbox requires explicit network access. The client.close() call releases underlying connection pools; in long-running services, reuse a single client rather than creating one per request.

SOCKS5 on Port 1080 in Deno

ProxyHat also exposes a SOCKS5 endpoint on port 1080. Deno's createHttpClient accepts a socks5:// URL for the proxy:

// SOCKS5 proxy in Deno
const socksClient = Deno.createHttpClient({
  proxy: {
    url: 'socks5://user-country-DE:pass@gate.proxyhat.com:1080',
  },
});

const res = await fetch('https://api.ipify.org?format=json', {
  client: socksClient,
});
console.log(await res.json());
socksClient.close();

Bun: One-Line fetch with { proxy }

Bun's fetch() accepts a proxy string directly — no separate client object. This is the most ergonomic API of the three major runtimes.

// bun run proxy_bun.ts
// Basic Bun proxy via fetch options

const proxy = 'http://user-country-US:pass@gate.proxyhat.com:8080';

try {
  const res = await fetch('https://api.ipify.org?format=json', {
    proxy,
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const json = await res.json();
  console.log('Egress IP:', json.ip);
} catch (err) {
  console.error('Request failed:', err);
}

Bun also honors HTTP_PROXY and HTTPS_PROXY environment variables automatically — useful when you can't modify the fetch call site (e.g., inside a third-party library).

SOCKS5 in Bun

// SOCKS5 in Bun
const res = await fetch('https://api.ipify.org?format=json', {
  proxy: 'socks5://user-country-DE:pass@gate.proxyhat.com:1080',
});
console.log(await res.json());

Encoding Geo-Targeting and Sticky Sessions in the Username

ProxyHat encodes routing instructions in the username field, not as query parameters. This keeps the proxy URL compatible with every HTTP client. The format is user-country-XX-city-yyy-session-zzz.

FlagExampleEffect
Countryuser-country-USExit IP in the United States
Country + Cityuser-country-DE-city-berlinExit IP in Berlin, Germany
Sticky sessionuser-session-abc123Same exit IP for the session ID's lifetime
Combineduser-country-US-session-abc123US IP, pinned to session abc123

Sticky sessions are essential when a target site issues cookies or tokens bound to an IP — rotating mid-session triggers CAPTCHAs or logouts. A typical pattern: generate one session ID per logical user or job, and reuse it for the job's lifetime.

Environment Variables vs Per-Client Configuration

Both runtimes respect HTTP_PROXY and HTTPS_PROXY. This is convenient for quick scripts and CI pipelines, but it has a hidden cost: every outbound request in the process inherits the same proxy. If you're mixing proxied scraping with direct API calls (e.g., posting results to your own backend), the env-var approach forces everything through the proxy, adding latency and consuming proxy bandwidth unnecessarily.

// Env-var approach — affects ALL fetch calls in the process
// Deno: deno run --allow-net --allow-env proxy_env.ts
// Bun:  bun run proxy_env.ts

process.env.HTTP_PROXY = 'http://user-country-US:pass@gate.proxyhat.com:8080';
// Bun also reads HTTPS_PROXY for https:// targets

// This request is proxied:
const a = await fetch('https://api.ipify.org?format=json');
console.log('Proxied:', await a.json());

// This request is ALSO proxied — usually not what you want:
const b = await fetch('https://internal.mycompany.com/health');
console.log('Also proxied:', await b.json());

Rule of thumb: use env vars for ad-hoc CLI tools and per-client config ({ client } in Deno, { proxy } in Bun) for libraries and services where you need granular control. Per-client config also lets you rotate different proxy pools for different tasks — e.g., residential for scraping, datacenter for internal health checks.

Why Residential Proxies for High-Block Targets

Datacenter IPs are cheap and fast, but they're also trivially detectable: their ASN ranges are published and widely flagged by anti-bot vendors. Residential proxies route traffic through real ISP-assigned IPs, making the traffic indistinguishable from a normal user on a home connection. For targets protected by Cloudflare, Akamai Bot Manager, or PerimeterX, residential exit IPs typically raise success rates from under 10% to over 90%.

The trade-off is latency and cost. A residential hop adds 50–200ms versus a direct datacenter connection, and per-GB pricing replaces flat-rate bandwidth. For SERP tracking, price monitoring, and ad verification — where a single blocked request costs more than a thousand successful ones — residential is the rational default.

Worked Example: Rotating Sticky Sessions Concurrently

Here's a production-grade pattern: a pool of sticky session IDs, each pinned to a US residential IP, fanned out with Promise.all and guarded by an AbortController timeout. This example works in both Deno and Bun with minor adjustments noted in comments.

// concurrent_pool.ts
// Deno: deno run --allow-net concurrent_pool.ts
// Bun:  bun run concurrent_pool.ts

const GATE = 'gate.proxyhat.com';
const PORT = 8080;
const USER = 'user';
const PASS = 'pass';
const TARGET = 'https://api.ipify.org?format=json';

function buildProxyUrl(sessionId: string, country = 'US'): string {
  // Encode country + sticky session in the username
  const username = `${USER}-country-${country}-session-${sessionId}`;
  return `http://${encodeURIComponent(username)}:${PASS}@${GATE}:${PORT}`;
}

interface FetchResult {
  session: string;
  ip: string | null;
  error: string | null;
  ms: number;
}

async function fetchOne(sessionId: string, timeoutMs = 8000): Promise<FetchResult> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  const start = performance.now();

  // Deno path — build a client per session
  // (In Bun, replace with fetch(url, { proxy: buildProxyUrl(sessionId), signal }))
  let client: Deno.HttpClient | null = null;
  try {
    const proxyUrl = buildProxyUrl(sessionId);
    const opts: RequestInit & { client?: Deno.HttpClient } = {
      signal: controller.signal,
    };

    // Deno-specific branch
    // @ts-ignore: Deno global exists in Deno runtime
    if (typeof Deno !== 'undefined' && typeof Deno.createHttpClient === 'function') {
      client = Deno.createHttpClient({ proxy: { url: proxyUrl } });
      opts.client = client;
    } else {
      // Bun branch
      (opts as any).proxy = proxyUrl;
    }

    const res = await fetch(TARGET, opts);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const json = await res.json();
    return {
      session: sessionId,
      ip: json.ip ?? null,
      error: null,
      ms: Math.round(performance.now() - start),
    };
  } catch (err) {
    return {
      session: sessionId,
      ip: null,
      error: err instanceof Error ? err.message : String(err),
      ms: Math.round(performance.now() - start),
    };
  } finally {
    clearTimeout(timer);
    if (client) client.close();
  }
}

// Fan out 10 sticky sessions concurrently
const sessions = Array.from({ length: 10 }, (_, i) => `sess-${Date.now()}-${i}`);
const results = await Promise.all(sessions.map((s) => fetchOne(s)));

const ok = results.filter((r) => r.ip);
console.log(`Success: ${ok.length}/${results.length}`);
for (const r of results) {
  console.log(`${r.session}: ip=${r.ip} ms=${r.ms} err=${r.error}`);
}

This pattern gives you 10 distinct exit IPs in parallel, each stable for its session ID. For a real scraping job, map each session to a logical unit of work (a keyword, a product ID, a store) so retries stay on the same IP and don't trip rate limits.

Production Tips: Retries, Backoff, Custom CAs, Connection Reuse

Retries with Exponential Backoff

Residential proxies occasionally return 502/503 during IP rotation. Wrap your fetch in a retry loop with jittered backoff:

// retry.ts — runtime-agnostic

async function fetchWithRetry(
  url: string,
  proxyUrl: string,
  maxAttempts = 4,
  baseDelayMs = 500,
): Promise<Response> {
  let lastErr: unknown;
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 10000);
    try {
      let opts: any = { signal: controller.signal };
      // @ts-ignore
      if (typeof Deno !== 'undefined' && typeof Deno.createHttpClient === 'function') {
        const client = Deno.createHttpClient({ proxy: { url: proxyUrl } });
        opts.client = client;
        const res = await fetch(url, opts);
        if (res.status >= 500 && attempt < maxAttempts - 1) {
          client.close();
          throw new Error(`Server error ${res.status}`);
        }
        return res; // caller must close client if needed
      } else {
        opts.proxy = proxyUrl;
        const res = await fetch(url, opts);
        if (res.status >= 500 && attempt < maxAttempts - 1) {
          throw new Error(`Server error ${res.status}`);
        }
        return res;
      }
    } catch (err) {
      lastErr = err;
      const jitter = Math.random() * baseDelayMs;
      const delay = baseDelayMs * 2 ** attempt + jitter;
      await new Promise((r) => setTimeout(r, delay));
    } finally {
      clearTimeout(timer);
    }
  }
  throw lastErr;
}

// Usage
const res = await fetchWithRetry(
  'https://api.ipify.org?format=json',
  'http://user-country-US-session-s1:pass@gate.proxyhat.com:8080',
);
console.log(await res.json());

Custom CA Certificates in Deno

If you're scraping a site with a self-signed or private CA, Deno's createHttpClient accepts caCerts as an array of PEM strings:

// custom_ca.ts (Deno only)
const caPem = await Deno.readTextFile('./private-ca.pem');

const client = Deno.createHttpClient({
  proxy: { url: 'http://user-country-US:pass@gate.proxyhat.com:8080' },
  caCerts: [caPem], // array of PEM-encoded certs
});

const res = await fetch('https://internal.corp.example.com/data', {
  client,
});
console.log(await res.text());
client.close();

Bun currently relies on the system trust store; for custom CAs there, set NODE_EXTRA_CA_CERTS to a PEM file path before startup.

Connection Reuse

Creating a new Deno.createHttpClient per request defeats HTTP keep-alive. In Deno, create one client per sticky session and reuse it across the session's requests. In Bun, the runtime pools connections automatically when the same proxy URL is reused — keep the proxy string stable per session to benefit.

Side-by-Side: Raw fetch vs ProxyHat Node SDK

ProxyHat ships a Node.js SDK that runs under both Deno (via npm specifier) and Bun (native npm install). It wraps the raw proxy URL with convenience helpers for rotation, geo-selection, and metrics. Here's the same task both ways:

// sdk_vs_raw.ts
// Deno: deno run --allow-net --allow-env npm:@proxyhat/sdk sdk_vs_raw.ts
// Bun:  bun add @proxyhat/sdk && bun run sdk_vs_raw.ts

// --- RAW approach (Deno) ---
const rawClient = Deno.createHttpClient({
  proxy: {
    url: 'http://user-country-US-session-raw1:pass@gate.proxyhat.com:8080',
  },
});
const rawRes = await fetch('https://api.ipify.org?format=json', {
  client: rawClient,
});
console.log('Raw:', await rawRes.json());
rawClient.close();

// --- SDK approach (Bun or Deno) ---
import { ProxyHat } from '@proxyhat/sdk';

const ph = new ProxyHat({
  username: 'user',
  password: process.env.PROXYHAT_PASS!,
  gateway: 'gate.proxyhat.com',
  port: 8080,
});

const session = ph.session({ country: 'US', id: 'sdk1' });
const sdkRes = await fetch('https://api.ipify.org?format=json', {
  proxy: session.proxyUrl(), // Bun path; for Deno, wrap in createHttpClient
});
console.log('SDK:', await sdkRes.json());

The SDK shines when you need automatic session rotation, per-request country switching, or structured logging of egress IPs. For a single hardcoded request, raw fetch is lighter. See the ProxyHat docs for the full SDK API.

Comparison: Deno vs Bun Proxy Support

FeatureDenoBun
Proxy config locationDeno.createHttpClient({ proxy })fetch(url, { client })fetch(url, { proxy })
SOCKS5 supportYes, via socks5:// URLYes, via socks5:// URL
Custom CA certscaCerts array in clientSystem trust store / NODE_EXTRA_CA_CERTS
Env-var fallbackHTTP_PROXY / HTTPS_PROXYHTTP_PROXY / HTTPS_PROXY
Connection poolingPer-client; close to releaseAutomatic per proxy URL
ErgonomicsVerbose but explicitOne-liner

Proxy access is a tool, not a license to bypass terms of service. In the United States, the Computer Fraud and Abuse Act (CFAA) criminalizes unauthorized access to protected computers; courts have generally held that public-facing pages are not "unauthorized," but the line is fact-specific. In the EU, the GDPR governs personal data — scraping emails, names, or behavioral profiles without a lawful basis is illegal regardless of how you connect.

Practical guidelines:

  • Prefer official APIs. If a site offers an API with rate limits, use it. Scraping should be a last resort.
  • Respect robots.txt. It's not legally binding everywhere, but ignoring it signals bad faith and invites blocks.
  • Collect only public data. Behind-login content, paywalled articles, and private profiles are off-limits without explicit permission.
  • Rate-limit yourself. Even with residential proxies, hammering a target degrades service for real users and is a poor citizen move.
  • Honor takedown requests. If a site operator asks you to stop, stop.

For more on compliant scraping workflows, see our web scraping use case and SERP tracking guide.

ProxyHat Setup Quick Reference

All examples in this guide use ProxyHat's gateway at gate.proxyhat.com. HTTP lives on port 8080; SOCKS5 on 1080. Credentials and routing flags go in the username. Check available locations for country and city codes, and pricing for residential, mobile, and datacenter tiers.

Key Takeaways

  • fetch() ignores proxies by spec — Deno uses Deno.createHttpClient, Bun uses { proxy } in fetch options.
  • Encode geo-targeting and sticky sessions in the username: user-country-US-session-abc123.
  • Prefer per-client config over HTTP_PROXY env vars when mixing proxied and direct traffic.
  • Residential proxies raise success rates on high-block targets from <10% to >90%, at the cost of 50–200ms added latency.
  • Reuse clients per sticky session to keep HTTP keep-alive alive; close them when done.
  • Always prefer official APIs, respect robots.txt, and stay within CFAA and GDPR bounds.

FAQ

What is Using Proxies in Deno and Bun?

It refers to routing fetch() traffic through a proxy server in the Deno and Bun JavaScript runtimes. Because the WHATWG Fetch spec doesn't include proxy parameters, each runtime extends fetch differently: Deno via Deno.createHttpClient({ proxy }) passed as { client }, and Bun via a { proxy } option on fetch() directly. Both also support HTTP_PROXY and HTTPS_PROXY environment variables.

Why does Using Proxies in Deno and Bun matter for proxy users?

Without explicit proxy configuration, Deno and Bun send requests from your server's real IP, which is easily blocked by anti-bot systems. Configuring proxies lets you rotate residential exit IPs, geo-target requests to specific countries, and maintain sticky sessions so cookies and tokens stay valid. This is critical for SERP tracking, price monitoring, and any scraping workflow against protected targets.

Which proxy type works best for Using Proxies in Deno and Bun?

Residential proxies are the best default for scraping and automated data collection because their exit IPs come from real ISPs and blend in with organic traffic. Datacenter proxies are faster and cheaper but are flagged by most modern anti-bot vendors. Mobile proxies offer the highest trust score but at premium pricing. For internal health checks or low-risk targets, datacenter is fine; for anything behind Cloudflare or Akamai, use residential.

How do you avoid blocks when implementing Using Proxies in Deno and Bun?

Use sticky sessions so each logical job keeps the same exit IP, rotate session IDs across jobs to distribute load, set per-request timeouts with AbortController, and implement retries with exponential backoff for transient 502/503 errors. Combine this with realistic request rates (not hundreds per second from one session) and respect the target's robots.txt. Residential IPs plus disciplined rate limiting typically achieve over 90% success rates on high-block targets.

Does the ProxyHat Node SDK work in Deno and Bun?

Yes. The ProxyHat SDK is npm-compatible and runs under both Deno (using an npm specifier like npm:@proxyhat/sdk) and Bun (native bun add). It wraps the raw proxy URL with helpers for session rotation, geo-selection, and metrics. For a single hardcoded request, raw fetch with gate.proxyhat.com:8080 is lighter; for multi-session workflows, the SDK reduces boilerplate.

Frequently asked questions

What is Using Proxies in Deno and Bun?

It refers to routing fetch() traffic through a proxy server in the Deno and Bun JavaScript runtimes. Because the WHATWG Fetch spec doesn't include proxy parameters, each runtime extends fetch differently: Deno via Deno.createHttpClient({ proxy }) passed as { client }, and Bun via a { proxy } option on fetch() directly. Both also support HTTP_PROXY and HTTPS_PROXY environment variables.

Why does Using Proxies in Deno and Bun matter for proxy users?

Without explicit proxy configuration, Deno and Bun send requests from your server's real IP, which is easily blocked by anti-bot systems. Configuring proxies lets you rotate residential exit IPs, geo-target requests to specific countries, and maintain sticky sessions so cookies and tokens stay valid. This is critical for SERP tracking, price monitoring, and any scraping workflow against protected targets.

Which proxy type works best for Using Proxies in Deno and Bun?

Residential proxies are the best default for scraping and automated data collection because their exit IPs come from real ISPs and blend in with organic traffic. Datacenter proxies are faster and cheaper but are flagged by most modern anti-bot vendors. Mobile proxies offer the highest trust score but at premium pricing. For internal health checks or low-risk targets, datacenter is fine; for anything behind Cloudflare or Akamai, use residential.

How do you avoid blocks when implementing Using Proxies in Deno and Bun?

Use sticky sessions so each logical job keeps the same exit IP, rotate session IDs across jobs to distribute load, set per-request timeouts with AbortController, and implement retries with exponential backoff for transient 502/503 errors. Combine this with realistic request rates and respect the target's robots.txt. Residential IPs plus disciplined rate limiting typically achieve over 90% success rates on high-block targets.

Does the ProxyHat Node SDK work in Deno and Bun?

Yes. The ProxyHat SDK is npm-compatible and runs under both Deno (using an npm specifier like npm:@proxyhat/sdk) and Bun (native bun add). It wraps the raw proxy URL with helpers for session rotation, geo-selection, and metrics. For a single hardcoded request, raw fetch with gate.proxyhat.com:8080 is lighter; for multi-session workflows, the SDK reduces boilerplate.

Verify your proxy setup in seconds

Free proxy checker — confirm your IPs are fast, anonymous and unblocked.

Check proxies free
← Back to Blog