# Don’t Let Django Serve Files Directly – Boost Download Performance with X‑Accel‑Redirect Most Django applications deliver protected files (e.g., login‑only downloads, post‑purchase content) by reading the file in the Python process and streaming it to the client via `FileResponse`. This works fine for low traffic or internal server communication. When download requests spike, the application server becomes tied up with file I/O, making it hard to process other logic such as permission checks, business rules, or API responses. The solution is to let Django perform the permission check and hand off the actual file transfer to Nginx using **X‑Accel‑Redirect**. --- ## Why Direct File Streaming Becomes a Bottleneck The typical flow for Django file delivery is: 1. Receive request 2. Verify permissions 3. Read file from disk or storage 4. Stream the file through the application process Steps 3 and 4 are heavy: * Larger files mean longer transfer times * More concurrent downloads tie up workers, threads, or processes * The result is delayed API responses, timeouts, and pressure to scale the app server Nginx, on the other hand, is optimized for static file delivery. It uses kernel‑level optimizations (`sendfile`), efficient event loops, buffering, and range request handling—all tailored for file delivery. --- ## Core Idea of X‑Accel‑Redirect **Django checks, Nginx streams.** ### How It Works 1. The client requests a URL such as `/download/123`. 2. Django performs a database lookup and permission check. 3. Django returns an empty body with the header: ``` X-Accel-Redirect: /_protected/real/path/to/file.webp ``` 4. Nginx sees the header, locates the file internally, and streams it directly to the client. Thus Django only decides *who* can download the file; Nginx handles the heavy lifting of file transfer. --- ## When This Approach Is Especially Beneficial 1. **High‑volume, high‑concurrency download or image services** – e.g., community forums, messaging apps, PDF reports. Simple logic but many requests. 2. **Large files or range‑request‑heavy services** – videos, audio, large archives. Browsers or players request byte ranges; Nginx handles these reliably. 3. **Reducing app‑server cost** – Python workers are expensive; offloading file delivery keeps the app server focused on business logic. --- ## When You Might Skip It * Low‑traffic internal communication. * Few file requests with API/DB logic as the real bottleneck. * Files stored in external object storage (e.g., S3) already served via CDN or pre‑signed URLs. In these cases, `FileResponse` is perfectly adequate. --- ## Example Implementation: Django + Nginx ![Web request flow diagram](/media/editor_temp/6/a1ad0374-979a-447e-b586-81ac02f2b447.png) ### Nginx Configuration The key directive is `internal`. A location marked `internal` cannot be accessed directly by clients; only internal redirects like X‑Accel‑Redirect can reach it. ```nginx # Serve protected files internally location /_protected/ { internal; alias /var/app/protected_media/; sendfile on; tcp_nopush on; # Optional caching or header control # add_header Cache-Control "private, max-age=0"; } ``` * Assume the real files live under `/var/app/protected_media/`. * Public URLs are routed through Django (e.g., `/download/...`). * Internal paths are unified under `/_protected/...`. --- ### Django View Example ```python from django.http import HttpResponse, Http404 from django.contrib.auth.decorators import login_required from django.utils.encoding import iri_to_uri @login_required def download(request, file_id): # 1) DB lookup + permission check obj = get_file_object_or_404(file_id) # example helper if not obj.can_download(request.user): raise Http404 # 2) Build internal path for Nginx internal_path = f"/_protected/{obj.storage_relpath}" # 3) Return only the X‑Accel‑Redirect header response = HttpResponse() response["X-Accel-Redirect"] = iri_to_uri(internal_path) # Optional: set download filename and MIME type response["Content-Type"] = obj.content_type or "application/octet-stream" response["Content-Disposition"] = f'attachment; filename="{obj.download_name}"' return response ``` * No file I/O occurs in the view. * The worker stays free to handle other requests. --- ## Security Checklist 1. **Server‑determined internal paths** – never trust user input to build the `X-Accel-Redirect` value. Store safe relative paths in the database or use a whitelist. 2. **`internal` location** – without it, clients could request `/_protected/...` directly and bypass permission checks. 3. **Django‑only permission logic** – Nginx is purely a delivery engine; all access control must happen in Django. --- ## Alternatives Using Third‑Party Services If cost is not a concern, you can serve files directly from a third‑party storage provider. This saves server resources and can be more reliable. * **CDN caching** – for public files, a CDN before Nginx can reduce load. * **Pre‑signed URLs (S3, etc.)** – for object storage, a pre‑signed URL may be simpler than X‑Accel‑Redirect. --- ## Takeaway Letting Nginx handle file delivery via X‑Accel‑Redirect gives you a clear performance advantage, especially under heavy download traffic or large files. Django focuses on permissions and business logic, while Nginx delivers the content efficiently. For low‑traffic scenarios, `FileResponse` remains a clean choice, but when the app server starts to choke on file I/O, X‑Accel‑Redirect is the fastest remedy. Remember the mantra: **“Permissions in Django, delivery in Nginx.”**