Home

Blog

How to handle File Downloads in FastAPI

9 min read
Streaming file downloads efficiently with FastAPI
By David Muraya • October 15, 2025

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.

Why Stream Files?

Reading a file into memory before sending it is a bad practice for a few key reasons:

  1. High Memory Usage: If you load a 500 MB file into memory, your application's RAM usage spikes by 500 MB for that single request. If ten users request the same file, you'd need 5 GB of RAM. This doesn't scale and can easily lead to server crashes.
  2. Poor Performance: The user has to wait for your server to read the entire file from the disk before the download even begins. For large files, this creates a noticeable delay and a poor user experience.
  3. Request Timeouts: Many web servers have request timeouts. If reading a very large file takes too long, the request might time out and fail before the download starts.

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.

Essential HTTP Headers for File Downloads

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).

Streaming a File with FastAPI's `FileResponse`

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:

  1. We define a STATIC_FILES_DIR where our downloadable files are stored.
  2. The endpoint takes a filename as a path parameter. We perform a basic security check to prevent users from accessing files outside the intended directory.
  3. We check if the file actually exists and return a 404 error if it doesn't.
  4. We use os.path.getsize() to determine the Content-Length and the mimetypes library to guess the Content-Type.
  5. Finally, we return a 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.

Frontend: Showing Download Progress

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.

FAQ

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.

Share This Article

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

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

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

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 Practical Guide to FastAPI Security

A Comprehensive Checklist for Production-Ready Security for a FastAPI Application

Read More...

On this page

Why Stream Files?Essential HTTP Headers for File DownloadsStreaming a File with FastAPI's `FileResponse`Frontend: Showing Download ProgressFAQ

Back to Blogs

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.

© 2025 David Muraya. All rights reserved.