Back to Blog
Python

Python WebSocket Client Background Connections

python websocket client background websocket connections: Run a Python WebSocket client in the background using asyncio tasks or daemon threads, with reconnection, hea...

websocketasynciobackground-taskspython-networkingrealtime-communicationthreading
Illustration of a Python WebSocket client running as a background task while the main application continues processing.

A WebSocket client in Python is inherently blocking. A synchronous client blocks the calling thread on recv(), and an asyncio client blocks the event loop when you await a message without scheduling the connection as a task. If you need python websocket client background websocket connections — a connection that stays alive while the rest of the application continues doing work — you have to move the connection out of the main execution path. This article covers the two practical ways to do that: an asyncio background task, and a daemon thread running a synchronous client.

Why a WebSocket Client Blocks the Application

A synchronous WebSocket client such as websocket-client exposes a recv() method that blocks the calling thread until a frame arrives. If you call it from your main thread, your entire application pauses waiting for network data. The same problem exists in asyncio code: await websocket.recv() suspends the current coroutine, and if that coroutine is the only thing the event loop is running, nothing else progresses.

The blocking behavior is not a bug. It is the natural contract of a stream-oriented API. The practical consequence is that a WebSocket client cannot simply be dropped into the middle of an application. It needs an execution context of its own — a task on the event loop, or a separate thread — so that receiving messages does not stall unrelated work.

# Synchronous client: this blocks the calling thread from websocket import create_connection ws = create_connection("wss://example.com/socket") message = ws.recv() # thread is blocked until a frame arrives

The same code written with websockets blocks the event loop instead:

import asyncio import websockets async def main(): async with websockets.connect("wss://example.com/socket") as ws: message = await ws.recv() # event loop is suspended here

Neither version is wrong. The problem appears only when the connection must coexist with other work. That is the scenario this article targets: a connection that runs in the background while the application keeps serving requests, processing data, or handling user input.

Running the Client as an Asyncio Background Task

When your application is already built on asyncio, the cleanest way to get background WebSocket connections is to wrap the connection loop in a coroutine and schedule it with asyncio.create_task(). The task runs concurrently with the rest of the event loop, so await points inside the connection no longer block the application's other coroutines.

import asyncio import websockets async def listen(uri: str) -> None: async with websockets.connect(uri) as ws: async for message in ws: print(f"received: {message}") async def main() -> None: task = asyncio.create_task(listen("wss://example.com/socket")) # The rest of the application continues here await asyncio.sleep(3600) task.cancel() asyncio.run(main())

The async for message in ws loop is the key part. It repeatedly awaits the next frame and yields it to your handler. Because the loop lives inside a task, the event loop can interleave other coroutines between frames. If you need to send data from elsewhere in the application, keep a reference to the WebSocket object and pass it to the task, or expose a small queue that the task drains.

One constraint applies: asyncio.create_task() requires a running event loop. If your application is synchronous — a Flask or Django request handler, for example — you cannot call it directly. You either run an event loop in a background thread, or use the thread-based approach described later.

Keeping the Background Connection Alive

A WebSocket connection does not stay open by itself. Networks drop idle connections, servers close connections after a timeout, and transient failures interrupt the link. A background client that exits on the first disconnect is useless. The connection loop needs two things: automatic reconnection and a keepalive mechanism.

A reconnect loop wraps the connection attempt in a loop that retries after failure. Exponential backoff prevents a dead server from turning the background task into a hot retry loop.

import asyncio import websockets async def listen_with_reconnect(uri: str) -> None: delay = 1 while True: try: async with websockets.connect(uri) as ws: delay = 1 async for message in ws: print(f"received: {message}") except (websockets.ConnectionClosed, OSError): await asyncio.sleep(delay) delay = min(delay * 2, 60)

The backoff resets to one second after a successful connection, so a healthy connection does not accumulate delay. The upper bound of 60 seconds keeps reconnection attempts reasonable during a long outage. For keepalive, most WebSocket servers expect periodic ping frames. The websockets library sends pings automatically based on the protocol's ping interval; with websocket-client, you typically schedule your own ping from a timer or rely on the server's ping policy. If your server closes idle connections aggressively, you may need to send application-level pings on a timer inside the loop.

Thread-Based Background Connection for Synchronous Code

If the application is synchronous and you do not want to introduce an event loop, run the client in a daemon thread. The websocket-client library provides WebSocketApp, which manages the connection lifecycle and dispatches callbacks for messages, errors, and close events. Its run_forever() method blocks, so it belongs on a dedicated thread.

from websocket import WebSocketApp import threading def on_message(ws, message): print(f"received: {message}") def on_error(ws, error): print(f"error: {error}") def start_background_client(uri: str) -> WebSocketApp: ws = WebSocketApp(uri, on_message=on_message, on_error=on_error) thread = threading.Thread(target=ws.run_forever, daemon=True) thread.start() return ws

Marking the thread as a daemon means it will not prevent the process from exiting when the main thread finishes. That is usually what you want for a background connection, but it also means the connection is terminated abruptly at shutdown unless you close it explicitly. WebSocketApp also supports automatic reconnection through its run_forever() loop with a reconnect parameter in recent versions, but the exact behavior depends on the library version, so verify the reconnection policy before relying on it.

Choose the thread approach when the rest of the application is synchronous and you want minimal architectural change. Choose the asyncio task when the application already runs an event loop, because mixing threads with asyncio adds locking and coordination overhead that the task approach avoids.

Shutting Down a Background Connection Cleanly

A background connection that is never closed leaks the socket, the thread, or the task, and can keep the process alive or produce warnings at interpreter exit. Shutdown must be explicit and ordered: stop receiving, close the socket, then wait for the worker to finish.

For an asyncio task, cancellation is the standard mechanism. Cancelling the task raises CancelledError inside the coroutine, which unwinds the async with block and closes the connection.

task.cancel() try: await task except asyncio.CancelledError: pass

For a thread-based client, call close() on the WebSocketApp and then join the thread with a timeout so shutdown does not hang indefinitely.

ws.close() thread.join(timeout=5)

The timeout matters. A thread stuck in a network call may not respond to close() immediately, and an unbounded join() can stall application shutdown. A short timeout lets the process exit even if the socket is unresponsive.

Failure Modes and Operational Concerns

Background connections fail silently by nature. Because the main thread never sees the exception, a dropped connection can go unnoticed until a user reports missing data. Log every reconnect attempt and every error callback with enough context — URI, attempt count, current backoff — to make the failure observable.

Memory is another concern. If the message handler stores every received frame without a bound, a long-running background connection grows the process footprint indefinitely. Apply the same retention limits you would apply to any stream consumer. Also be aware that some libraries buffer outgoing messages when the connection is down; unbounded buffering during an outage can exhaust memory before the reconnect succeeds.

Finally, consider the interaction between the background connection and application lifecycle. In a web framework that forks workers, a background thread started before the fork may not survive the fork correctly. Start the connection after the worker is ready, or run it in a separate process. The exact behavior depends on the framework and deployment model, so the general rule is: initialize background connections after the application is fully started, and close them during shutdown hooks rather than relying on interpreter exit.

python websocket client background websocket connections: Pr | RYUSLOG DEV