When building a web application, you often need to let users download files. This could be anything from a PDF invoice to a user-exported video. A simple approach might be to read the file into memory and send it in the response, but this method quickly fails when dealing with large files. Loading a 1 GB video into memory can crash your server, especially with multiple concurrent users.
The solution is to stream the file directly from the disk. This guide explains why streaming is necessary and shows how to implement it efficiently in FastAPI using FileResponse. We'll build a practical example that serves a file from the disk while showing a progress bar on the frontend.
Reading a file into memory before sending it is a bad practice for a few key reasons:
Streaming solves these problems. Instead of loading the whole file, the server reads it in small chunks and sends each chunk to the user as it's read. This keeps memory usage low and constant, and the download starts almost instantly.
When serving files for download, a few HTTP headers are critical to ensure browsers handle the process correctly.
Content-Disposition: This header tells the browser how to handle the file. By setting it to attachment; filename="your-file.mp4", you instruct the browser to open a "Save As" dialog rather than trying to display the file in the browser window. It also provides a default name for the file.Content-Length: This header specifies the total size of the file in bytes. It is essential for the browser to display a download progress bar. Without it, the user sees an indeterminate "loading" indicator, which is a poor experience.Content-Type: Also known as the media_type, this tells the browser what kind of file it is (e.g., video/mp4, application/pdf).FastAPI provides the FileResponse class to efficiently stream files from disk. It handles reading the file in chunks and setting the appropriate headers automatically.
Let's create a simple endpoint that streams a file from a predefined directory on the server.
# filepath: main.py import os import mimetypes from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse app = FastAPI() # Define a directory to serve files from. # For security, ensure this path is well-defined and doesn't allow directory traversal. STATIC_FILES_DIR = "static/downloads" @app.get("/download/{filename}") async def download_file(filename: str): """ Streams a file from the predefined static directory. """ # Sanitize the filename to prevent directory traversal attacks. if ".." in filename or "/" in filename: raise HTTPException(status_code=400, detail="Invalid filename.") file_path = os.path.join(STATIC_FILES_DIR, filename) if not os.path.isfile(file_path): raise HTTPException(status_code=404, detail="File not found.") # Guess the media type based on the file extension. media_type, _ = mimetypes.guess_type(file_path) if media_type is None: media_type = "application/octet-stream" # Default for unknown file types file_size = os.path.getsize(file_path) # Return a FileResponse to stream the file. return FileResponse( path=file_path, filename=filename, media_type=media_type, headers={ "Content-Disposition": f"attachment; filename=\"{filename}\"", "Content-Encoding": "identity", # Disable gzip compression "Content-Length": str(file_size), }, )
In this code:
STATIC_FILES_DIR where our downloadable files are stored.filename as a path parameter. We perform a basic security check to prevent users from accessing files outside the intended directory.os.path.getsize() to determine the Content-Length and the mimetypes library to guess the Content-Type.FileResponse. We explicitly set the Content-Disposition, Content-Length, and Content-Encoding headers. Including Content-Encoding: identity prevents intermediate proxies from applying additional compression, which is important for serving already-compressed files like ZIP archives or JPEGs.Because we included the Content-Length header, we can now use JavaScript to monitor the download progress and display it to the user. Here is an example using jQuery's AJAX functionality.
// AJAX function to download a file function downloadFile(filename, downloadButton) { const url = `/download/${filename}`; $.ajax({ url: url, method: "GET", xhrFields: { responseType: "blob", // Treat the response as a binary file }, xhr: function () { const xhr = new window.XMLHttpRequest(); // Listen to the "progress" event xhr.addEventListener("progress", function (event) { if (event.lengthComputable) { const percentComplete = Math.round((event.loaded / event.total) * 100); downloadButton.text(`Downloading.. ${percentComplete}%`); } }); return xhr; }, success: function (data, status, xhr) { // Get filename from the Content-Disposition header const disposition = xhr.getResponseHeader("Content-Disposition"); const resolvedFileName = disposition ? disposition.split("filename=")[1].replace(/"/g, '') : filename; // Create a temporary link to trigger the download const blob = new Blob([data]); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = resolvedFileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); }, error: function (xhr, status, error) { console.log("Error Response:", xhr, status, error); // Handle errors... }, complete: function () { downloadButton.text("Download"); downloadButton.prop("disabled", false); }, }); }
The key part is the xhr function. It adds a progress event listener to the XMLHttpRequest object. The browser can calculate the percentage complete because our FastAPI endpoint provided the Content-Length.
1. Is the security check in the example enough for production?
No. The check for .. and / is a basic safeguard against directory traversal attacks, but it's not foolproof. A more secure approach is to avoid exposing filenames in the URL altogether. Instead, use a unique identifier (like a UUID) for each file.
For example, your endpoint could be /download/{file_id}. Your code would then look up the actual file path on the server based on that ID. This prevents users from guessing file paths and accessing unauthorized files.
2. Does FileResponse block the server while streaming a large file?
No, FileResponse is designed for async frameworks and does not block the event loop. It reads the file from the disk in small chunks and sends them asynchronously. This allows your FastAPI application to remain responsive and handle other incoming requests while a large download is in progress.
3. How can I protect file downloads for authenticated users only? You can use FastAPI's dependency injection system. Create a dependency that verifies the user's authentication status (e.g., by checking a JWT or session cookie). Then, add that dependency to your download endpoint. I have covered authentication in detail in my FastAPI Authentication Guide.
async def get_current_user(token: str = Depends(oauth2_scheme)): # Your logic to validate token and get user ... return user @app.get("/download/{filename}") async def download_file(filename: str, current_user: User = Depends(get_current_user)): # This code will only run if the user is authenticated. ...
4. What if the file is generated on-the-fly and not stored on disk?
If you are generating file content in memory (e.g., creating a CSV or PDF report), you should use StreamingResponse instead of FileResponse. StreamingResponse requires an iterator (like a generator function) that yields the file content in chunks. This provides the same memory-saving benefits as FileResponse but for data that doesn't exist as a physical file. For a practical streaming file example, see how we use it to generate and stream secure PDFs.
About the Author
David Muraya is a Solutions Architect specializing in Python, FastAPI, and Cloud Infrastructure. He is passionate about building scalable, production-ready applications and sharing his knowledge with the developer community. You can connect with him on LinkedIn.
Related Blog Posts
Enjoyed this blog post? Check out these related posts!

How to Handle File Uploads in FastAPI
A Practical Guide to Streaming and Validating File Uploads
Read More...

How to Create and Secure PDFs in Python with FastAPI
A Guide to Generating and Encrypting PDFs with WeasyPrint, pypdf, and FastAPI
Read More...

How to Protect Your FastAPI OpenAPI/Swagger Docs with Authentication
A Guide to Securing Your API Documentation with Authentication
Read More...

A Practical Guide to FastAPI Security
A Comprehensive Checklist for Production-Ready Security for a FastAPI Application
Read More...
Contact Me
Have a project in mind? Send me an email at hello@davidmuraya.com and let's bring your ideas to life. I am always available for exciting discussions.