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:

ModuleWhat you learnedHow it connects here
M1: DIDepends(), sub-deps, yieldWebSocket endpoints use dependencies
M2: APIRouterModularization, middlewareA separate WebSocket router
M3: ResponsesStreamingResponse, schemasFile responses for downloads
M4: BackgroundBackgroundTasks, patternsWS 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

  1. FastAPI WebSockets — The official docs
  2. FastAPI Request Files — UploadFile docs
  3. MDN WebSocket API — The protocol reference
  4. RFC 6455 — WebSocket Protocol — The full specification
  5. 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-data and UploadFile — 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 — UploadFile doesn'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.