Files
dotfiles/.agents/skills/cloudflare-deploy/references/workers-vpc/gotchas.md
2026-03-17 16:53:22 -07:00

4.4 KiB

Gotchas and Troubleshooting

Common pitfalls, limitations, and solutions for TCP Sockets in Cloudflare Workers.

Platform Limits

Connection Limits

Limit Value
Max concurrent sockets per request 6 (hard limit)
Socket lifetime Request duration
Connection timeout Platform-dependent, no setting

Problem: Exceeding 6 connections throws error

Solution: Process in batches of 6

for (let i = 0; i < hosts.length; i += 6) {
  const batch = hosts.slice(i, i + 6).map(h => connect({ hostname: h, port: 443 }));
  await Promise.all(batch.map(async s => { /* use */ await s.close(); }));
}

Blocked Destinations

Cloudflare IPs (1.1.1.1), localhost (127.0.0.1), port 25 (SMTP), Worker's own URL blocked for security.

Solution: Use public IPs or Tunnel hostnames: connect({ hostname: "db.internal.company.net", port: 5432 })

Scope Requirements

Problem: Sockets created in global scope fail

Cause: Sockets tied to request lifecycle

Solution: Create inside handler: export default { async fetch() { const socket = connect(...); } }

Common Errors

Error: "proxy request failed"

Causes: Blocked destination (Cloudflare IP, localhost, port 25), DNS failure, network unreachable

Solution: Validate destinations, use Tunnel hostnames, catch errors with try/catch

Error: "TCP Loop detected"

Cause: Worker connecting to itself

Solution: Connect to external service, not Worker's own hostname

Error: "Port 25 prohibited"

Cause: SMTP port blocked

Solution: Use Email Workers API for email

Error: "socket is not open"

Cause: Read/write after close

Solution: Always use try/finally to ensure proper closure order

Error: Connection timeout

Cause: No built-in timeout

Solution: Use Promise.race():

const socket = connect(addr, opts);
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000));
await Promise.race([socket.opened, timeout]);

TLS/SSL Issues

StartTLS Timing

Problem: Calling startTls() too early

Solution: Send protocol-specific STARTTLS command, wait for server OK, then call socket.startTls()

Certificate Validation

Problem: Self-signed certs fail

Solution: Use proper certs or Tunnel (handles TLS termination)

Performance Issues

Not Using Connection Pooling

Problem: New connection overhead per request

Solution: Use Hyperdrive for databases (built-in pooling)

Not Using Smart Placement

Problem: High latency to backend

Solution: Enable: { "placement": { "mode": "smart" } } in wrangler.jsonc

Forgetting to Close Sockets

Problem: Resource leaks

Solution: Always use try/finally:

const socket = connect({ hostname: "api.internal", port: 443 });
try {
  // Use socket
} finally {
  await socket.close();
}

Data Handling Issues

Assuming Single Read Gets All Data

Problem: Only reading once may miss chunked data

Solution: Loop reader.read() until done === true (see patterns.md)

Text Encoding Issues

Problem: Using wrong encoding

Solution: Specify encoding: new TextDecoder('iso-8859-1').decode(data)

Security Issues

SSRF Vulnerability

Problem: User-controlled destinations allow access to internal services

Solution: Validate against strict allowlist:

const ALLOWED = ['api1.internal.net', 'api2.internal.net'];
const host = new URL(req.url).searchParams.get('host');
if (!host || !ALLOWED.includes(host)) return new Response('Forbidden', { status: 403 });

When to Use Alternatives

Use Case Alternative Reason
PostgreSQL/MySQL Hyperdrive Connection pooling, caching
HTTP/HTTPS fetch() Simpler, built-in
HTTP with SSRF protection VPC Services (beta 2025+) Declarative bindings

Debugging Tips

  1. Log connection details: const info = await socket.opened; console.log(info.remoteAddress);
  2. Test with public services first: Use tcpbin.com:4242 echo server
  3. Verify Tunnel: cloudflared tunnel info <name> and cloudflared tunnel route ip list