How to Deploy Django on AWS Lambda (and When Not To)
Django on AWS Lambda is viable and works well for the right workloads. Getting it right means understanding three main friction points: static files, database connections, and package size. This guide walks through each one and helps you decide when Lambda is actually the right call.
The short answer
Yes, you can deploy Django on AWS Lambda. You wrap your Django app in an ASGI or WSGI adapter (Mangum or Zappa), deploy it as a Lambda function, and front it with API Gateway. The app handles requests and returns responses — same request-response pattern as any web server, just without a server to manage.
Serverless Django is a reasonable choice for existing apps with variable or spiky traffic where you want less operational overhead. It is a poor fit for apps that need WebSockets, rely on Celery workers, have large native dependencies, or serve sustained high throughput.
| Best for | Not ideal for |
|---|---|
| Admin dashboards and internal tools | Sustained high-traffic APIs |
| Small-to-medium REST APIs | WebSocket or realtime workloads |
| Spiky or variable traffic | Celery worker-heavy systems |
| Existing Django apps with low ops budget | Apps with large native dependencies (scipy, OpenCV) |
| Prototypes and MVPs | Latency-sensitive apps that cannot tolerate cold starts |
How it works in plain English
If you are new to serverless, here is what actually happens when a user makes a request to a Django app running on Lambda.
Think of each Lambda execution environment like a new browser tab. It opens when a request arrives, runs your Django code, sends a response, and then sits idle until the next request. Nothing persists between requests unless it is saved somewhere external: a database, a cache, or S3.
The request path itself works like this: a request hits API Gateway, which translates the HTTP request into a Lambda event and invokes your function. Inside the function, an adapter (Mangum or Zappa) translates the Lambda event into something Django understands, either ASGI or WSGI. Django processes the request and returns a response. The adapter converts that back into a Lambda response, which API Gateway sends to the user as an HTTP response.
Two things behave differently from a standard Django deployment:
- Static files: Lambda has no persistent filesystem. Django cannot serve CSS, JavaScript, or images from it. Static files live in S3, with CloudFront in front for caching. Your Django app just points to the right URL.
- Database connections: each Lambda execution environment is a separate process that opens its own database connection. Under concurrency, this causes connection exhaustion. RDS Proxy sits between Lambda and your database and pools the real connections.
Understanding those two things explains most of the production issues teams run into with serverless Django.
You can run your Mangum-wrapped Django app locally using uvicorn lambda_handler:handler or the Django dev server. Catch configuration mistakes before they become Lambda deploy cycles.
Architecture overview
The request path for a Django app running on Lambda:
- User request: hits your domain, routed via Route 53 or a custom domain
- API Gateway: receives the HTTP request, translates it into a Lambda event, invokes the function
- Lambda runtime: your function starts (or resumes from a warm execution environment)
- Adapter (Mangum or Zappa): translates the Lambda event into ASGI or WSGI format
- Django app: processes the request through views, models, middleware, and templates
- RDS Proxy: if the view queries the database, Django connects to the proxy, which maintains a real connection pool to RDS
- Response: travels back through the same chain to the user
Static files and user-uploaded media follow a separate path entirely:
- Static files (CSS, JS) live in S3, served through CloudFront
- User uploads go directly to S3 via pre-signed URLs or django-storages
- Lambda never handles file bytes in either case
Secrets (database passwords, Django’s SECRET_KEY, API keys) come from environment variables configured via AWS Secrets Manager or Lambda environment variables, never hardcoded.
When to use Django on Lambda
Lambda is a strong choice for Django in these scenarios:
- Internal admin tools. Traffic is low and variable. Cold starts are acceptable. No need to run a server around the clock for a tool used by a small team.
- Small-to-medium APIs. If your API handles hundreds to low thousands of requests per minute with occasional spikes, Lambda scales automatically without any tuning.
- Spiky workloads. Django on Lambda handles sudden traffic bursts well. Lambda scales out fast. See how Lambda scales under load for the concurrency model.
- Existing Django apps. Wrapping an existing app in Mangum or Zappa is a lower-effort path to reducing ops overhead than rewriting it for ECS.
- Prototypes and MVPs. Lambda costs near zero at low volumes, and you pay only for actual usage.
When not to use Django on Lambda
Lambda is the wrong tool when:
- You need sustained high throughput. Lambda’s concurrency model is built for bursts. For steady high request volume (thousands of RPM continuously), a persistent container on ECS or App Runner is cheaper and faster.
- Your app uses WebSockets or realtime features. Lambda functions terminate after each request. Persistent connections are not supported without significant extra infrastructure.
- You rely on Celery or background workers. Lambda is invocation-based. Celery needs persistent processes. You can run workers separately, but it adds complexity and cost.
- Your dependencies are large. Django with numpy, scipy, OpenCV, or similar libraries can exceed Lambda’s 250 MB zip limit. Container images work around this, but change the deployment model significantly.
- Latency-sensitive requests. Cold starts for a typical Django app run 1–3 seconds. If occasional slow first requests are not acceptable, use provisioned concurrency or run on a container platform.
Deployment approaches: Mangum vs Zappa
There are two common approaches for running Django on Lambda. They solve different problems.
| Mangum | Zappa | |
|---|---|---|
| What it is | ASGI adapter library | WSGI wrapper + deployment tool |
| Who it’s for | Teams that want direct control over packaging and deployment | Teams that want automated packaging and one-command deploys |
| Advantages | Minimal, framework-agnostic, transparent | Handles packaging, IAM, and API Gateway config automatically |
| Trade-offs | You manage packaging (zip or container) yourself | More abstraction, harder to debug when things go wrong |
| Django version | Django 3.1+ (requires ASGI support) | Any Django version (WSGI) |
Use Mangum if you want clean, direct control over your Lambda function. It is minimal, well-maintained, and works the same way as the FastAPI on Lambda setup, so it is easy to understand and debug. Use Zappa if you want the fastest possible path from an existing Django app to Lambda with minimal manual steps.
Mangum setup
Your Django project’s asgi.py (Django generates this automatically):
# myproject/asgi.py
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
application = get_asgi_application()The Lambda handler file:
# lambda_handler.py
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
from mangum import Mangum
from myproject.asgi import application
handler = Mangum(application, lifespan="off")Zappa setup
pip install zappa
zappa initZappa generates a zappa_settings.json:
{
"production": {
"django_settings": "myproject.settings",
"aws_region": "us-east-1",
"runtime": "python3.12",
"timeout_seconds": 30,
"memory_size": 512,
"s3_bucket": "my-zappa-deployments-bucket"
}
}# First deploy
zappa deploy production
# Update after code changes
zappa update productionRecommended production architecture
A production-ready Django on Lambda setup uses these components:
- API Gateway: receives HTTP traffic and routes it to Lambda
- Lambda: runs your Django app inside a function
- S3: stores static files (CSS, JS) and user-uploaded media
- CloudFront: CDN in front of S3 for static files, handling HTTPS and global caching
- RDS Proxy: connection pooler between Lambda and your database
- RDS or Aurora: relational database (PostgreSQL or MySQL)
- VPC: Lambda and RDS Proxy run inside a VPC so database traffic stays private and off the public internet
- Secrets Manager or Parameter Store: stores DB passwords, SECRET_KEY, and API keys at rest
- CloudWatch: captures Lambda logs and metrics automatically. See monitoring Lambda for what to watch.
- IAM execution role: Lambda needs an IAM role with least-privilege permissions: S3 access, Secrets Manager read, and VPC network access
The Lambda security model covers the execution role and network isolation patterns in detail.
Deployment checklist
The real sequence for getting Django on Lambda, from scratch to running:
- Prepare your Django app. Make sure settings read from environment variables (SECRET_KEY, DB credentials, ALLOWED_HOSTS). Remove any assumptions about a persistent filesystem.
- Create the Lambda handler. Add
lambda_handler.pywrapping your ASGI app with Mangum, or runzappa initfor the Zappa path. - Package dependencies. Install dependencies into a local folder (
pip install -r requirements.txt -t ./package), zip it with your app code. Switch to a Docker image if dependencies are large. - Deploy the Lambda function. Use the AWS Console, CLI, CDK, Terraform, or Zappa. Set memory, timeout, and environment variables at this step.
- Connect API Gateway. Create an HTTP API in API Gateway and route all paths (
/{proxy+}) to your Lambda function. - Configure static files. Create an S3 bucket, configure django-storages, run
python manage.py collectstatic, then create a CloudFront distribution in front of the bucket. - Set up RDS Proxy. Create RDS Proxy in the same VPC as your Lambda function. Point Django’s database
HOSTto the proxy endpoint, not the RDS endpoint directly. - Run migrations separately. Run
python manage.py migratefrom your CI/CD pipeline before deploying new Lambda code. Never inside the Lambda handler. - Verify logs and health. Make a test request and check CloudWatch Logs for errors. Confirm database connectivity and static file URLs are resolving correctly.
If you are new to Lambda itself, the deploying your first Lambda function guide covers the basics before you add Django into the mix.
Static files and media
Lambda’s filesystem is ephemeral and read-only (except /tmp). Django cannot serve static files from it. You need to separate static files and user-uploaded media from your app entirely.
Static files (CSS, JS, bundled assets)
Use django-storages with the S3 backend. Django then writes static files to S3 when you run collectstatic, and your templates generate URLs pointing to S3 or CloudFront.
# settings.py
INSTALLED_APPS = [
# ... other apps
'storages',
]
AWS_STORAGE_BUCKET_NAME = 'my-static-files-bucket'
AWS_S3_REGION_NAME = 'us-east-1'
AWS_DEFAULT_ACL = None
AWS_S3_FILE_OVERWRITE = False
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
STATIC_URL = 'https://your-cloudfront-domain.cloudfront.net/static/'Run this as part of your deployment pipeline, before updating the Lambda function:
python manage.py collectstatic --noinputUser-uploaded media files
Media files (user avatars, document uploads, images) also go to S3, never to Lambda’s /tmp directory. The /tmp directory is not shared between execution environments and is wiped between cold starts. It is useless for anything a user needs to retrieve later.
For uploads, use django-storages with a separate media bucket, or generate pre-signed S3 URLs and let clients upload directly to S3 without routing through Lambda at all. The direct upload pattern is more efficient for large files.
Put CloudFront in front of your static files S3 bucket. It caches files at edge locations globally, adds HTTPS with a custom domain, and reduces S3 API costs. Point STATIC_URL to your CloudFront distribution domain, not the raw S3 URL.
Database connections and RDS Proxy
This is the most common production failure point for Django on Lambda. Here is why it happens.
A PostgreSQL instance has a connection limit, typically 100 to 200 for a small RDS instance. In a traditional deployment on EC2 or ECS, your app has a small fixed number of processes, each holding one or two connections. Predictable, manageable.
In Lambda, each execution environment is a separate process. If 200 requests arrive simultaneously, Lambda spins up 200 execution environments, each opening its own database connection. The database hits its limit immediately, new connections fail, and requests error out. This happens faster than you would expect, and at lower traffic volumes.
If you skip RDS Proxy, your database will hit its connection limit under any real concurrent traffic. This is not a theoretical edge case. It is the number one reason Django on Lambda breaks in production.
Solution: RDS Proxy
Think of RDS Proxy like a hotel reception desk. Hundreds of guests (Lambda executions) all want access to the back office (the database). Without reception, they crowd the back door directly and overwhelm the staff. The front desk queues them, handles the interaction, and keeps the back office running at a sane pace.
Lambda connects to RDS Proxy (fast, lightweight). The proxy maintains a pool of real connections to your RDS instance. Two hundred Lambda executions all connect to the proxy; the proxy uses a pool of perhaps 20 real database connections. The database never sees more than the configured pool size.
Point Django’s database config at the RDS Proxy endpoint instead of the RDS endpoint directly:
# settings.py
import os
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'HOST': os.environ['DB_PROXY_ENDPOINT'], # RDS Proxy, not RDS directly
'PORT': '5432',
'NAME': os.environ['DB_NAME'],
'USER': os.environ['DB_USER'],
'PASSWORD': os.environ['DB_PASSWORD'],
'OPTIONS': {
'connect_timeout': 5,
},
'CONN_MAX_AGE': 0, # Close connection after each request
}
}CONN_MAX_AGE: 0 is important. Django normally reuses database connections within the same process. In Lambda, frozen execution environments hold the connection open until it goes stale. Setting CONN_MAX_AGE to zero tells Django to open and close a connection per request. With RDS Proxy, this is inexpensive because the proxy handles the real pooling.
Never call python manage.py migrate inside your Lambda handler. Migrations can take longer than Lambda’s timeout, and running them during cold starts causes race conditions when multiple instances start simultaneously. Run migrations as a dedicated step in your CI/CD pipeline before deploying new Lambda code.
Cold starts and package size
A cold start is like booting a laptop from scratch rather than waking it from sleep. The first request takes a few seconds while Lambda imports Django, loads settings, discovers installed apps, and initialises the ORM. Subsequent requests on the same warm environment are fast.
For a medium-sized Django app, expect cold starts of 1–3 seconds. Cold starts matter for user-facing requests but are irrelevant for background jobs or rarely-invoked functions.
If your Django app cold starts consistently exceed 3 seconds and you have user-facing endpoints, either enable provisioned concurrency or move to App Runner. Do not accept slow first requests as a fact of life if they affect real users.
For latency-sensitive endpoints, your options are:
- Provisioned concurrency: Lambda keeps a set number of warm instances ready (extra cost)
- Keep the package small: fewer imports means faster initialisation
- Move to App Runner or ECS: persistent containers have no cold start at all
Package size
Lambda has a 250 MB unzipped limit for zip deployments. A typical Django app with PostgreSQL support and django-storages fits comfortably. The limit becomes an issue when you add heavier libraries:
| Dependencies | Approximate size | Fits in zip? |
|---|---|---|
| Django + psycopg2 + django-storages | ≈50 MB | Yes |
| + Pillow | ≈80 MB | Yes |
| + pandas + numpy | ≈200 MB | Tight |
| + scipy or OpenCV | ≈300+ MB | No — use container image |
When you exceed the zip limit, switch to a Lambda container image. Package your app as a Docker image, push it to ECR, and deploy it as a Lambda container. Lambda supports images up to 10 GB, and image layers are cached, which can also reduce cold start times for large apps.
# Dockerfile for Django on Lambda
FROM public.ecr.aws/lambda/python:3.12
WORKDIR ${LAMBDA_TASK_ROOT}
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["lambda_handler.handler"]The container images in AWS guide covers building and pushing images to ECR.
If large native dependencies are a recurring concern, this is often the signal to move entirely to a container platform. App Runner or ECS remove the size constraint entirely and add persistent process benefits on top.
Django settings for Lambda
A few settings need adjusting for the Lambda environment:
# settings.py
import os
SECRET_KEY = os.environ['DJANGO_SECRET_KEY'] # Never hardcode
DEBUG = False # Always False in Lambda
ALLOWED_HOSTS = [
os.environ.get('API_GATEWAY_DOMAIN', ''),
'.execute-api.us-east-1.amazonaws.com',
]
# Static files go to S3, not the local filesystem
STATICFILES_DIRS = []
# /tmp is the only writable directory in Lambda
MEDIA_ROOT = '/tmp/media/'
# Logging to stdout; CloudWatch captures it automatically
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {'class': 'logging.StreamHandler'},
},
'root': {
'handlers': ['console'],
'level': 'WARNING',
},
'loggers': {
'django': {
'handlers': ['console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
'propagate': False,
},
},
}Common mistakes
- No RDS Proxy. Without RDS Proxy, Django on Lambda exhausts database connections under any real load. This is the most common production failure. Add it before you see real traffic, not after.
- CONN_MAX_AGE greater than zero. Persistent connections go stale in frozen Lambda environments. Set
CONN_MAX_AGE: 0and let RDS Proxy handle pooling. - Serving static files from Lambda. Lambda cannot serve static files from its filesystem. Use S3 with django-storages. Always.
- Running migrations inside the handler. Migrations inside Lambda risk timeouts and race conditions. Run them from CI/CD before deploying.
- Treating Lambda like a long-running server. Lambda has no persistent state between invocations beyond what is in the database or cache. Do not use module-level variables to store state that must survive across requests.
- Ignoring cold starts. A first request hitting a cold Django Lambda can take 2–5 seconds. If that is unacceptable, use provisioned concurrency or move to a container platform.
- Hardcoding secrets. Never hardcode SECRET_KEY, database passwords, or API keys in your Lambda code. Use Lambda environment variables or AWS Secrets Manager.
Django on Lambda vs App Runner vs ECS
Lambda is not always the right compute platform for Django. Here is a direct comparison with the two most common alternatives:
| Lambda | App Runner | ECS (Fargate) | |
|---|---|---|---|
| Traffic pattern | Spiky, variable | Consistent, low-to-medium | High volume, sustained |
| Cold starts | Yes (1–3s for Django) | No | No |
| WebSockets | Not natively | Yes | Yes |
| Celery workers | No (needs separate infra) | Yes (separate service) | Yes (separate task) |
| Package size limit | 250 MB zip / 10 GB image | None (container) | None (container) |
| Ops overhead | Very low | Low | Medium |
| Cost model | Pay per request | Pay per hour (vCPU + memory) | Pay per container hour |
| Best for | Variable traffic, low ops budget | Consistent traffic, easy container deploy | High throughput, full control |
The Lambda vs ECS vs EC2 decision guide goes deeper on when to pick each platform. The App Runner overview covers the managed container path in detail.
Summary
- Django on Lambda works via Mangum (ASGI) or Zappa (WSGI). Mangum gives more control; Zappa automates more of the packaging and deployment.
- Static files and media must live in S3. Use django-storages and CloudFront. Lambda cannot serve them from its filesystem.
- RDS Proxy is essential for production. Without it, Lambda exhausts database connections under any real concurrency.
- Set
CONN_MAX_AGE: 0in Django’s database config. RDS Proxy handles the real pooling. - Run database migrations from CI/CD, not from the Lambda handler.
- If dependencies exceed 250 MB, switch to Lambda container images. If the app needs WebSockets, Celery workers, or sustained high throughput, App Runner or ECS is the better choice.
Frequently asked questions
Can Django run on AWS Lambda?
Yes. Django runs inside Lambda using an ASGI or WSGI adapter: Mangum for ASGI, Zappa for WSGI. The main challenges are static files, database connections under concurrency, and package size. For the right workload, it works well in production.
Is Django on Lambda suitable for production?
Yes, for the right workloads. Admin dashboards, internal tools, small-to-medium APIs, and spiky-traffic apps are good fits. Sustained high-throughput traffic, WebSocket apps, and Celery-heavy systems are not. Always use RDS Proxy with a relational database.
How do I handle static files with Django on Lambda?
Lambda cannot serve static files from its filesystem. Use django-storages to point Django at an S3 bucket, run collectstatic as part of your deployment, and put CloudFront in front of S3 for caching and HTTPS.
Why do database connections fail under load?
Each Lambda execution environment opens its own database connection. Under high concurrency, this exhausts the database connection limit quickly. RDS Proxy solves this by pooling real connections so Lambda connects to the proxy, not the database directly.
When should I use App Runner or ECS instead of Lambda?
Use App Runner or ECS when your Django app has sustained high traffic, uses WebSockets, relies on Celery workers, or has large native dependencies. These platforms run your app as a persistent container and avoid Lambda cold start, connection exhaustion, and size limitations.