starlette.websockets.WebSocketDisconnect: 1000
Encountering
starlette.websockets.WebSocketDisconnect: 1000means a WebSocket connection was closed normally; this guide explains its implications and how to manage it effectively.
What This Error Means
When you see starlette.websockets.WebSocketDisconnect: 1000 in your FastAPI application logs, it signifies that a WebSocket connection has been closed. Crucially, the code 1000 indicates a "Normal Closure" as defined by the WebSocket protocol (RFC 6455, Section 7.4.1). Unlike many other WebSocketDisconnect codes (like 1006 for abnormal closure or 1011 for internal error), 1000 is generally not an error. Instead, it's an informational message indicating that the connection terminated gracefully and intentionally, either by the client or the server.
From a practical standpoint, this means the WebSocket handshake completed successfully, data was potentially exchanged, and then one party initiated a proper closing handshake. This is the expected behavior for any WebSocket connection that needs to end.
Why It Happens
The starlette.websockets.WebSocketDisconnect: 1000 message occurs because the underlying Starlette (which FastAPI builds upon) WebSocket implementation detects a clean shutdown of the connection. This can happen for several legitimate reasons, and understanding the context is key to determining if it's expected behavior or a symptom of something else.
In my experience, this message often appears when:
- Client-Initiated Closure: The client application explicitly closes the WebSocket connection. This is the most common scenario. For example, a user closes the browser tab, navigates away from the page, the JavaScript application calls
webSocket.close(), or a mobile app goes into the background and decides to disconnect. - Server-Initiated Graceful Closure: The FastAPI server application itself decides to close the connection. This could be due to:
- A planned server shutdown or restart (e.g., during a deployment).
- The server explicitly calling
await websocket.close()after completing a task or due to application logic (e.g., a session timeout).
- Application Logic: Your application might have specific conditions under which it deems a connection no longer necessary and initiates a clean close from either the client or server side.
It's important to differentiate this from unexpected disconnections, which would typically result in different error codes (e.g., 1006 for abnormal closure without a closing handshake, often due to network issues or abrupt termination).
Common Causes
Let's dive deeper into the specific scenarios that frequently lead to WebSocketDisconnect: 1000:
- User Interaction:
- A user closing their web browser or a specific tab/window.
- Navigating to a different URL within a single-page application (SPA) where the previous WebSocket connection is no longer needed.
- Explicitly logging out or performing an action that triggers a client-side
WebSocket.close().
- Client-Side Application Logic:
- JavaScript frameworks or libraries automatically closing connections when components unmount or become inactive.
- Mobile applications disconnecting when backgrounded to conserve resources.
- Desktop applications exiting or switching modes that don't require the WebSocket.
- Server-Side Application Logic:
- A FastAPI endpoint explicitly calling
await websocket.close()after fulfilling its purpose (e.g., streaming a finite amount of data). - An internal event triggering a server-side disconnect for a specific client (e.g., user session invalidation).
- A FastAPI endpoint explicitly calling
- Deployment and Server Restarts:
- During a CI/CD pipeline, when a new version of your FastAPI application is deployed, the old instances shut down gracefully. This often involves the server initiating
SIGTERMor similar signals, prompting Uvicorn/Gunicorn to close existing connections, leading to1000codes for active WebSockets.
- During a CI/CD pipeline, when a new version of your FastAPI application is deployed, the old instances shut down gracefully. This often involves the server initiating
- Load Balancer/Proxy Behavior (Indirectly): While less common for explicit
1000codes, sometimes load balancers or proxies have idle timeouts. If a client is inactive and the proxy closes the connection, the client might then send a clean close, or the server might detect the closure. For true idle timeouts, you'd more often see1006or1001if not handled correctly, but a well-configured proxy can facilitate a clean shutdown.
Step-by-Step Fix
Since 1000 typically indicates a normal closure, the "fix" isn't about correcting an error but rather about understanding and potentially managing these disconnections.
-
Acknowledge and Understand:
- First and foremost, understand that
WebSocketDisconnect: 1000is usually a non-error event. Don't immediately treat it as a bug. - Consider the context: Did a user just navigate away? Was the server redeployed? This initial assessment is critical.
- First and foremost, understand that
-
Review Client-Side Closure Logic:
- If you're seeing frequent
1000disconnections, check your client-side code (JavaScript, mobile app logic, etc.). - Are you explicitly calling
webSocket.close()? If so, ensure it's at an appropriate time. - Are there frameworks or libraries that might be closing connections automatically (e.g., React component unmount, Vue
beforeDestroy)? Ensure this behavior aligns with your application's needs.
- If you're seeing frequent
-
Implement Graceful Server Shutdowns:
- When deploying or restarting your FastAPI application, ensure your Uvicorn/Gunicorn setup allows for graceful shutdowns. This gives active WebSocket connections a chance to close properly.
- Uvicorn typically handles
SIGTERMby attempting to shut down gracefully, closing open connections. I've found that ensuring a reasonabletimeout_graceful_shutdownsetting for Gunicorn (if you're using it with Uvicorn workers) is crucial.
```bash
Example Gunicorn command for graceful shutdown
gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000 --timeout 60 --graceful-timeout 30
`` Here,graceful-timeout` gives workers time to finish requests and close connections. -
Refine Logging and Monitoring:
- If the volume of
1000messages is overwhelming or you need to debug specific connection behaviors, adjust your logging levels. - You might want to log these disconnections at an
INFOorDEBUGlevel rather thanWARNINGorERRORto reduce noise in production. - Consider adding specific logging within your FastAPI WebSocket endpoint to capture client details (e.g., user ID, session ID) upon disconnect. This can help correlate
1000events with user actions.
```python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
import logginglogger = logging.getLogger("uvicorn.error") # Or your custom logger
app = FastAPI()
@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: str):
await websocket.accept()
logger.info(f"WebSocket connected: {client_id}")
try:
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message text was: {data}")
except WebSocketDisconnect as e:
# Check for normal closure code
if e.code == 1000:
logger.info(f"WebSocket normally disconnected: {client_id} (code: {e.code})")
else:
logger.warning(f"WebSocket disconnected abnormally: {client_id} (code: {e.code}, reason: {e.reason})")
except Exception as e:
logger.error(f"WebSocket error for {client_id}: {e}")
finally:
logger.info(f"WebSocket handler finished for {client_id}")
``` - If the volume of
-
Proactive Connection Management (If Unexpectedly High Disconnects):
- If you observe a pattern of clients connecting and immediately disconnecting with
1000without apparent user action, it might indicate a client-side bug where it's establishing and closing connections unnecessarily. Review the client's connection lifecycle. - For very long-lived connections, implementing server-side or client-side heartbeats can help detect truly dead connections proactively, which might prevent eventual
1006errors and ensure1000is used for intentional closes.
- If you observe a pattern of clients connecting and immediately disconnecting with
Code Examples
Here are some concise, copy-paste ready examples for both server and client illustrating graceful WebSocket closures.
FastAPI Server Endpoint
This example shows a basic FastAPI WebSocket endpoint that logs disconnections, differentiating between normal and abnormal closures.
# main.py
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
import logging
# Configure logging for better visibility
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("my_fastapi_app")
app = FastAPI()
html = """
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Test</title>
</head>
<body>
<h1>WebSocket Client</h1>
<form action="" onsubmit="sendMessage(event)">
<input type="text" id="messageText" autocomplete="off"/>
<button>Send</button>
</form>
<ul id='messages'>
</ul>
<button onclick="closeConnection()">Close Connection</button>
<script>
var ws = null;
var client_id = Date.now();
document.addEventListener('DOMContentLoaded', (event) => {
ws = new WebSocket(`ws://localhost:8000/ws/${client_id}`);
ws.onopen = (event) => {
var messages = document.getElementById('messages');
var item = document.createElement('li');
item.textContent = `Connected to server (Client ID: ${client_id})`;
messages.appendChild(item);
};
ws.onmessage = (event) => {
var messages = document.getElementById('messages');
var item = document.createElement('li');
item.textContent = event.data;
messages.appendChild(item);
};
ws.onclose = (event) => {
var messages = document.getElementById('messages');
var item = document.createElement('li');
item.textContent = `Disconnected from server (Code: ${event.code}, Reason: ${event.reason})`;
messages.appendChild(item);
ws = null; // Clear WebSocket instance
};
ws.onerror = (error) => {
var messages = document.getElementById('messages');
var item = document.createElement('li');
item.textContent = `WebSocket Error: ${error}`;
messages.appendChild(item);
};
});
function sendMessage(event) {
var input = document.getElementById("messageText");
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(input.value);
input.value = '';
} else {
alert("WebSocket is not connected or closing.");
}
event.preventDefault();
}
function closeConnection() {
if (ws) {
ws.close(1000, "Client initiated close"); // Explicitly close with code 1000
ws = null;
}
}
</script>
</body>
</html>
"""
@app.get("/")
async def get():
return HTMLResponse(html)
@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: str):
await websocket.accept()
logger.info(f"WebSocket connected for client: {client_id}")
try:
while True:
data = await websocket.receive_text()
logger.info(f"Received message from {client_id}: {data}")
await websocket.send_text(f"You said: {data}")
except WebSocketDisconnect as e:
if e.code == 1000:
logger.info(f"Client {client_id} disconnected normally (code: {e.code}, reason: {e.reason})")
else:
logger.warning(f"Client {client_id} disconnected with code: {e.code}, reason: {e.reason}")
except Exception as e:
logger.error(f"An unexpected error occurred with client {client_id}: {e}")
finally:
logger.info(f"WebSocket handler ending for client: {client_id}")
To run this:
1. Save the code as main.py.
2. Install FastAPI and Uvicorn: pip install "fastapi[all]" uvicorn
3. Run the server: uvicorn main:app --reload
4. Open http://localhost:8000 in your browser.
5. Send messages, then click "Close Connection" or simply close the browser tab. Observe the server logs. You should see Client <ID> disconnected normally (code: 1000, reason: Client initiated close) or similar.
Environment-Specific Notes
The interpretation and management of WebSocketDisconnect: 1000 can vary slightly across different deployment environments.
-
Local Development:
- This is the easiest environment to debug. Disconnections are usually direct consequences of your client-side actions (browser refresh, tab close) or server restarts.
- Logs are immediately visible in your terminal, making it straightforward to correlate client behavior with server-side disconnect events.
-
Docker Containers:
- When stopping a Docker container running your FastAPI application (e.g.,
docker stop <container_id>), Docker sends aSIGTERMsignal. - A well-behaved Uvicorn or Gunicorn process inside the container will catch
SIGTERMand attempt a graceful shutdown, leading to1000codes for active WebSocket connections as it closes them. - If
docker killis used or the process doesn't handleSIGTERMcorrectly, you might see1006(Abnormal Closure) instead, as the connection is abruptly severed. Ensure your container's entrypoint orCMDallows the Python process to receive and handleSIGTERM.
- When stopping a Docker container running your FastAPI application (e.g.,
-
Cloud Environments (AWS, GCP, Azure):
- Load Balancers: Cloud load balancers (e.g., AWS ALB, GCP Load Balancer, Azure Application Gateway) handle routing WebSocket traffic. They often have idle timeouts. While a
1000typically comes from an explicit close, if a load balancer terminates an idle connection without a clean WebSocket close handshake, you'd typically see1006. If your client is configured to send a clean close before the LB timeout, then1000is possible. - Container Orchestration (E.g., Kubernetes - GKE, EKS, AKS): When pods are scaled down, redeployed, or evicted, Kubernetes sends
SIGTERMto the container's main process. Similar to Docker, a graceful shutdown strategy for your application within the pod is critical to ensure1000(normal closure) rather than1006(abnormal closure) or other unexpected errors. ConfigureterminationGracePeriodSecondsin your deployment manifest to give your application enough time to close connections. I've seen issues in production when this period is too short for long-lived WebSocket connections. - Serverless (E.g., AWS Lambda, GCP Cloud Functions with API Gateway): While direct WebSockets are less common in traditional serverless functions (which are request-response based), platforms like AWS API Gateway support WebSocket APIs. Disconnections here are managed by the gateway, and client-side closes would still result in a
1000if the gateway correctly relays the close frame. The challenge is more about persistent connection management on the serverless side rather than the1000code itself.
- Load Balancers: Cloud load balancers (e.g., AWS ALB, GCP Load Balancer, Azure Application Gateway) handle routing WebSocket traffic. They often have idle timeouts. While a
Frequently Asked Questions
Q: Is WebSocketDisconnect: 1000 always a good thing?
A: Generally, yes. It means the WebSocket connection was closed gracefully, as per the protocol. It's an informational event rather than an error that needs fixing.
Q: How can I distinguish between an intentional client close and a server restart?
A: Implement detailed logging. On the client side, log when WebSocket.close() is called. On the server side, within your WebSocketDisconnect exception handler, log the client ID and the reason. For server restarts, your application's startup/shutdown logs should indicate when processes are being terminated, helping you correlate disconnections with deployments.
Q: Could a high volume of 1000 disconnects indicate a problem?
A: Potentially. While 1000 is a normal close, if you're seeing a rapid connect-disconnect cycle for many clients, it might point to a client-side bug (e.g., incorrectly re-establishing connections, or connecting to a service it immediately realizes it doesn't need). Review client application logic for such patterns.
Q: What if I'm getting other codes like 1001, 1006, or 1011?
A: Those codes do typically indicate problems.
* 1001: Going Away (client leaving, e.g., browser navigating).
* 1006: Abnormal Closure (no close frame sent, often network issues, abrupt client/server termination). This one often requires investigation.
* 1011: Internal Error (server-side issue preventing fulfillment of request).
Each of these warrants a deeper dive into client or server logs and network conditions.