Module 5: WebSockets and File Uploads
Module 5: WebSockets and File Uploads
Module context
This is Module 5 of 6 in the FastAPI Advanced Features guide. So far you've built a modular API with dependency injection, APIRouter, advanced response models, and background tasks. Your Task Manager API already has a professional architecture.
But there's a fundamental problem with HTTP: it's request-response. The client asks, the server answers, and the connection closes. If you want the client to know that "someone just completed a task," it has two options: ask every 5 seconds (polling) or wait for the server to tell it. The first is inefficient. The second requires WebSockets.
And there's another gap: your API only speaks JSON. What happens when a user wants to attach a file to a task? A PDF, an image, a document? You need file uploads — and FastAPI handles them elegantly with UploadFile.
This module adds two fundamental capabilities to your API: real-time communication and file handling.
Module objectives
By the end of this module you'll be able to:
- Implement WebSocket endpoints in FastAPI with the accept/send/receive cycle
- Build a ConnectionManager to manage multiple simultaneous connections
- Broadcast messages to every connected client
- Handle disconnections gracefully without crashing the server
- Receive files via multipart/form-data with
UploadFile - Validate files by MIME type and size before saving them
- Combine file upload with JSON data using
Form()+File() - Upload multiple files in a single request
Module roadmap
Capsule 02 Capsule 03 Capsule 04 Capsule 05
Basic WebSocket → ConnectionManager → File Uploads → Project: Real-time + Uploads
(echo server, (broadcast, (UploadFile, (WS notifications,
lifecycle, rooms, validation, attachments on tasks,
handshake) disconnections) multipart) full integration)
What you already know
You're coming from 4 modules that gave you a solid foundation:
| Module | What you learned | How it connects here |
|---|---|---|
| M1: DI | Depends(), sub-deps, yield | WebSocket endpoints use dependencies |
| M2: APIRouter | Modularization, middleware | A separate WebSocket router |
| M3: Responses | StreamingResponse, schemas | File responses for downloads |
| M4: Background | BackgroundTasks, patterns | WS notifications + background tasks |
Combining WebSockets (M5) with BackgroundTasks (M4) is particularly powerful: a background task can notify every connected client via WebSocket when it finishes.
HTTP vs WebSocket: the fundamental difference
HTTP (what you already know)
Client Server
│ │
│── GET /tasks ─────────────►│
│◄──── 200 [{...}] ─────────│
│ │ (connection closed)
│ │
│── POST /tasks ────────────►│
│◄──── 201 {...} ───────────│
│ │ (connection closed)
Every request is independent. The server never contacts the client first.
WebSocket (what you'll learn)
Client Server
│ │
│── Upgrade: websocket ─────►│
│◄──── 101 Switching ───────│
│ │
│◄──── "Task created" ──────│ ← the server sends without the client asking
│── "Mark task 3 done" ────►│
│◄──── "Task 3 completed" ──│
│ │
│◄──── "New task assigned" ──│ ← push notification
│ │
│── close ──────────────────►│
The connection stays open. Both sides can send messages at any time.
Multipart vs JSON: how files travel
Your API today only speaks JSON:
POST /tasks
Content-Type: application/json
{"title": "Write docs", "priority": "high"}
But files don't fit in JSON (they're binary). To send them you need multipart/form-data:
POST /tasks/1/attachments
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="spec.pdf"
Content-Type: application/pdf
(binary content of the PDF)
------WebKitFormBoundary--
FastAPI handles this automatically with UploadFile. You just declare the parameter.
What this module does NOT cover
- Advanced WebSocket authentication — covered in the Authentication guide (#9)
- WebSocket at scale (Redis PubSub) — requires Redis, covered in the Redis guide (#10)
- File storage in the cloud (S3) — covered in the deployment guides
- Video streaming — requires specialized protocols (HLS, DASH)
- WebSocket with frontend frameworks — React/Vue are out of scope
The focus is: implementing WebSockets and file uploads in FastAPI in a way that works and is correct.
Connection to the project
In Capsule 05 of this module:
- You'll add a WebSocket endpoint to the Task Manager API that notifies every client when a task is created, updated, or completed
- You'll implement file uploads to attach files to tasks with type and size validation
- You'll integrate WebSockets with the background tasks from Module 4
In Module 6 (the final project), everything comes together into a complete, professional API.
Testing tools
To test WebSockets you'll need one of these options:
Option 1: websocat (terminal)
# Install
brew install websocat # Mac
# or
pip install websockets # Python
# Connect
websocat ws://localhost:8000/ws
Option 2: JavaScript in the browser
const ws = new WebSocket("ws://localhost:8000/ws");
ws.onmessage = (event) => console.log("Received:", event.data);
ws.send("Hello");
Option 3: A Python script
import asyncio
import websockets
async def test():
async with websockets.connect("ws://localhost:8000/ws") as ws:
await ws.send("Hello")
response = await ws.recv()
print(f"Received: {response}")
asyncio.run(test())
Any of them works. Use whichever feels most comfortable.
Technical prerequisites
- Everything from Modules 1-4, working
pip install python-multipart(for file uploads — FastAPI requires it)- Optional:
pip install websockets(for testing with Python) - Optional:
brew install websocat(for testing from the terminal)
Check that python-multipart is installed:
pip install python-multipart
pip freeze | grep multipart
If you don't install it, FastAPI will show a clear error when you try to use UploadFile or Form().
Common traps and mistakes in this module
1. Assuming WebSocket is "always better" than HTTP
WebSocket is for persistent bidirectional communication, not for replacing HTTP. If your client only needs to "get data when it asks for it," HTTP is enough. WebSocket has costs: one open connection per client, the complexity of handling disconnections, and it doesn't play well with generic load balancers.
How to spot it: you're using WebSocket where a GET endpoint would solve the case. How to fix it: use WebSocket only for flows where the server needs to push without the client asking. Notifications, chat, live dashboards. For normal CRUD, HTTP.
2. Not handling WebSocketDisconnect
The client can disconnect at any moment (close the tab, lose the network, navigate away). If your code doesn't catch WebSocketDisconnect, every disconnection looks like a crash in the logs and the ConnectionManager is left holding dead references.
How to spot it: logs full of exceptions when clients navigate away; the broadcast tries to send to clients that no longer exist.
How to fix it: wrap await websocket.receive_text() in try/except WebSocketDisconnect, and in the except call manager.disconnect(websocket).
3. File uploads without size validation
UploadFile doesn't impose a limit by default. An attacker (or a distracted user) can upload a 10GB file. The process runs out of RAM or disk, and your API goes down.
How to spot it: server memory grows without control during uploads; OOM kill in the logs.
How to fix it: validate size early with Content-Length before reading the file, or read in chunks and abort if it goes over the limit. Also limit it at the proxy/load balancer layer.
4. Saving files to disk with the user's original filename
If you save a file using the user's name (user_report.pdf), an attacker can upload ../../../etc/passwd and overwrite system files (path traversal). Also names with strange characters that break the filesystem.
How to spot it: a passive security review or, worse, an incident.
How to fix it: generate a safe name server-side (UUID + a validated extension). Store the original name only as metadata in the DB. Never use the client's filename directly in open().
Self-check: are you ready to start?
Before moving on to capsule 02, make sure you can answer:
1. Why does a server with HTTP polling every 5 seconds scale worse than the same server with WebSocket?
Polling every 5s generates a full HTTP request per client every 5 seconds: TCP handshake, headers, parsing, authentication, a DB query, a response. For 10K clients, that's 2K req/s of polling alone — most of them returning "nothing new."
WebSocket opens a single TCP connection per client and keeps it alive. The server sends data only when there's something to send. For 10K clients, that's 10K idle connections (~1KB each in RAM) and you only spend CPU when there's a real event.
The trade-off: WebSocket consumes RAM per persistent connection; polling consumes CPU on redundant requests. For cases where "the server needs to tell you something," WebSocket always wins.
2. multipart/form-data vs application/json: when do you use each?
JSON for structured data with primitive types (string, int, bool, lists, objects). It's efficient, easy to validate with Pydantic, and serializes uniformly.
Multipart when there are binary files. JSON can't hold binary bytes efficiently (you'd have to base64-encode and it grows by 33%). Multipart sends the bytes as-is + metadata as other fields.
Hybrid: if you're uploading a file + data about the file (name, description, tags), you use multipart with Form() for the fields and File() for the file. FastAPI supports this natively.
3. Your ConnectionManager broadcasts to every client when someone creates a task. What happens if one of the clients disconnected 10 seconds ago but is still in your list?
await client.send_text(...) raises an exception (typically WebSocketDisconnect or ConnectionClosed). If the broadcast doesn't wrap each send in try/except, one disconnection breaks the broadcast for everyone else — the clients that come after the broken one never receive the message.
How it's done right: iterate over the list, send with try/except per client, and in the except mark the client to be disconnected after the loop. You never let one broken client block broadcasts to healthy clients.
This pattern is called "graceful broadcast" and it's what you're going to implement in capsule 03.
If all three feel clear, you're ready to get started with WebSocket.
Resources
- FastAPI WebSockets — The official docs
- FastAPI Request Files — UploadFile docs
- MDN WebSocket API — The protocol reference
- RFC 6455 — WebSocket Protocol — The full specification
- python-multipart — The library FastAPI uses internally
Summary and next step
- WebSockets enable persistent bidirectional communication — the server pushes without the client asking.
- File uploads require
multipart/form-dataandUploadFile— they don't fit in JSON. - The ConnectionManager is the standard pattern for managing multiple simultaneous WS clients.
- File validation (type, size, safe name) is your responsibility —
UploadFiledoesn't do it by default. - This module is the last piece before the integrative project (M6).
Checkpoint: before moving on, you should be able to explain in your own words why WebSocket doesn't replace HTTP for CRUD but clearly wins for real-time notifications.
Bridge to the next capsule: you've seen the "what" and the "when" of WebSockets and file uploads. Capsule 02 tackles the first technical step: creating your first WebSocket endpoint in FastAPI, understanding the handshake (HTTP 101 Switching Protocols), the accept/send/receive/disconnect cycle, and building an echo server. Without that mechanical foundation, the ConnectionManager and the broadcasting in later capsules are just magic.
What's next?
In Capsule 02 we start with WebSockets: you'll create your first WebSocket endpoint, understand the lifecycle of a connection, and build an echo server that responds to messages in real time.