Real-time data powers most modern software: livestock prices, chat applications, sports scores, collaboration tools. And to build these systems, you’ll need to understand how real-time communication works—which isn’t always straightforward.
I ran into this myself while trying to create a Live Options dashboard. HTTP requests weren’t cutting it, and everything I was reading seemed overly complicated until I got back to the basics. This article is the result of that process.
We will cover Python. websockets Library from scratch, then move to the Fast API, where many of the Python backends reside. It’s worth noting that WebSockets aren’t the only solution for real-time communication. WebRTC may be a better fit depending on your use case, but understanding WebSockets is the right starting point before exploring further.
Table of Contents
WebSocket Connections and Methods
A WebSocket connection enables two-way communication between a client and a server. Once a connection is established, both parties can communicate freely without being asked first. This differs from a normal HTTP request, where the client always has to ask the server before it responds.
It looks something like this:
CLIENTÂ <===== open connection =====>Â SERVER
Note that a WebSocket URL is not a regular web page, so you cannot “visit it” like a website. You need a client to talk to.
Different frameworks provide different methods for handling WebSocket connections. With Python websockets The library, for example, automatically accepts a connection when a client connects. With a framework like FastAPI, you have to make the call explicitly. await websocket.accept()otherwise the connection is rejected.
Let’s look at the basic methods provided by Python. websockets Library:
websockets.serve(...): Starts the WebSockets server.websockets.connect(...): Connects to a WebSocket server.websockets.send(...): Sends a message both ways.websockets.recv(): Receives a message from a client or server.
recv() Takes no arguments because it is purely a wait process. It waits for the next message and returns it:
message = await websocket.recv()
How to Create Your First WebSocket in Python
Before we dive into the framework, let’s explore Python’s. websockets Library You’ll set up a simple server and client, and exchange messages over a WebSockets connection, giving you a solid foundation for understanding WebSockets under the hood.
Environmental setup
Run the following in your virtual environment to install or verify the WebSockets package.
pip install websockets
# or, to check if it's already installed:
pip show websockets
Create a websocket server
make server.py in your project folder, and paste this:
import asyncio
import websockets
async def handler(connection):
print("Client connected")
message = await connection.recv()
print("Received from client:", message)
await connection.send("Hello client!")
async def main():
async with websockets.serve(handler, "localhost", 8000):
print("Server running at ws://localhost:8000")
#await asyncio.Future() # runs forever
await asyncio.sleep(30)
asyncio.run(main())
When this line executes:
async with websockets.serve(handler, "localhost", 8000):
The library opens a TCP socket on the specified host and port and waits for incoming clients. When one connects, it creates a connection object and passes it to your handler function.
A handler is needed because it defines what the server does with each connection. gave host And port Arguments are also important. Both default None – Passing neither causes an error because the OS cannot bind to a network server without a port.
You could pass. port=0 To allow the OS to automatically assign a free port, but then you need an extra step to know which port is selected, so the client can connect:
server.sockets(0).getsockname()
It’s easy to specify both the host and the port explicitly, so the client knows exactly where the server is running.
Configure the client.
make client.py in the same folder and add this:
import asyncio
import websockets
async def client():
async with websockets.connect("ws://localhost:8000") as websocket:
await websocket.send("Hello server!")
response = await websocket.recv()
print("Server replied:", response)
asyncio.run(client())
Check the connection.
First, open and run a terminal server.py. You should see:
Server running at ws://localhost:8000
In another terminal, run client.py. Messages should appear in both terminals confirming that the connection is active and communication is taking place on both sides.
Note that the server must be running before starting your client – ​​otherwise the client has nothing to connect to, and the connection will fail.
Keeping the server alive: A note on asyncio.Future()
i server.pythere is currently a line that is commented out:
await asyncio.Future()
This keeps the server running indefinitely. However, for local development and testing, await asyncio.sleep(30) There is an easy alternative. It keeps the server alive for a fixed period of time without running forever.
File Transfer over WebSockets
WebSockets are not limited to text. They also support raw bytes, which means you can send files directly over the connection. Here’s how a client can send a file to a server over a WebSocket connection:
Update. server.py
async def file_handler(ws):
print("Client connected, waiting for file...")
file_bytes = await ws.recv() # receive bytes
with open("received_file.png", "wb") as f:
f.write(file_bytes)
print("File received and saved!")
await ws.send("File received successfully!")
async def main():
async with websockets.serve(file_handler, "localhost", 8000):
print("Server running on ws://localhost:8000")
await asyncio.sleep(50) # keep server alive
asyncio.run(main())
The handler waits with incoming bytes. await ws.recv(); gave websockets The library automatically detects whether the incoming message is text or bytes, so no additional configuration is required. Once received, the file is written to disk in binary mode ("wb") and the server sends a confirmation message back to the client.
Update. client.py
import asyncio
import websockets
async def send_file():
uri = "ws://localhost:8000"
async with websockets.connect(uri) as ws:
with open("portfolio-image.png", "rb") as f: #open file in binary mode
file_bytes = f.read()
await ws.send(file_bytes) # send bytes
response = await ws.recv()
print("Server response:", response)
asyncio.run(send_file())
The client opens the image in binary mode ("rb"), reads the entire file into memory as bytes, and sends it in a single. ws.send() The call then waits for confirmation from the server before closing the connection.
Check it out.
Add an image to your project folder and make sure the file name is inside. client.py Run matches server.py First, then client.py In another terminal.
After the transfer is complete, the server saves the file as received_file.png in the same directory. You should see it appear in your workspace immediately.
This approach loads the entire file into memory before sending it. For large files, it’s best to read them and send them in chunks. But this is the simplest way to understand WebSocket byte transfer.
How to connect to an external websocket
So far you’ve been connecting to servers you’ve created yourself. But WebSocket clients can also connect to public servers. For example, a client can connect to Postman’s Echo server:
import asyncio
import websockets
async def connect_external():
uri = "wss://ws.postman-echo.com/raw" # public WebSocket server
async with websockets.connect(uri) as ws:
print("Connected to external server!")
# Send a message
await ws.send("Hello external server!")
print("Message sent")
# Receive response
response = await ws.recv()
print("Received from server:", response)
asyncio.run(connect_external())
Notice that the client connects to Postman’s echo server. wss:// URI scheme instead ws://. This indicates that the connection is encrypted using TLS, like how https:// Saves regular web requests.
Echo Server returns exactly what you send. So “Hello external server!” Returns directly as a response. It’s a useful sandbox for testing your client-side WebSocket code without needing your own server.
WebSockets in Fast API
The Fast API provides a WebSocket object (via Starlette under the hood) to manage real-time connections. You can define WebSocket endpoints like HTTP routes, while Uvicorn handles the event loop – no manual asyncio server management required. This makes FastAPI a natural fit for real-time projects, from chat apps to live dashboards and data feeds.
Before jumping into the code, here’s a quick reference to the basic methods you’ll be working with.
Acceptance:
await websocket.accept(): theaccept()The method must be called first before anything else. Omit it and the connection will be rejected.
Sending:
await websocket.send_text(data): Sends a string.await websocket.send_bytes(data): Sends binary data.await websocket.send_json(data): Serializes and sends JSON.
Receiving:
await websocket.receive_text(): Waiting for text message.await websocket.receive_bytes(): Waiting for binary data.await websocket.receive_json(): Receives JSON and deserializes it.async for msg in websocket.iter_text(): Repeats on incoming messages, exits gracefully on disconnection.
Closing:
await websocket.close(code=1000): Standard code for general closure. It accepts an optional “reason” argument.
Here’s what the WebSocket lifecycle looks like in FastAPI:

Building a Simple Echo Server with FastAPI
As you saw with the Postman example, the echo server sends back the message that the client provides. Let’s create one with FastAPI.
1. Install Fast API:
pip install "fastapi(standard)"
2. Update. server.py:
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
data = await websocket.receive_text()
await websocket.send_text(f"You said: {data}")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)
A few things are worth noting here compared to the field. websockets Library:
WebSocket endpoints are defined.
@app.websocket("/ws")Just like an HTTP route.await websocket.accept()Before anything else is necessary. FastAPI will not accept connections without it.Uvicorn handles the event loop and server startup for you.
if name == "__main__"Not blockedasyncio.run()orasyncio.Future()Need
3. Update client.py:
async def test_client():
uri = "ws://127.0.0.1:8000/ws"
async with websockets.connect(uri) as ws:
await ws.send("Hello FastAPI server!")
response = await ws.recv()
print("Server replied:", response)
asyncio.run(test_client())
Since the FastAPI server is not secured with TLS, the client uses the URI. ws:// instead of wss://. Make sure to match the host and port with your server code.
4. Interact with Echo Server:
Get started. server.pythen run client.py In another terminal. The server terminal should display the echoed message.

How to handle websocket disconnection in Fast API
Clients will inevitably disconnect in real-time applications, sometimes intentionally, sometimes unexpectedly. If not handled properly, it can crash your server or leave it in a broken state.
gave WebSocketDisconnect An exception in FastAPI is raised when a client unexpectedly closes the connection, allowing the server to gracefully handle the disconnection, log the event, and clean up the resource without crashing.
Here is an example:
@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
await ws.accept()
try:
while True:
data = await ws.receive_text()
if "bye" in data or "quit" in data:
await ws.send_text("Closing connection")
await ws.close(code=1000, reason="Server requested close")
break
await ws.send_text(f"I got your request: {data}")
except WebSocketDisconnect:
print("Client disconnected") # connection already closed
The server runs a continuous loop waiting for messages. If the client’s message contains “bye” or “quit”, the server responds, calling await ws.close(code=1000)and cleanly exits the loop.
But if the client disconnects unexpectedly, WebSocketDisconnect The exception is caught by the block and the server continues without crashing. At this point the connection is already closed on the client side, hence making the call ws.close() Except is unnecessary within the block.
The result
WebSockets enable real-time communication by keeping a persistent connection open between the client and the server. Starting with Python. websockets The library helps define how the protocol works under the hood, while frameworks like FastAPI provide the structure needed for production applications.
The parts that most people travel quickly. asyncio and FastAPI’s explicit websocket.accept(). with the asynciothe question is usually why this is needed and why the server dies instantly without keeping anything alive. And it’s easy to overlook. websocket.accept() If you are coming from Maidan. websockets library Where it happens automatically. Once you click on them, everything else follows naturally.