AgileSoftLabs Logo
NirmalrajBy Nirmalraj
Published: July 2026|Updated: July 2026|Reading Time: 16 minutes

Share:

React Native WebSocket Not Connecting: Complete Debug and Fix Guide (2026)

Published: July 23, 2026 | Reading Time: 16 minutes 

About the Author

Nirmalraj R is a Full-Stack Developer at AgileSoftLabs, specialising in MERN Stack and mobile development, focused on building dynamic, scalable web and mobile applications.

Key Takeaways

  • WebSocket connections in React Native fail silently in ways that do not happen in browsers — a WebSocket that works perfectly in a browser test refuses to connect on an Android emulator; the onclose event fires with code 1006 and no further information about the actual failure.
  • The most common Android emulator failure is using localhost instead of 10.0.2.2 — the Android emulator routes host machine traffic through 10.0.2.2, not 127.0.0.1, which is the single most frequently encountered fix across all environment setup issues.
  • Close code 1006 means the TCP connection died without a proper WebSocket close handshake — always a network-layer issue (server timeout, mobile network switch, Android Doze mode) rather than an application logic error.
  • The #1 Socket.io fix for React Native is forcing transports: ['websocket'] — without it, Socket.io attempts HTTP long-polling first, which frequently fails in React Native and produces confusing "server error" messages that obscure the real cause.
  • A 25-second ping/pong heartbeat keeps connections alive through load balancer timeouts — most cloud load balancers kill idle connections at 60 seconds; a client-side ping every 25 seconds prevents this without requiring server configuration changes.
  • The AppState zombie connection pattern — app goes to background, connection is silently dropped by the OS, app returns to foreground with readyState === WebSocket.OPEN but no messages arriving — requires explicit AppState detection and a ping-on-foreground verification to catch.
  • ProGuard stripping WebSocket classes in Android release builds is the most common cause of "works in debug, fails in production" failures — adding -keep class okhttp3.** rules to proguard-rules.pro resolves it without affecting bundle size meaningfully.

Introduction

WebSocket connections in React Native fail silently in ways that do not happen in browsers. A WebSocket that works perfectly in your browser test refuses to connect on an Android emulator. Your iOS device connects fine, but the Android release build drops the connection after 60 seconds. The onclose event fires with code 1006, and you have no idea why.

At AgileSoftLabs, we have debugged WebSocket issues across dozens of production React Native apps — chat apps, real-time dashboards, live sports updates, and financial tickers. This guide covers every failure pattern we have encountered, with the exact fix for each.

Mobile App Development Services builds production real-time React Native applications — including the WebSocket architecture, reconnection logic, and background-state handling described in this guide.

Diagnosing WebSocket Issues in React Native

Before hunting for a specific error, instrument your WebSocket connection so you can see exactly what is happening:

const ws = new WebSocket('wss://your-server.com/ws');

ws.onopen = () => console.log('[WS] Connected');

ws.onclose = (event) => {
  console.log(
    `[WS] Closed: code=${event.code}, reason=${event.reason}, wasClean=${event.wasClean}`
  );
};

ws.onerror = (error) => console.log('[WS] Error:', JSON.stringify(error));

ws.onmessage = (event) => console.log('[WS] Message:', event.data);

Close Codes: What They Mean

Code Meaning Common Cause
1000 Normal closure Expected disconnect
1001 Going away Server restart or page unload
1006 Abnormal closure Network issue, no close frame received
1007 Invalid payload Message format mismatch
1008 Policy violation Server rejected connection
1011 Server error Server-side exception
1015 TLS handshake failure Certificate issue

Code 1006 is the most common and the hardest — it means the TCP connection died without a proper WebSocket close handshake. This is always a network-layer issue, not an application logic problem. Knowing which code you are getting tells you which category of fix to apply before reading a line of your application code.

Custom Bug Tracker Software production deployments track close code distributions over time — a shift in 1006 frequency across a release is a signal that something changed in the server-side timeout configuration or the CDN/load balancer layer, detectable before user complaints surface.

Error 1: WebSocket Not Connecting on Android Emulator

Symptom: WebSocket works in the browser and iOS simulator but fails on the Android emulator with an immediate connection error.

Root cause: The Android emulator uses 10.0.2.2 to reach the host machine, not localhost or 127.0.0.1. Any connection to localhost from inside the Android emulator attempts to connect to the emulator's own loopback interface, where nothing is listening.

Fix:

const WS_URL = __DEV__
  ? Platform.select({
      android: 'ws://10.0.2.2:3001/ws',  // Android emulator → host machine
      ios: 'ws://localhost:3001/ws',       // iOS simulator → host machine
    })
  : 'wss://api.yourapp.com/ws';           // Production

const ws = new WebSocket(WS_URL);

For a physical Android device on the same development network, use your machine's LAN IP address instead:

const WS_URL = __DEV__
  ? 'ws://192.168.1.100:3001/ws'  // Your machine's local network IP
  : 'wss://api.yourapp.com/ws';

Find your LAN IP with ipconfig (Windows) or ifconfig / ip addr (macOS/Linux). The physical device and your development machine must be on the same WiFi network.

Error 2: SSL/TLS Handshake Failure (wss://)

Symptom:

WebSocket closed with code 1015: TLS handshake failure

Or on Android specifically:

javax.net.ssl.SSLHandshakeException: Certificate expired

Root causes: 

  1. Self-signed certificate in development
  2. Expired certificate in production
  3. Missing intermediate certificate in the chain
  4. Android does not trust the CA

Fix — Development (self-signed certificate):

For development environments only, allow self-signed certificates on Android by creating a network security configuration:

<!-- android/app/src/main/res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <debug-overrides>
    <trust-anchors>
      <certificates src="system"/>
      <certificates src="user"/>
    </trust-anchors>
  </debug-overrides>
</network-security-config>

Reference it in your AndroidManifest.xml:

<!-- android/app/src/main/AndroidManifest.xml -->
<application
  android:networkSecurityConfig="@xml/network_security_config"
  ...>

Fix — Production (expired or missing certificate chain):

# Verify your full certificate chain
openssl s_client -connect api.yourapp.com:443 -showcerts

# Check certificate expiry dates
echo | openssl s_client -connect api.yourapp.com:443 2>/dev/null \
  | openssl x509 -noout -dates

Renew via Let's Encrypt or your certificate provider. Critically, ensure the full chain — root certificate, intermediate certificate, and server certificate — is installed on your server. A missing intermediate is the most common cause of "works in browser but fails on Android" certificate errors, because browsers aggressively cache intermediate certificates while Android's WebSocket client does not.

Error 3: Connection Drops with Code 1006

Symptom: WebSocket connects successfully but randomly disconnects with code 1006, especially after the phone screen turns off or after several minutes of inactivity.

Root causes: 

  1. Server timeout (common: 60-second idle timeout on load balancers)
  2. Mobile network switching (WiFi → LTE)
  3. Android Doze mode killing network connections
  4. TCP keepalive not configured on server

Fix — Client-side ping/pong heartbeat:

class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.pingInterval = null;
    this.ws = null;
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('[WS] Connected');
      this.startHeartbeat();
    };

    this.ws.onclose = (event) => {
      console.log(`[WS] Closed: ${event.code}`);
      this.stopHeartbeat();
      if (event.code !== 1000) {
        setTimeout(() => this.connect(), 3000);
      }
    };

    this.ws.onmessage = (event) => {
      if (event.data === 'pong') return; // Ignore pong responses
      this.handleMessage(event.data);
    };
  }

  startHeartbeat() {
    this.pingInterval = setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.ws.send('ping');
      }
    }, 25000); // 25 seconds — safely under most 30-60s server timeouts
  }

  stopHeartbeat() {
    clearInterval(this.pingInterval);
  }
}

Fix — Server-side TCP keepalive (Node.js / ws library):

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 3001 });

wss.on('connection', (ws) => {
  // Enable TCP keepalive at the socket level
  ws._socket.setKeepAlive(true, 30000);

  ws.on('message', (data) => {
    if (data.toString() === 'ping') {
      ws.send('pong');
    }
  });
});

The 25-second client-side ping interval is deliberate — it keeps the connection active through the most common 30-second and 60-second server idle timeouts without requiring any server configuration changes. Cloud Development Services configures load balancer timeout values and TCP keepalive settings at the infrastructure level for production WebSocket deployments, eliminating the 1006 drops that catch teams by surprise when an app moves from staging (direct server connection) to production (load-balanced fleet).

Error 4: Works in Dev, Fails in Production Build

Symptom: WebSocket connects and works correctly in debug builds but fails immediately or with unusual behavior in release builds.

Root causes: Release builds use Hermes and full minification. Common issues:

  • __DEV__ is false, Selecting the wrong URL
  • Certificate pinning logic enabled only in production
  • ProGuard (Android) stripping WebSocket classes

Fix — Android ProGuard rules:

# android/app/proguard-rules.pro
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
-keep class com.facebook.react.modules.network.** { *; }

Fix — Verify URL selection with explicit logging:

// Add explicit logging for URL selection
const WS_URL = 'wss://api.yourapp.com/ws';

console.log('[WS] Connecting to:', WS_URL, '| __DEV__:', __DEV__);

Log the URL every time a connection is attempted in the release build — a surprising number of "production fails" turn out to be the app connecting to ws:// (no SSL) rather than wss:// due to a conditional that did not behave as expected in a release environment.

Error 5: Socket.io Not Connecting (React Native Specific)

Symptom:

Error: server error
Transport: polling

Root cause: Socket.io defaults to HTTP long-polling before attempting to upgrade to WebSocket. The polling transport frequently fails in React Native due to how React Native's HTTP stack handles chunked transfer encoding and long-lived HTTP connections — a browser-specific behavior that Socket.io's transport upgrade relies on.

Fix — Force WebSocket transport (this is the #1 Socket.io fix for React Native):

import io from 'socket.io-client';

const socket = io('https://api.yourapp.com', {
  transports: ['websocket'], // Skip polling entirely
  upgrade: false,
  forceNew: true,
  reconnectionDelay: 1000,
  reconnectionAttempts: 5,
  timeout: 20000,
});

Without transports: ['websocket'], Socket.io will attempt polling first and frequently fail with misleading error messages that point to the wrong layer. This single configuration change resolves the overwhelming majority of Socket.io connection issues in React Native.

Streamly Plus live sports and media streaming applications use native WebSocket rather than Socket.io specifically to avoid this transport negotiation layer — for high-throughput real-time data where connection reliability matters more than Socket.io's feature set, the simpler native implementation is more predictable.

Error 6: WebSocket Reconnection Not Working

Symptom: After a network interruption (switching from WiFi to LTE, entering a tunnel, brief signal loss), the WebSocket never reconnects — it simply stays disconnected until the app is restarted.

Fix — Exponential backoff reconnection with attempt tracking:

class ReconnectingWebSocket {
  constructor(url) {
    this.url = url;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = 10;
    this.baseDelay = 1000;
    this.ws = null;
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('[WS] Connected');
      this.reconnectAttempts = 0; // Reset counter on successful connection
    };

    this.ws.onclose = (event) => {
      if (event.code === 1000) return; // Intentional close — do not reconnect

      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        const delay = Math.min(
          this.baseDelay * Math.pow(2, this.reconnectAttempts),
          30000 // Cap at 30 seconds
        );
        console.log(
          `[WS] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`
        );
        this.reconnectAttempts++;
        setTimeout(() => this.connect(), delay);
      } else {
        console.log('[WS] Max reconnect attempts reached — giving up');
      }
    };
  }
}

The exponential backoff is critical — reconnecting immediately on every close event floods a struggling server with connection attempts and can trigger rate limiting that makes the situation worse. Capping at 30 seconds prevents the delay from growing so long that users think the feature is broken. AI Workflow Automation real-time pipeline status feeds use this exact reconnection pattern — workflow state updates must survive transient network interruptions without requiring a manual refresh, and the backoff cap ensures reconnection within 30 seconds of network restoration.

Error 7: Messages Not Received After Background/Foreground

Symptom: The app goes to background and returns to foreground. The WebSocket appears connected — readyState reads as WebSocket.OPEN — but no new messages arrive. The connection is in a zombie state.

Root cause: The OS may suspend the network connection while the app is backgrounded, but the WebSocket close event never fires. The client does not know the connection is dead.

Fix — AppState detection with connection verification:

import { AppState } from 'react-native';

class WebSocketManager {
  constructor(url) {
    this.url = url;
    this.ws = null;
    this.appStateSubscription = null;
  }

  start() {
    this.connect();

    this.appStateSubscription = AppState.addEventListener('change', (state) => {
      if (state === 'active') {
        // App returned to foreground — verify the connection is actually alive
        if (!this.isConnected()) {
          console.log('[WS] App foregrounded with dead connection, reconnecting...');
          this.connect();
        } else {
          // Connection appears open — send a ping to confirm it is actually alive
          this.ws.send('ping');
        }
      }
    });
  }

  isConnected() {
    return this.ws?.readyState === WebSocket.OPEN;
  }

  stop() {
    this.appStateSubscription?.remove();
    this.ws?.close(1000);
  }
}

The ping-on-foreground is the key detail. readyState === WebSocket.OPEN tells you the JavaScript object believes the connection is open — it does not tell you the underlying TCP connection is alive. The ping forces a round trip that either succeeds (proving the connection is real) or triggers a close event (revealing the zombie state) within a few seconds.

IoT Development Services mobile monitoring applications face an identical pattern with device telemetry feeds — a field technician locks their phone mid-inspection, returns to it, and needs the live sensor data feed to be working immediately without a manual reconnect step.

Building a Production-Ready WebSocket Client

Combining all seven fixes into a single production-grade client:

import { AppState, Platform } from 'react-native';

const WS_URL = __DEV__
  ? Platform.select({
      android: 'ws://10.0.2.2:3001/ws',
      ios: 'ws://localhost:3001/ws',
    })
  : 'wss://api.yourapp.com/ws';

class ProductionWebSocket {
  constructor() {
    this.ws = null;
    this.pingInterval = null;
    this.reconnectTimeout = null;
    this.reconnectAttempts = 0;
    this.listeners = {};
    this.isIntentionallyClosed = false;
    this.appStateSubscription = null;
  }

  connect() {
    if (this.ws?.readyState === WebSocket.OPEN) return;

    this.ws = new WebSocket(WS_URL);

    this.ws.onopen = () => {
      this.reconnectAttempts = 0;
      this.startHeartbeat();
      this.emit('connected');
    };

    this.ws.onclose = (event) => {
      this.stopHeartbeat();
      if (!this.isIntentionallyClosed) {
        this.scheduleReconnect();
      }
    };

    this.ws.onmessage = (event) => {
      if (event.data === 'pong') return;
      try {
        const data = JSON.parse(event.data);
        this.emit('message', data);
      } catch {
        this.emit('message', event.data);
      }
    };
  }

  send(data) {
    if (this.ws?.readyState !== WebSocket.OPEN) {
      console.warn('[WS] Attempted to send while disconnected');
      return false;
    }
    this.ws.send(typeof data === 'string' ? data : JSON.stringify(data));
    return true;
  }

  startHeartbeat() {
    this.pingInterval = setInterval(() => {
      this.send('ping');
    }, 25000);
  }

  stopHeartbeat() {
    clearInterval(this.pingInterval);
  }

  scheduleReconnect() {
    const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
    this.reconnectTimeout = setTimeout(() => {
      this.reconnectAttempts++;
      this.connect();
    }, delay);
  }

  startAppStateMonitoring() {
    this.appStateSubscription = AppState.addEventListener('change', (state) => {
      if (state === 'active') {
        if (!this.isConnected()) {
          this.connect();
        } else {
          this.send('ping');
        }
      }
    });
  }

  isConnected() {
    return this.ws?.readyState === WebSocket.OPEN;
  }

  on(event, callback) {
    if (!this.listeners[event]) this.listeners[event] = [];
    this.listeners[event].push(callback);
  }

  emit(event, data) {
    (this.listeners[event] || []).forEach(cb => cb(data));
  }

  disconnect() {
    this.isIntentionallyClosed = true;
    this.appStateSubscription?.remove();
    clearTimeout(this.reconnectTimeout);
    this.stopHeartbeat();
    this.ws?.close(1000);
  }
}

export const wsClient = new ProductionWebSocket();

For Loan Management Software real-time financial dashboards where live rate feeds and application status updates cannot afford missed reconnections, this client pattern — combining platform-aware URL selection, heartbeat, exponential backoff, and AppState monitoring — provides the reliability that manual new WebSocket() calls cannot sustain across the full range of mobile network conditions. Explore AgileSoftLabs case studies for real-time production deployments across fintech, logistics, and consumer apps with 100K+ daily active users.

Building a Real-Time React Native App?

WebSocket reliability in production requires more than a single new WebSocket() call — it requires platform-aware URL selection, certificate configuration, heartbeat logic, exponential backoff reconnection, and AppState monitoring working together. The patterns in this guide represent what production apps with 100K+ daily active users actually require.

AgileSoftLabs has built real-time systems for fintech, logistics, and consumer apps — from WebSocket architecture to push notification fallback strategies. Explore the full mobile and technology services portfolio or contact our team to discuss your real-time architecture requirements.

Frequently Asked Questions

Why does WebSocket work in Expo Go but not in a standalone build? 

Expo Go includes additional networking polyfills and bundled library versions that are not present in standalone builds, which use the bare React Native network stack. If a library was functioning via Expo Go's bundled version, you may need to explicitly install and link it in the standalone build. The most common case is libraries that depend on Expo-specific networking internals — check the library's documentation for bare workflow installation requirements and compare it against your package.json.

Should I use the native WebSocket API directly or use Socket.io? 

For most React Native use cases, the native WebSocket API is simpler and has fewer failure modes. Socket.io adds reconnection, namespaces, and rooms — but also adds the polling-transport footgun described in Error 5 and significantly more complexity. Use Socket.io only if your server already uses it and you need its specific room or namespace features. If you are starting from scratch and the server is yours to design, native WebSocket with the ProductionWebSocket client above handles reconnection robustly without the Socket.io overhead.

How do I test WebSocket connections in CI without a real server? 

Use jest-websocket-mock for unit testing WebSocket behavior — it mocks the WebSocket server so you can test reconnection logic, message handling, and error scenarios without network dependency. For integration tests, run a real WebSocket server (using the ws npm library) locally in your test setup as a beforeAll / afterAll fixture. Never mock the WebSocket API itself — mock the server instead, so your client code runs unchanged against a controlled server that produces the specific scenarios you want to test.

What is the best approach for sending messages that were queued while the WebSocket was disconnected? 

Implement a message queue that accumulates sends while readyState !== WebSocket.OPEN and flushes on the onopen event. The send() method in the ProductionWebSocket above returns false when disconnected — callers can push failed sends to a queue and the onopen handler drains it. Limit queue size to prevent unbounded memory growth during extended disconnections — a queue cap of 50–100 messages is appropriate for most real-time apps, where messages older than a few reconnect cycles are stale anyway.

Android Doze mode is killing my WebSocket even with a heartbeat — what can I do? 

Android Doze mode aggressively suspends network activity for background apps and can kill WebSocket connections even with heartbeats, because the network access itself is suspended during Doze standby. The standard approaches in order of intrusiveness: use a foreground service with a persistent notification to keep your app's network access active during Doze; use Firebase Cloud Messaging as a fallback notification channel to wake the app when important events occur; request the REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission (which Google discourages and may affect Play Store review). For most consumer apps, FCM as a fallback with WebSocket for real-time is the most user-friendly and Play-Store-compliant approach.

The onmessage event fires but event.data is always null — what is happening? 

This is almost always a message format issue. React Native's WebSocket implementation expects text or binary data — if the server sends a null or undefined body, event.data will be null. Log the raw event object (not just event.data) to see all available fields. If the server is sending binary data (ArrayBuffer, Blob), ensure your server is explicitly sending text strings or that your client is configured for binary type: ws.binaryType = 'arraybuffer'. If the server is sending JSON, verify the Content-Type header and that JSON.stringify on the server side is not producing an empty object from an unserializable value.

Stuck on a React Native performance issue?

Get a free 30-minute mobile audit — we’ll review your stack, perf metrics, and ship recommendations you can act on the same day.

React Native WebSocket Not Connecting: Complete Debug and Fix Guide (2026) - AgileSoftLabs Blog