WebSocket handshake succeeds but Envoy proxy closes connection with code 1006 after ~72ms consistently
m1gu3l4ngel
HOBBYOP

a month ago

Hey folks and Railway team,

I'm hitting a strange WebSocket issue in one of my Railway services and could use a second pair of eyes. Every WS connection opens successfully, then dies about 72 milliseconds later with code=1006, no error surfaced anywhere. Reproducible with a minimal browser-only test that doesn't touch my app code, so I'm pretty sure it's not a code bug in my project — but I want to check if anyone here has seen this pattern before I lose more hair.

Setup

  • Runtime: Node.js custom server wrapping Next.js 16
  • WebSocket lib: ws@8.21.0 with noServer: true + server.on('upgrade', ...)
  • Auth in handshake: next-auth v5 JWT via getToken() (pure crypto verify, no DB call, resolves in ~3-15ms consistently)
  • Deploy: Railway default .up.railway.app domain

Symptom

Every WebSocket connection to /ws:

  1. HTTP upgrade succeeds
  2. Server-side wss.emit('connection') runs correctly
  3. ~65ms later the socket is closed with code=1006, empty reason
  4. The server does NOT initiate the close (no ws.close(), no ws.terminate())
  5. The ws.on('error') handler never fires on either side

Isolated reproduction (no app code)

In DevTools Console on any authenticated page of my app:

const ws = new WebSocket('wss://YOUR-APP.up.railway.app/ws');

ws.onopen = () => console.log('OPEN', new Date().toISOString());

ws.onclose = (e) => console.log('CLOSE code=', e.code, 'at', new Date().toISOString());

setTimeout(() => console.log('30s later state:', ws.readyState), 30000);

Result on every attempt:

  • OPEN at T
  • CLOSE code= 1006 at T + ~2ms (client sees it)
  • readyState: 3 at 30s

Railway edge logs (representative entry)

Every /ws request logs like this:

{

  "method": "GET",

  "path": "/ws",

  "httpStatus": 0,

  "responseDetails": "connection upgraded to WebSocket",

  "totalDuration": 72,

  "upstreamRqDuration": 72,

  "txBytes": 0,

  "rxBytes": 3834,

  "upstreamErrors": ""

}

Consistently ~72ms total duration, txBytes: 0, upstreamErrors empty. No indication of what caused the close.

Server-side instrumentation (during handshake)

I added logs to the upgrade handler to trace where things go wrong:

upgrade received path=/ws

getToken done in 3ms hasToken=true isValid=true

calling handleUpgrade userId=1

handleUpgrade complete in 4ms — emitting connection

connection handler entered userId=1

CLOSE code=1006 reason="(empty)" lived=63ms userId=1

Everything looks healthy on the upstream side. The socket just... dies ~65ms after the connection handler runs.

What I tried (all no-op)

  1. Application-level heartbeat client-side (never reaches the 25s mark)
  2. Protocol-level ping/pong from server every 15s with isAlive tracking
  3. Immediate ws.ping() on connection open (in case the proxy wants bytes right away)
  4. Immediate ws.send({type: "HELLO"}) on connection open (data frame instead of ping)
  5. TCP tuning on the raw socket before handleUpgrade: setKeepAlive(true, 15000), setNoDelay(true), setTimeout(0)
  6. Completely stripping Sentry from the code path (no captureException, no captureRequestError, withSentryConfig disabled, defaultIntegrations: false, SDK disabled)
  7. Reverting the WebSocket server + client hook + custom server to a byte-for-byte copy of another Railway project of mine that works fine (see comparison below)

Nothing moves the needle. Every attempt closes at the same ~72ms.

Weird part — another Railway project works fine

I have a second Railway service running practically the same architecture (Next.js custom server, ws lib, noServer: true, same JWT-in-handshake pattern). WebSockets in that service stay alive for hours, real-time works flawlessly. When I copy the WS code from the working project byte-for-byte into this broken one, it still fails. So the code path is functionally identical between the two, but only one service exhibits this behavior.

The only variable I can't rule out is something at the Railway service level.

Questions for the community

  • Has anyone else seen WebSockets close with code=1006 at a suspiciously consistent ~65-75ms after handshake, on a Railway service?
  • Any known scenarios where txBytes: 0 + upstreamErrors: "" in the edge log points to a specific cause (upstream health check, edge idle timeout on WS, region-specific behavior)?
  • Any way to get more verbose edge/proxy logs than what shows up in the standard log stream?

Happy to share more instrumentation output or reproduce on demand. Thanks in advance!

Solved$10 Bounty

5 Replies

Railway
BOT

a month ago

This thread has been opened as a bounty so the community can help solve it.

Status changed to Open Railway about 1 month ago


a month ago

Try redeploy your service or change the deployment region, is this deployed to US East?


mayori

Try redeploy your service or change the deployment region, is this deployed to US East?

m1gu3l4ngel
HOBBYOP

a month ago

Yes, it is US East. I’ll try re-deploying. Thanks and I’ll comment back if this work or not 👍🏼


mayori

Try redeploy your service or change the deployment region, is this deployed to US East?

m1gu3l4ngel
HOBBYOP

a month ago

I moved it to West and still not working the sockets. Not sure what it could be. Frustrating


m1gu3l4ngel
HOBBYOP

a month ago

Adding the x-railway-request-id: pVKv8gtkTbeqEHVGg4a9AQ ... Still battling with this issue !


m1gu3l4ngel
HOBBYOP

a month ago

screenshot20260720115233122312331232.png


Status changed to Solved m1gu3l4ngel 29 days ago


Welcome!

Sign in to your Railway account to join the conversation.

Loading...