Gotchas and Troubleshooting
Common pitfalls, limitations, and solutions for TCP Sockets in Cloudflare Workers.
Overview
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
- Log connection details:
const info = await socket.opened; console.log(info.remoteAddress); - Test with public services first: Use tcpbin.com:4242 echo server
- Verify Tunnel:
cloudflared tunnel info <name>andcloudflared tunnel route ip list
Related
- Hyperdrive - Database connections
- Smart Placement - Latency optimization
- Tunnel Troubleshooting