websockets
multithreading
concurrent programming
client-server communication
python

How do I send something to connected websocket clients from another thread?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

If your WebSocket server runs on an asyncio event loop, the safest way to send messages from another thread is to hand work back to that loop. The key rule is that the thread should not try to manipulate WebSocket connections directly; it should schedule a coroutine or place a message onto a thread-safe bridge that the loop owns.

Why Direct Cross-Thread Sends Are Risky

Most Python WebSocket servers keep connection objects tied to a single event loop. A background thread may be able to see those objects, but using them directly from the wrong thread can cause race conditions, event-loop errors, or corrupted connection state.

The safer pattern is:

  1. the event loop owns the WebSocket clients
  2. a worker thread produces messages
  3. the worker thread schedules work back onto the loop

That preserves the normal async model instead of mixing thread execution directly into socket operations.

Keep Track of Connected Clients in the Event Loop

A basic WebSocket server can hold the active connections in a set.

python
1import asyncio
2import websockets
3
4clients = set()
5
6
7async def handler(websocket):
8    clients.add(websocket)
9    try:
10        async for message in websocket:
11            await websocket.send(f"echo: {message}")
12    finally:
13        clients.discard(websocket)

Only the event loop should add or remove clients from this set. That keeps ownership clear.

Schedule Broadcasts From Another Thread

If a worker thread wants to broadcast a message, use asyncio.run_coroutine_threadsafe and pass it the main event loop.

python
1import asyncio
2import threading
3import websockets
4
5clients = set()
6
7
8async def broadcast(message: str):
9    if not clients:
10        return
11
12    await asyncio.gather(
13        *(client.send(message) for client in list(clients)),
14        return_exceptions=True,
15    )
16
17
18async def handler(websocket):
19    clients.add(websocket)
20    try:
21        async for _ in websocket:
22            pass
23    finally:
24        clients.discard(websocket)
25
26
27def worker(loop: asyncio.AbstractEventLoop):
28    for i in range(3):
29        future = asyncio.run_coroutine_threadsafe(
30            broadcast(f"background update {i}"),
31            loop,
32        )
33        future.result()
34
35
36async def main():
37    loop = asyncio.get_running_loop()
38    threading.Thread(target=worker, args=(loop,), daemon=True).start()
39
40    async with websockets.serve(handler, "127.0.0.1", 8765):
41        await asyncio.Future()
42
43
44asyncio.run(main())

This is the core solution: the thread does not send directly. It asks the event loop to run the broadcast coroutine on its behalf.

Use a Queue When Message Volume Is Higher

If the worker produces many messages, a queue is often cleaner than scheduling one coroutine per event. The thread pushes messages into a thread-safe queue, and an async task drains that queue inside the event loop.

python
1import asyncio
2import queue
3import threading
4
5message_queue = queue.Queue()
6
7
8def worker():
9    for i in range(5):
10        message_queue.put(f"queued update {i}")
11
12
13async def queue_pump():
14    while True:
15        message = await asyncio.to_thread(message_queue.get)
16        await broadcast(message)

This design helps when many producers need to feed one broadcast channel, because the event loop still remains the only place that touches the connections.

Handle Closed Clients Carefully

Broadcast code should expect some clients to disconnect between the moment you snapshot the set and the moment you send. That is why return_exceptions=True is useful in asyncio.gather.

You can also prune closed clients after failed sends if your library or wrapper exposes a clear closed-state signal. The important point is to treat disconnections as normal rather than exceptional application failures.

Common Pitfalls

The biggest mistake is calling websocket.send() directly from the worker thread. Even if it appears to work under light load, it breaks the event-loop ownership model.

Another common issue is mutating the shared client set from both the loop and the thread. Keep client registration and removal inside the event loop only.

People also sometimes block the event loop accidentally by using a normal queue.get() inside async code. If you use a standard thread queue, pull from it with asyncio.to_thread or a dedicated nonblocking bridge.

Finally, do not assume every broadcast reaches every client. Disconnections and send failures are part of normal WebSocket operation, so the broadcast path should be resilient.

Summary

  • Let the asyncio event loop own all WebSocket connections.
  • From another thread, schedule sends back onto that loop with asyncio.run_coroutine_threadsafe.
  • Use a queue when worker threads produce many messages.
  • Keep connection registration and cleanup inside the event loop, not in worker threads.
  • Treat disconnects as normal and write broadcast code that tolerates them.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.