# Handling Outbound Network Changes

If your Render service maintains a long-lived outbound connection to an external resource (such as a third-party API or database), that connection might experience an interruption due to a change in Render's outbound IP routing for your service:

```mermaid
flowchart LR
    web("Service<br/>Instance") 
    subgraph "<b>Outbound Routing</b>"
    ip1("<s><code>192.0.2.100</code></s><br/>❌")
    ip2("<code>192.0.2.101</code><br/>✅")
    end
    external("External<br/>API / DB")
    web -.->|Previous IP|ip1
    web edge1@-->|New IP| ip2
    ip2 edge2@--> external;
    class ip1 failure;
    class ip2 success;
    edge1@{animation: slow}
    edge2@{animation: slow}
```

Common causes of an IP routing change include:

| Change | Initiated by |
| --- | --- |
| Adding or deleting [dedicated outbound IPs](dedicated-ips) that apply to your service | You |
| Periodic rotation of in-use IP addresses in Render's [default IP ranges](outbound-ip-addresses) | Render |
| Routine maintenance of Render's IP routing infrastructure | Render |

Whenever a routing change occurs, Render proactively resets affected TCP connections to public destinations. This closes the connection with a TCP reset so your service can reconnect using its new outbound IP.

From your service's perspective, this process appears the same as any other transient network interruption.

## Restoring an interrupted connection

To restore an outbound connection after an IP routing change, your service can use the same retry logic it would use for other connection interruptions.

The examples below use Node.js and Python, but you can apply this same pattern to other languages and frameworks.

> *Only retry actions that are safe to repeat.*
>
> Make sure to enforce idempotency or deduplication safeguards where appropriate.

### Example: HTTP retries

This example demonstrates outbound HTTP connections that retry transient upstream failures using exponential backoff.

**Tab: Node.js**

```javascript
import express from "express";
import { Agent, fetch } from "undici";

const app = express();
const upstreamClient = new Agent();
const retryableStatuses = new Set([502, 503, 504]);

async function fetchWithRetry(url, options = {}, retries = 3) {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      const response = await fetch(url, {
        ...options,
        dispatcher: upstreamClient,
      });

      // Retry transient upstream failures, but not permanent 4xx errors.
      if (retryableStatuses.has(response.status)) {
        await response.body?.cancel();
        throw new Error(`Request failed with status ${response.status}`);
      }

      return response;
    } catch (error) {
      // An IP routing change results in an error that's caught here.

      if (attempt === retries) {
        throw error;
      }

      // Exponential backoff with jitter avoids retry bursts.
      const delay = 500 * 2 ** attempt * (0.5 + Math.random());
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}

app.get("/data", async (_request, response) => {
  try {
    const upstreamResponse = await fetchWithRetry("https://api.example.com/data");
    const data = await upstreamResponse.json();
    response.json(data);
  } catch {
    response.status(503).send("The upstream service is temporarily unavailable.");
  }
});

app.listen(process.env.PORT || 3000, "0.0.0.0");

process.on("SIGTERM", async () => {
  await upstreamClient.close();
});
```

**Tab: Python**

```python
import asyncio
import random
from contextlib import asynccontextmanager

import httpx
from fastapi import FastAPI, HTTPException

upstream_client: httpx.AsyncClient | None = None
retryable_statuses = {502, 503, 504}


@asynccontextmanager
async def lifespan(_app: FastAPI):
    global upstream_client

    async with httpx.AsyncClient() as client:
        upstream_client = client
        yield


app = FastAPI(lifespan=lifespan)


async def fetch_with_retry(url: str, retries: int = 3) -> httpx.Response:
    assert upstream_client is not None

    for attempt in range(retries + 1):
        try:
            response = await upstream_client.get(url)
            # Retry transient upstream failures, but not permanent 4xx errors.
            if response.status_code in retryable_statuses:
                await response.aclose()
                response.raise_for_status()

            return response
        except httpx.HTTPError:
            # An IP routing change results in an error that's caught here.

            if attempt == retries:
                raise

            # Exponential backoff with jitter avoids retry bursts.
            await asyncio.sleep(0.5 * 2**attempt * (0.5 + random.random()))

    raise RuntimeError("Request failed unexpectedly")


@app.get("/data")
async def get_data():
    try:
        response = await fetch_with_retry("https://api.example.com/data")
        return response.json()
    except httpx.HTTPError as error:
        raise HTTPException(
            status_code=503,
            detail="The upstream service is temporarily unavailable.",
        ) from error
```

### Example: WebSocket retries

This example demonstrates an outbound WebSocket connection that reconnects using exponential backoff with jitter between reconnection attempts.

Note that as part of reconnecting, your application might also need to reauthenticate or resubscribe to the upstream service.

**Tab: Node.js**

```javascript
import express from "express";
import WebSocket from "ws";

const app = express();
const wsUrl = "wss://api.example.com/events";
let isShuttingDown = false;
let activeSocket;

function sleep(delay) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}

async function connectWithRetry() {
  let attempt = 0;

  while (!isShuttingDown) {
    await new Promise((resolve) => {
      const socket = new WebSocket(wsUrl);
      activeSocket = socket;

      socket.on("open", () => {
        console.log("Connected to the upstream WebSocket server.");
        attempt = 0;
      });

      socket.on("message", (message) => {
        console.log("Received:", message.toString());
      });

      socket.on("error", (error) => {
        console.error("WebSocket error:", error.message);
      });

      // A routing change closes the connection and triggers a reconnect.
      socket.once("close", () => {
        activeSocket = undefined;
        resolve();
      });
    });

    if (!isShuttingDown) {
      // Exponential backoff with jitter avoids retry bursts.
      const delay = Math.min(30_000, 500 * 2 ** attempt) * (0.5 + Math.random());
      attempt++;
      await sleep(delay);
    }
  }
}

connectWithRetry();

app.get("/", (_request, response) => {
  response.send("WebSocket client is running.");
});

app.listen(process.env.PORT || 3000, "0.0.0.0");

process.on("SIGTERM", () => {
  isShuttingDown = true;
  activeSocket?.close();
});
```

**Tab: Python**

```python
import asyncio
import random
from contextlib import asynccontextmanager, suppress

import websockets
from fastapi import FastAPI

ws_url = "wss://api.example.com/events"


async def connect_with_retry():
    attempt = 0

    while True:
        try:
            async with websockets.connect(ws_url) as websocket:
                print("Connected to the upstream WebSocket server.")
                attempt = 0

                async for message in websocket:
                    print("Received:", message)
        except (OSError, websockets.WebSocketException) as error:
            print("WebSocket error:", error)

        # Exponential backoff with jitter avoids retry bursts.
        delay = min(30, 0.5 * 2**attempt) * (0.5 + random.random())
        attempt += 1
        await asyncio.sleep(delay)


@asynccontextmanager
async def lifespan(_app: FastAPI):
    reconnect_task = asyncio.create_task(connect_with_retry())
    yield
    reconnect_task.cancel()
    with suppress(asyncio.CancelledError):
        await reconnect_task


app = FastAPI(lifespan=lifespan)


@app.get("/")
async def root():
    return {"message": "WebSocket client is running."}
```

## FAQ

###### How often does Render perform actions that change outbound IP routing?

These changes are uncommon, but they are occasionally required as part of Render's routine network operations or when responding to unexpected infrastructure events.

Render maintains dynamic, redundant routing paths to preserve your service's outbound internet connectivity. Whenever traffic moves to a different route (whether as part of routine maintenance or to resolve a transient network-link interruption), established outbound connections can be reset.

When routing changes are necessary, your service might experience multiple resets in short succession.

###### Do outbound IP changes affect incoming requests to my service?

No. Outbound IP changes don't affect how Render routes incoming requests to your service. They can only interrupt established, outbound internet connections from your service.

###### Do outbound IP changes affect private network connections?

No. Private network connections between your Render services are not affected by outbound IP routing changes.


---

##### Appendix: Glossary definitions

###### private network

Your Render services in the same *region* can reach each other without traversing the public internet, enabling faster and safer communication.

Related article: https://render.com/docs/private-network.md