Auto Scale a Django Service on Amazon ECS
Configure ECS service auto scaling for Django web and Celery services: target tracking on CPU or ALB requests per target, step scaling on SQS or custom queue metrics, sensible min and max tasks, cooldowns, database connection limits, and graceful shutdown.
Problem statement
Your Django app runs on ECS (Fargate or EC2) with a fixed number of tasks. At peak hours response times climb; at night you pay for idle tasks. Celery queues back up during imports and sit empty otherwise. ECS service auto scaling adjusts task counts automatically, but scaling on the wrong metric, without limits that respect the database, or without graceful shutdown leads to flapping, dropped requests, and connection exhaustion.
- Register the service as a scalable target with
min(at least 2 for web) andmaxcapacity. - Web: target tracking on
ALBRequestCountPerTargetor average CPU (for example 60 percent). - Celery: scale on queue depth (SQS
ApproximateNumberOfMessagesVisible, or a custom CloudWatch metric for Redis queues). - Cap
maxso total database connections (tasks × workers × threads) stay under the database limit, or add RDS Proxy. - Handle SIGTERM gracefully and set the ALB deregistration delay so scale-in does not drop requests.
Step-by-step solution
1. Register the scalable target
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--resource-id service/prod/shop-web \
--scalable-dimension ecs:service:DesiredCount \
--min-capacity 2 --max-capacity 12
Two tasks minimum gives redundancy across availability zones and during deployments.
2. Scale web tasks on requests or CPU
aws application-autoscaling put-scaling-policy \
--service-namespace ecs --resource-id service/prod/shop-web \
--scalable-dimension ecs:service:DesiredCount \
--policy-name web-requests --policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 300,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ALBRequestCountPerTarget",
"ResourceLabel": "app/shop-alb/abc123/targetgroup/shop-web/def456"
},
"ScaleOutCooldown": 60,
"ScaleInCooldown": 300
}'
Requests per target tracks load directly for I/O-bound Django apps, where CPU stays low even when workers are saturated. For CPU-heavy apps, ECSServiceAverageCPUUtilization with a 60 percent target works. Find the right target value with a load test: the request rate per task at which p95 latency starts to rise.
3. Scale Celery workers on queue depth
With SQS as the broker, use step scaling on ApproximateNumberOfMessagesVisible, or target tracking on backlog per task (messages divided by running tasks via metric math). With Redis, publish the queue length as a custom metric from a small scheduled task:
import boto3, redis
r = redis.from_url(os.environ["REDIS_URL"])
boto3.client("cloudwatch").put_metric_data(
Namespace="Shop/Celery",
MetricData=[{"MetricName": "QueueLength", "Dimensions": [{"Name": "Queue", "Value": "default"}],
"Value": r.llen("default")}],
)
Scale out when the backlog per worker exceeds what one task clears in a few minutes; scale in slowly.
4. Respect database limits
Every new task opens database connections: Gunicorn workers × threads per web task, Celery concurrency per worker task. Compute the maximum:
web: 12 tasks x 3 workers x 4 threads = 144
celery: 8 tasks x 4 concurrency = 32
total 176 (+ migrations, admin, cron)
If that exceeds max_connections, lower max capacity, reduce per-task concurrency, or add RDS Proxy or PgBouncer.
5. Scale in without dropping requests
On scale-in, ECS deregisters the task from the target group, waits the deregistration delay, then sends SIGTERM and, after stopTimeout, SIGKILL. Gunicorn finishes in-flight requests on SIGTERM within its graceful_timeout. Set the target group's deregistration delay (for example 30 seconds) and stopTimeout longer than your slowest request. For Celery, acks-late with idempotent tasks makes killed tasks safe to retry.
Explanation
Application Auto Scaling adjusts the ECS service's desired count based on CloudWatch alarms tied to your policies. Target tracking keeps a metric near a target value by adding or removing tasks; step scaling reacts to alarm thresholds with defined increments. Because each Django task holds database connections and in-flight requests, safe autoscaling depends on capacity limits that protect the database and on shutdown behavior that drains tasks before stopping them.
Edge cases / notes
- Deployments. Scaling and deployments interact; during a rolling deployment, ECS honors the desired count changes, so set
minimumHealthyPercentto keep capacity. - Cold starts. Fargate tasks take time to pull images and start; keep images small and scale out early with lower targets.
- Scheduled scaling. For predictable peaks (business hours, campaigns), scheduled actions raise the minimum ahead of time.
Next steps
- Review the base deployment in Deploy Django on AWS ECS Fargate.
- Compare with Kubernetes in Autoscale Django on Kubernetes With HPA.
- Make shutdowns clean with Django Graceful Shutdown on SIGTERM.
FAQ
Should Django web tasks scale on CPU or requests?
Requests per target for most Django apps, which are I/O-bound; CPU for compute-heavy apps.
How do I scale Celery on ECS?
On queue depth: SQS metrics directly, or a custom CloudWatch metric for Redis queue length.
Why did scaling out cause database errors?
New tasks added connections beyond the database limit. Cap max tasks or add a pooler like RDS Proxy.
How do I avoid dropping requests during scale-in?
Use an ALB deregistration delay and a stop timeout longer than your slowest request, with Gunicorn handling SIGTERM gracefully.