No dejes que Django entregue los archivos directamente: mejora el rendimiento de descargas con X-Accel-Redirect

La mayoría de las aplicaciones Django sirven archivos protegidos (solo usuarios autenticados, pagos previos, etc.) mediante FileResponse, lo que implica que el proceso Python lee el archivo y lo envía al cliente. Si el tráfico es bajo o la comunicación es interna, esto funciona bien.

Pero cuando las peticiones de archivos se disparan, el servidor de aplicaciones queda ocupado con la entrega de archivos, dificultando la ejecución de la lógica de negocio, la verificación de permisos y la respuesta a la API. En estos casos, delegar la transferencia de archivos a Nginx mediante X-Accel-Redirect es la solución.


¿Por qué enviar archivos directamente desde Python crea cuellos de botella?

El flujo típico de Django al servir un archivo es:

  1. Recibe la petición
  2. Verifica permisos
  3. Lee el archivo desde disco/almacenamiento
  4. Envía el contenido al cliente (streaming)

Los pasos 3 y 4 son los que más peso tienen:

  • Archivos grandes tardan más en transmitirse
  • Con más descargas simultáneas, los workers/threads/procesos se bloquean
  • Se generan retrasos en la API, timeouts y presión para escalar el servidor

Nginx, por otro lado, está optimizado para la entrega de archivos estáticos: sendfile, bucles de eventos eficientes, buffering y peticiones de rango.


Idea central de X-Accel-Redirect

Django solo verifica, Nginx solo entrega.

Funcionamiento

  1. El cliente solicita /download/123
  2. Django realiza la consulta a la BD y la verificación de permisos
  3. Django devuelve una respuesta con cabecera: X-Accel-Redirect: /_protected/real/path/to/file.webp y cuerpo vacío
  4. Nginx detecta la cabecera y busca el archivo internamente, enviándolo directamente al cliente

Así, Django no lee el contenido del archivo; solo decide si el usuario puede acceder.


Cuándo es especialmente útil

1) Servicios con muchas descargas o imágenes y alta concurrencia

  • Comunidades, mensajería, archivos adjuntos, PDFs de reportes
  • Patrones donde la lógica es simple y la cantidad de peticiones es alta

2) Archivos grandes o peticiones de rango importantes

  • Vídeos, audios, archivos comprimidos de gran tamaño
  • Navegadores o reproductores que usan Range para reproducir o continuar la descarga

3) Reducir costos del servidor de aplicación

  • Los workers de Python son costosos (memoria/CPU). Si se encargan de la entrega, el gasto aumenta
  • Delegar a Nginx permite que el servidor de aplicación se concentre en la lógica

Cuando no es necesario

  • Comunicación interna con tráfico bajo
  • Pocas peticiones de archivos; la lógica de la API es el cuello de botella
  • Archivos almacenados en S3 u otro objeto externo con CDN o URLs prefirmadas

En estos casos, FileResponse sigue siendo suficiente.


Ejemplo de implementación: Django + Nginx

Flujo de procesamiento de peticiones web

Configuración de Nginx

El punto clave es internal. Una ubicación marcada como internal no puede ser accedida directamente por el cliente; solo por redirecciones internas como X-Accel-Redirect.

# Endpoint interno que sirve archivos protegidos
location /_protected/ {
    internal;

    # Directorio donde están los archivos reales
    alias /var/app/protected_media/;

    # Opciones de rendimiento (ajusta según tu entorno)
    sendfile on;
    tcp_nopush on;

    # Opcional: control de caché/headers
    # add_header Cache-Control "private, max-age=0";
}
  • Se asume que los archivos reales están bajo /var/app/protected_media/
  • La URL pública es /download/... manejada por Django
  • La ruta interna es /_protected/...

Vista de Django

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) Consulta a la BD + verificación de permisos
    obj = get_file_object_or_404(file_id)  # ejemplo
    if not obj.can_download(request.user):
        raise Http404

    # 2) Construye la ruta interna (mapeada a /_protected/)
    internal_path = f"/_protected/{obj.storage_relpath}"

    # 3) Configura la cabecera X-Accel-Redirect y deja el cuerpo vacío
    response = HttpResponse()
    response["X-Accel-Redirect"] = iri_to_uri(internal_path)

    # Opcional: nombre de descarga y tipo MIME
    response["Content-Type"] = obj.content_type or "application/octet-stream"
    response["Content-Disposition"] = f'attachment; filename="{obj.download_name}"'

    return response

Puntos clave:

  • No hay FileResponse(open(...)) ni I/O de archivos
  • El tiempo de respuesta de Django es mínimo; los workers no se bloquean

Checklist de seguridad

1) La ruta interna debe ser decidida por el servidor

  • Evita que el cliente construya rutas como /_protected/../../etc/passwd
  • Usa rutas relativas seguras almacenadas en la BD o un mapeo basado en whitelist

2) La ubicación de Nginx debe ser internal

  • Sin internal, un usuario podría acceder directamente a /_protected/...

3) La lógica de permisos debe confiar únicamente en Django

  • Nginx solo es el motor de entrega; la autorización la gestiona Django

Alternativas con servicios de terceros

Si el presupuesto lo permite, se puede servir directamente desde un almacenamiento externo. Esto ahorra recursos del servidor y suele ser más fiable.

  • CDN: para archivos públicos, un CDN antes de Nginx aporta mayor rendimiento
  • URLs prefirmadas (S3, etc.): Si el objeto está en S3, usar URLs prefirmadas puede ser más sencillo que X-Accel-Redirect

Conclusión

En resumen, delegar la entrega de archivos a Nginx mediante X-Accel-Redirect mejora notablemente el rendimiento y la escalabilidad de las aplicaciones Django. Nginx, optimizado para archivos estáticos, maneja la transmisión de manera eficiente, mientras que Django se centra en la lógica de negocio y la autorización. Si el tráfico no es intenso, FileResponse sigue siendo una opción limpia; pero cuando las descargas se disparan, X-Accel-Redirect es la solución más rápida y efectiva.

Recuerda: “Permisos en Django, entrega en Nginx.”