Modern JavaScript runtimes like Deno and Bun promise a clean, standards-compliant fetch() out of the box — but when you try to route that fetch through a residential proxy, you'll quickly discover that the spec says nothing about proxies. Using proxies in Deno and Bun requires runtime-specific configuration, and getting it wrong means silent leaks of your real IP, failed geo-targeting, or connection timeouts on the very targets you're trying to reach.
This guide walks through the exact APIs each runtime exposes, how to encode geo-targeting and sticky sessions in your ProxyHat username, when to prefer environment variables over per-client config, and how to build a production-grade concurrent scraper with retries, backoff, and timeouts. Every example uses gate.proxyhat.com on port 8080 (HTTP) or 1080 (SOCKS5).
Why Native fetch() Ignores Proxies — and How Each Runtime Solves It
The WHATWG Fetch standard defines fetch() as a function that takes a URL and an options bag — and nowhere in that options bag is there a proxy field. Browsers handle proxies at the OS or browser level, outside the JavaScript layer. Node.js, Deno, and Bun each inherit this gap and solve it differently.
Deno provides Deno.createHttpClient(), which returns a custom HTTP client you pass into fetch() via the client option. Bun takes a simpler approach: it accepts a proxy string directly in the fetch() options. Both approaches bypass the standard options bag, which means your proxy-aware code is runtime-specific — but the patterns are clean enough to abstract behind a shared interface.
Deno: Deno.createHttpClient with proxy and basicAuth
Deno's createHttpClient API accepts a proxy object with url and optional basicAuth fields. You then pass the resulting client to every fetch() call that should use it.
// deno_proxy.ts
// Run: deno run --allow-net --unsafely-ignore-certificate-errors deno_proxy.ts
const proxyUrl = "http://gate.proxyhat.com:8080";
const username = "user-country-US-session-abc123";
const password = "pass";
const client = Deno.createHttpClient({
proxy: {
url: proxyUrl,
basicAuth: {
username,
password,
},
},
});
const res = await fetch("https://httpbin.org/ip", { client });
const body = await res.json();
console.log(body);
// { origin: "198.51.100.42" } — a ProxyHat residential IP in the US
Note the --allow-net flag — Deno's permission system requires explicit network access. The --unsafely-ignore-certificate-errors flag is only needed if your proxy uses a self-signed TLS certificate; ProxyHat's gateway uses standard TLS so you can omit it in production.
Bun: One-Line fetch with proxy
Bun's approach is more ergonomic — you pass a proxy URL string (including credentials) directly in the fetch options, as described in the Bun fetch documentation.
// bun_proxy.ts
// Run: bun run bun_proxy.ts
const proxyUrl =
"http://user-country-US-session-abc123:pass@gate.proxyhat.com:8080";
const res = await fetch("https://httpbin.org/ip", {
proxy: proxyUrl,
});
const body = await res.json();
console.log(body);
// { origin: "198.51.100.42" }
Bun parses the user:pass from the URL automatically. No extra client object, no permission flags — just one string. This is the simplest proxy fetch JavaScript developers can write today.
Encoding Geo-Targeting and Sticky Sessions in the Username
ProxyHat encodes routing instructions in the proxy username rather than in query parameters or headers. This keeps your code portable across any HTTP client — the proxy URL is the entire configuration surface.
| Flag | Example Username | Effect |
|---|---|---|
| Country | user-country-DE |
Exit through a German residential IP |
| Country + City | user-country-DE-city-berlin |
Exit through a Berlin residential IP |
| Sticky session | user-session-abc123 |
Pin all requests to one IP for the session lifetime |
| Combined | user-country-US-session-abc123 |
US IP, sticky for this session ID |
Sticky sessions are essential when a target site uses CSRF tokens or multi-step flows that tie an IP to a cookie jar. Rotate the session ID when you want a new IP; keep it stable when you need to maintain login state across requests.
SOCKS5 on Port 1080
Some targets block HTTP CONNECT proxies but allow SOCKS5. ProxyHat exposes SOCKS5 on port 1080. Both Deno and Bun support SOCKS5 proxy URLs natively:
// bun_socks5.ts
const socksProxy =
"socks5://user-country-DE-session-xyz789:pass@gate.proxyhat.com:1080";
const res = await fetch("https://httpbin.org/ip", {
proxy: socksProxy,
});
console.log(await res.json());
// deno_socks5.ts
const client = Deno.createHttpClient({
proxy: {
url: "socks5://gate.proxyhat.com:1080",
basicAuth: {
username: "user-country-DE-session-xyz789",
password: "pass",
},
},
});
const res = await fetch("https://httpbin.org/ip", { client });
console.log(await res.json());
HTTP_PROXY / HTTPS_PROXY Environment Variables — and When to Avoid Them
Both Deno and Bun respect the standard HTTP_PROXY and HTTPS_PROXY environment variables. This is convenient for quick scripts or when you want every outbound request to use the same proxy without modifying call sites:
# Set env vars, then any fetch() uses the proxy automatically
export HTTPS_PROXY="http://user-country-US:pass@gate.proxyhat.com:8080"
bun run scraper.ts
# or
deno run --allow-net --allow-env scraper.ts
However, environment variables are the wrong choice when you need per-request routing. If one request needs a US residential IP and another needs a German datacenter IP, a single HTTPS_PROXY can't express that. Per-client configuration (Deno) or per-call proxy options (Bun) give you the granularity you need for multi-region scraping, A/B testing proxy types, or mixing residential and datacenter pools.
As a rule: use env vars for development and debugging, use explicit config for production scraping where geo-targeting and session control matter. See our proxy locations page for the full list of supported country and city codes.
Why Residential Proxies for High-Block Targets — A Concurrent Example
Datacenter IPs are fast and cheap, but they're also trivially detectable. Major scraping targets — e-commerce sites, search engines, social platforms — maintain ASN blocklists that flag entire datacenter ranges. Residential proxies exit through real ISP-assigned IPs, making them far harder to distinguish from organic traffic.
The trade-off is latency. A residential hop typically adds 50–200ms compared to a direct datacenter connection. For high-throughput scraping, you compensate with concurrency: run 50–100 concurrent sticky sessions and the aggregate throughput rivals a datacenter pool with a far lower block rate.
Here's a worked example in Bun that rotates a pool of sticky sessions across concurrent requests, with an AbortController timeout on each:
// bun_concurrent.ts
// Run: bun run bun_concurrent.ts
const TARGET = "https://httpbin.org/anything";
const PROXY_BASE = "http://gate.proxyhat.com:8080";
const USERNAME = "user-country-US";
const PASSWORD = "pass";
const CONCURRENCY = 50;
const TIMEOUT_MS = 10000;
function buildProxyUrl(sessionId: string): string {
const creds = `${USERNAME}-session-${sessionId}:${PASSWORD}`;
return `${PROXY_BASE.replace("://", `://${creds}@`)}`;
}
async function fetchWithTimeout(url: string, proxy: string): Promise<number> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
try {
const res = await fetch(url, {
proxy,
signal: controller.signal,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.status;
} finally {
clearTimeout(timer);
}
}
const sessions = Array.from({ length: CONCURRENCY }, (_, i) =>
`sess-${Date.now()}-${i}`
);
const results = await Promise.allSettled(
sessions.map((sid) => fetchWithTimeout(TARGET, buildProxyUrl(sid))),
);
const ok = results.filter((r) => r.status === "fulfilled").length;
console.log(`${ok}/${CONCURRENCY} requests succeeded`);
Each session ID gets its own sticky residential IP. Using Promise.allSettled instead of Promise.all ensures one failure doesn't reject the entire batch — you get per-request success/failure visibility, which is critical for monitoring success rates in production.
Production Tips: Retries, Custom CAs, and Connection Reuse
Retries with Exponential Backoff
Residential proxies occasionally return 502 or 503 when an upstream IP rotates or a carrier connection drops. A simple retry with exponential backoff dramatically improves effective success rates:
// deno_retry.ts
// Run: deno run --allow-net deno_retry.ts
function buildClient(sessionId: string) {
return Deno.createHttpClient({
proxy: {
url: "http://gate.proxyhat.com:8080",
basicAuth: {
username: `user-country-US-session-${sessionId}`,
password: "pass",
},
},
});
}
async function fetchWithRetry(
url: string,
sessionId: string,
maxRetries = 3,
): Promise<Response> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const client = buildClient(sessionId);
try {
const res = await fetch(url, { client });
if (res.ok) return res;
if (res.status >= 500 && attempt < maxRetries) {
await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
continue;
}
return res;
} catch (err) {
if (attempt === maxRetries) throw err;
await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
}
}
throw new Error("unreachable");
}
const res = await fetchWithRetry("https://httpbin.org/ip", "abc123");
console.log(await res.json());
Rebuilding the client per attempt ensures a fresh connection — if the proxy's upstream IP changed, a stale connection pool could route to the old (now dead) IP.
Custom CA Certificates in Deno
If your target uses a private CA (common in internal tooling or enterprise environments), pass the certificate via caCerts:
// deno_custom_ca.ts
const caCert = await Deno.readTextFile("./custom-ca.pem");
const client = Deno.createHttpClient({
proxy: {
url: "http://gate.proxyhat.com:8080",
basicAuth: { username: "user-country-US", password: "pass" },
},
caCerts: [caCert],
});
const res = await fetch("https://internal.example.com/api", { client });
console.log(res.status);
Side-by-Side with the ProxyHat Node SDK
Both Deno and Bun can run npm packages via their respective compatibility layers. The ProxyHat Node SDK works under both runtimes and abstracts away the proxy URL construction, rotation, and session management:
// proxyhat_sdk.ts
// Run: bun run proxyhat_sdk.ts or deno run --allow-net --allow-env proxyhat_sdk.ts
import { ProxyHat } from "@proxyhat/node";
const ph = new ProxyHat({
username: "user",
password: "pass",
// Optional: default country for all sessions
country: "US",
});
// Get a sticky session URL
const proxyUrl = ph.getProxyUrl({
country: "DE",
city: "berlin",
session: "order-flow-42",
});
// Works with Bun's fetch
const res = await fetch("https://httpbin.org/ip", {
proxy: proxyUrl,
});
console.log(await res.json());
// Or use the SDK's built-in fetch wrapper (handles retries + rotation)
const data = await ph.fetch("https://httpbin.org/ip", {
session: "order-flow-42",
timeout: 10000,
retries: 3,
});
console.log(await data.json());
The SDK is useful when you want a single dependency that handles URL construction, session pooling, and retry logic. For maximum control — custom TLS, fine-grained client reuse, or non-standard proxy protocols — the raw runtime APIs shown above are the better choice. Check our pricing page for plan details and session limits.
Ethical Scraping: Public Data, CFAA, GDPR, and APIs First
Before you point a concurrent scraper at any target, consider the legal and ethical landscape:
- Public data only. In the US, the Computer Fraud and Abuse Act (CFAA) has been narrowed by the Van Buren v. United States (2021) ruling, but accessing data behind authentication or in violation of a site's Terms of Service still carries legal risk.
- GDPR in the EU. Scraping personal data (names, emails, user profiles) from EU-based services may trigger GDPR processing obligations, even if the data is publicly visible.
- Use official APIs first. Many platforms — from search engines to e-commerce sites — offer structured APIs with higher rate limits and no legal ambiguity. A proxy is a fallback, not a first resort.
- Respect
robots.txt. It's not legally binding in all jurisdictions, but it signals the site owner's preferences and ignoring it weakens any good-faith defense.
For legitimate use cases — SERP tracking, price monitoring, academic research — residential proxies are a tool for reliability, not for circumventing access controls. Full API documentation is available at docs.proxyhat.com.
Key Takeaways
- Deno uses
Deno.createHttpClient({ proxy })passed as{ client }tofetch()— explicit, typed, and supports custom CAs.- Bun uses
fetch(url, { proxy: string })— the simplest one-liner in the JS ecosystem.- Geo-targeting and sticky sessions are encoded in the username (
user-country-US-session-abc123), not in headers or query params.- SOCKS5 is available on port
1080for targets that block HTTP CONNECT proxies.- Use env vars for dev/debugging; use per-client or per-call config for production multi-region scraping.
- Concurrency + retries compensate for residential latency — 50 concurrent sticky sessions with backoff can outperform a datacenter pool on success rate.
- Ethics first: public data only, official APIs when available, respect robots.txt and ToS.
Frequently Asked Questions
What is Using Proxies in Deno and Bun?
It refers to configuring Deno and Bun — two modern JavaScript/TypeScript runtimes — to route HTTP requests through a proxy server. Because the WHATWG fetch standard doesn't define a proxy option, each runtime provides its own mechanism: Deno uses Deno.createHttpClient({ proxy }) passed to fetch() as { client }, while Bun accepts a proxy string directly in the fetch options bag.
Why does Using Proxies in Deno and Bun matter for proxy users?
Without explicit proxy configuration, Deno and Bun will send requests from your real IP address, bypassing any proxy you intended to use. This matters for scraping, SERP tracking, and geo-targeted requests where IP reputation and location directly affect success rates. Properly configured proxies also enable sticky sessions and country-level routing that are impossible with a bare fetch call.
Which proxy type works best for Using Proxies in Deno and Bun?
Residential proxies are best for high-block targets like search engines and e-commerce sites, because they exit through real ISP-assigned IPs that are harder to detect. Datacenter proxies are faster and cheaper but easily blocklisted. For Deno and Bun specifically, HTTP proxies on port 8080 work with both runtimes' native fetch, while SOCKS5 on port 1080 is useful for targets that block HTTP CONNECT proxies.
How do you avoid blocks when implementing Using Proxies in Deno and Bun?
Use residential proxies with sticky sessions encoded in the username, rotate session IDs across concurrent requests, implement exponential backoff retries for 5xx errors, and set per-request timeouts with AbortController. Keep concurrency reasonable (50–100 sessions), respect robots.txt, and prefer official APIs when available. Avoid hammering a single endpoint with one IP — distribute load across multiple sticky sessions and geo-locations.
Can I use the same proxy code in both Deno and Bun?
The fetch call itself is shared, but the proxy configuration differs. Deno requires a Deno.createHttpClient object passed as { client }, while Bun takes a proxy string in the options. You can abstract this behind a shared interface or use the ProxyHat Node SDK, which runs under both runtimes and normalizes the proxy URL construction and session management.






