Railway production TCP timeout to bseindices.com:443
iammeghana007-stack
HOBBYOP

a month ago

Railway production outbound TCP connection times out to bseindices.com:443

I’m troubleshooting a production networking issue on Railway.

My service can resolve:

bseindices.com -> 43.228.177.148

However, repeated outbound TCP connections from the Railway production service to:

43.228.177.148:443

do not complete and eventually time out.

Application diagnostics repeatedly show:

DNS_OK | host=bseindices.com | ips=('43.228.177.148',)

followed by:

TCP_TLS_FAILED | ip=43.228.177.148 | error_type=TimeoutError | error=timed out

Railway network-flow captures repeatedly show outbound TCP flows to 43.228.177.148:443 with:

  • direction=egress
  • l4Protocol=tcp
  • flowState=partial
  • dropCause=null
  • l4LatencyMs=0.0
  • packet counts between 1 and 5
  • no completed TCP flow observed in the captured records

The same endpoint was independently tested from a GitHub Codespace and works normally over IPv4:

  • TCP connection succeeds
  • TLS 1.3 succeeds
  • HTTP 200 OK

Measured control request:

HTTP=200 CONNECT=0.005667 TLS=0.032739 TOTAL=0.057378

This appears to be a connectivity-path difference between the Railway production environment and the external endpoint, rather than an HTTP/application parsing problem.

The application is intentionally fail-closed when this authoritative reference source is unavailable, so no trading bypass or workaround is being requested.

Could anyone help determine whether this pattern indicates an outbound routing/NAT/egress issue, an upstream connectivity issue, or another Railway networking condition?

In particular, what does a repeated partial TCP flow with dropCause=null and no completed connection indicate, and what Railway-side diagnostic should be used next?

I can provide deployment/service identifiers and additional logs in the thread if needed.

$10 Bounty

2 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 • 26 days ago


Status changed to Solved iammeghana007-stack • 26 days ago


iammeghana007-stack
HOBBYOP

a month ago

what is the solution pls tell


Status changed to Open 0x5b62656e5d • 26 days ago


hdnexus
PROTop 10% Contributor

10 days ago

Reading the flow records

Three fields tell the whole story:

dropCause=null — Railway's network did not drop the packet. If egress policy, a quota, or the NAT had killed it, this field names the reason. It's empty, so the packet was forwarded.

flowState=partial — the connection was never established. A SYN went out; no SYN-ACK came back. The flow sits half-open until it ages out.

l4LatencyMs=0.0 — latency is measured from the handshake round-trip. Zero here isn't "fast," it's "never had two packets to measure between."

1–5 packets per flow — that's the Linux SYN retry pattern (tcp_syn_retries, ~5 attempts over ~2 minutes), then TimeoutError.

Compare this to the failure modes it isn't: a NAT exhaustion or egress-policy problem fills in dropCause. A TLS or SNI mismatch completes the TCP handshake first, so you'd see latency and a cert error, not a connect timeout. An upstream routing blackhole typically gives ICMP unreachables somewhere. A silent DROP rule on the destination firewall is the one failure mode that produces exactly your signature: packets out, nothing back, no error anywhere.

Why your Codespace control test succeeded

That test is the most useful data point in your post, and it points away from Railway rather than toward it. Same destination IP, same port, same protocol, different source ASN — and it handshakes in 5ms. The path to 43.228.177.148 is up. The variable that changed is who's asking.

bseindices.com is BSE (Bombay Stock Exchange) index infrastructure. Exchange and exchange-adjacent endpoints very commonly geofence or allowlist by source IP, and cloud-provider egress ranges — Railway's shared NAT pools included — are a standard thing to block, because that's where scrapers come from. GitHub Codespaces egresses from Azure ranges that are far less likely to be on such a list.

Confirm it in three commands

Run these from a shell inside the deployed service (railway ssh, or a temporary start command), not from your laptop:

1. What IP does BSE actually see?

curl -s https://api.ipify.org; echo

2. Where does the path die? (TCP traceroute to 443, not ICMP)

traceroute -T -p 443 -n 43.228.177.148

3. Is the destination the variable, or the region?

curl -sS -o /dev/null -w 'nseindia=%{http_code} connect=%{time_connect}\n' \

--connect-timeout 10 https://www.nseindia.com/

curl -sS -o /dev/null -w 'cloudflare=%{http_code} connect=%{time_connect}\n' \

--connect-timeout 10 https://1.1.1.1/

How to read the results:

Traceroute dies at a consistent hop inside the destination's network (after the transit providers, in AS-something-Indian) → their filter. Confirmed.

Traceroute dies at the first or second hop out of Railway → genuinely a Railway egress problem, and now you have a specific hop to escalate with.

Another Indian endpoint connects fine while BSE times out → the region and the path are fine, the endpoint is filtering you.

Everything outbound times out → different problem entirely, open a separate thread.

Also worth checking whether the block is IP-based or ASN-wide: redeploy the service to a different Railway region and retest. If a different region's egress IP connects, you have an IP-level block and an allowlist request will solve it. If every region times out, they're blocking the whole ASN and no allowlist ask will help.

Fixes, cheapest first

Static egress IP + allowlist request. Railway supports a dedicated static egress IP. Enable it, then contact BSE with that single IP and ask for it to be permitted. This is the only fix that keeps everything on Railway, and it's the one to try first — but it depends on BSE answering, and exchanges are slow about this.

Proxy the one call. Stand up a small box in an Indian region (or any provider whose IPs they accept), run a minimal forward proxy on it, and point only this fetch at it. Your Railway service stays where it is. This is what most people end up doing, because it doesn't require the exchange to cooperate.

Use a licensed data feed. If this is production trading infrastructure, scraping bseindices.com directly is fragile regardless of the networking — one WAF rule change and you're down. BSE licenses index data properly, and a paid feed won't geofence you.

One thing to rule out before you spend money on any of them: confirm you're not hitting an IPv6 path issue. Your diagnostic resolved only an A record, so this is probably moot, but if the service ever gets a AAAA back and tries v6 first, a provider without v6 transit to that destination produces the same timeout. Force v4 (curl -4, or family=socket.AF_INET in Python) and see if anything changes.

On the fail-closed design — that's the right call, and it's worth keeping even after you fix the path. Whatever you land on in the list above, the proxy or the static IP becomes a new single point of failure, so the fail-closed behavior should stay rather than being relaxed once connectivity works.


Welcome!

Sign in to your Railway account to join the conversation.

Loading...