WebSocket Troubleshooting

Every WebSocket connection failure we've seen reported, with a recipe to fix it. Start from the close code if you have one; otherwise scan the symptom list.

Reference Quickstart Examples Player props Edge alerts Troubleshooting

Step 0 — Verify with curl first

Before debugging in code, run a raw curl handshake. If this works and your code doesn't, the problem is in your client, not the server.

curl --include --no-buffer \
  --header "Connection: Upgrade" \
  --header "Upgrade: websocket" \
  --header "Sec-WebSocket-Key: $(head -c 16 /dev/urandom | base64)" \
  --header "Sec-WebSocket-Version: 13" \
  --header "X-API-Key: $PARLAY_API_KEY" \
  "https://parlay-api.com/v1/ws/odds/baseball_mlb"

Expected first line: HTTP/1.1 101 Switching Protocols. If you get a 4xx instead, see the corresponding row below.

Close-code reference (with fix)

Close code 1008

"Invalid API key"

Cause. The key you sent doesn't exist, was revoked, or was rolled.

Fix.

Close code 4001

"WebSocket requires Business tier or above"

Cause. Your key is on Free, Starter, or Pro tier. WebSocket is gated to Business / Enterprise / Scale.

Fix.

Close code 4002

"Concurrent connection cap reached"

Cause. Too many WebSocket / SSE connections open under one API key.

Fix.

Close code 4003

"Missing apiKey"

Cause. No API key found in any of the three accepted forms (query param, X-API-Key header, Sec-WebSocket-Protocol subprotocol).

Fix.

Close code 1006

"Abnormal closure"

Cause. Network-level disconnect: TCP RST, NAT timeout, mobile carrier handoff. This is normal in any long-lived WebSocket setup.

Fix.

Close code 1001

"Going away"

Cause. We're restarting (deploys, planned maintenance, worker recycle).

Fix. Wait 5 s, reconnect. Deploys complete in 30–60 s; you'll be back online by your second retry. Subscribe to status to be notified of planned windows.

Close code 1011

"Server error"

Cause. Either a server-side exception, or a ping/pong timeout (we send transport pings; if the client doesn't respond for 30 s we treat it as dead).

Fix.

HTTP errors (before WebSocket upgrade)

403 Forbidden on handshake

Almost always means the API key wasn't recognized at the WAF / edge layer. Check:

426 Upgrade Required

You hit a non-WebSocket endpoint with WebSocket headers. Double-check the URL path — common typo is /v1/odds/... (REST) vs /v1/ws/odds/... (WebSocket).

404 Not Found

Same — URL typo. The four valid URL prefixes are /ws/odds/, /ws/live/, /v1/ws/odds/, /v1/ws/live/. Anything else 404s.

"It connects, but I get no odds_update frames"

This is usually fine and not a bug. The collector only pushes when prices change. In quiet windows you can go 60+ seconds with nothing on the wire.

Quick check:

Proxy, VPN, and corporate-network issues

Symptoms: connection establishes fine, but frames arrive in irregular bursts (5–30 s gaps then a flood) instead of streaming smoothly.

Likely causes (in order of frequency):

  1. HTTP proxy without WebSocket support. Older squid / forward-proxy setups don't pass Upgrade through cleanly. Tunnel via a corporate-approved HTTPS path if available, or fall back to SSE (which is plain HTTP).
  2. Inspection proxy holding frames. Some enterprise DLP / TLS-inspection appliances buffer WebSocket payloads to scan them. Add parlay-api.com to your inspection bypass list.
  3. NAT timeout shorter than our 30 s heartbeat. If your NAT kills idle TCP sockets at 20 s, you'll see disconnect-reconnect-disconnect. Configure NAT timeout > 60 s, or switch to SSE.
  4. VPN with split-tunneling. Make sure parlay-api.com is in the tunnel set if you're behind a corporate VPN.

AWS Lambda / serverless gotchas

Don't open WebSocket connections inside Lambda function handlers — they get torn down when the function returns. Two viable patterns:

Browser quirks

Safari closes the socket on tab background

Known Safari behavior on iOS / macOS — it pauses background tabs aggressively. Your client should reconnect on tab visibility change:

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "visible" && ws.readyState !== WebSocket.OPEN) {
    ws = new WebSocket(URL);
    // re-wire onmessage / onclose / onerror
  }
});

Chrome devtools shows "Pending" forever

That's the WebSocket frame display, not an error. Click the WS request → Messages tab to see actual frames. The "Status: Pending" just means the connection is open (and pending close).

CORS errors

WebSocket bypasses CORS by spec — you should never see a CORS error on the upgrade itself. If you do, it's actually the page-load that's CORS-blocked, not the WebSocket. Make sure the JS file calling new WebSocket() loaded successfully first.

How to file a useful support ticket

If none of the above helps, please include the following at /support so we don't have to ping-pong:

Median response time on Business+ tickets is under an hour during US business hours.