Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errorsScale a Django app on a VPS in stages: make one server production-safe, measure its real bottleneck, optimize Django and PostgreSQL, then add workers, shared services, or additional hosts only when the measurements justify them. There is no universal VPS size, Gunicorn worker count, or requests-per-second threshold; your code, query mix, payloads, and resource limits determine the answer.
Antihost offers Linux VPS plans with full root access, unlimited traffic on a 1 Gbps port and a 14-day free trial.
What “scaling a Django app on a VPS” actually involves
Scaling is not simply increasing the number of Gunicorn workers. A web request travels through several capacity limits: the reverse proxy, application processes, Python code, database queries, cache, background jobs, storage, and the network. Improving one layer can expose a different limit. For example, adding workers may increase concurrent requests until PostgreSQL runs out of connections, or until the VPS begins swapping.
As an Amazon Associate I earn from qualifying purchases.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
A practical progression is:
- Build a production-safe single VPS.
- Measure latency, throughput, errors, and resource use under a repeatable workload.
- Remove inefficient queries and application work.
- Set application-process and database-connection budgets together.
- Add shared caching and a background queue when their workload requires them.
- Separate PostgreSQL, Redis, workers, or the web tier only when co-location is demonstrably limiting performance or availability.
This sequence usually costs less and creates fewer failure modes than adopting multiple servers or Kubernetes before the application needs them.
Stage 1: Make the single VPS production-safe
Use a real application server
Django’s runserver command is a lightweight development server and is not suitable for production. Deploy through a WSGI or ASGI server instead. WSGI is the conventional synchronous interface; ASGI is the asynchronous-friendly interface used by asynchronous Python and Django features. Choose the interface that matches your code and protocol requirements, then run it under a supervisor such as systemd.
#1 Best Overall
Gunicorn is a common WSGI choice. Uvicorn or Daphne can serve ASGI applications. The application server should listen on a private address or Unix socket, not expose itself directly to the public internet.
Put Nginx in front
Gunicorn’s deployment guidance strongly recommends a proxy server. Nginx can terminate TLS, serve static files, enforce request limits, and forward dynamic requests to the application server. With Gunicorn’s synchronous workers, configure proxy buffering so slow clients do not occupy application workers while they receive a response.
Free tools Windows power users keep installed
One-click scans. No signup required.
Apply Django’s deployment checks
Run the built-in check before going live:
python manage.py check --deploy
Resolve every warning that applies to your deployment. At minimum:
- Set
DEBUG=False. - Set
ALLOWED_HOSTSto the domains that should serve the application. - Keep the secret key and database credentials outside source control, supplied through protected environment variables or a secret manager.
- Force HTTPS at the proxy and configure Django’s secure-cookie and proxy settings consistently.
- Define static-file collection and delivery separately from dynamic requests.
- Choose an explicit media-upload strategy and protect private media from unauthorised access.
- Configure structured error reporting and logs before traffic increases.
Supervise every long-running process
Run Gunicorn or the ASGI server with systemd (or an equivalent supervisor) so it starts after reboot, restarts on failure, and records logs. Apply the same supervision to Celery workers and beat processes when you introduce them. A process that silently exits is an availability incident, not a scaling strategy.
Back up the database and test recovery
Schedule PostgreSQL backups, retain them separately from the VPS, and periodically perform a restore into a disposable environment. Record the recovery steps and the expected recovery point and recovery time. A backup that has never been restored is an assumption, not a recovery plan.
Stage 2: Establish a measurable baseline
Do not choose a worker count or a larger VPS from a rule of thumb. Run a repeatable load test against a representative staging or controlled production workload and compare every change with the same test conditions.
| Measure | What it tells you | Useful warning signal |
|---|---|---|
| p50, p95, and p99 request latency | Typical and tail-user experience | Tail latency rising while the median remains stable often indicates queueing or a slow dependency. |
| Throughput | Requests or jobs completed over time | Throughput stops increasing when a shared resource is saturated. |
| Error rate and status codes | Whether load causes failures | Timeouts, 5xx responses, and connection errors identify different classes of pressure. |
| CPU utilisation and load | Whether Python, PostgreSQL, or another process is compute-bound | Sustained saturation leaves no headroom for bursts. |
| Memory and swap activity | Whether processes fit in RAM | Swap activity can turn a modest increase in concurrency into severe latency. |
| Disk I/O and storage latency | Whether database or log writes are waiting on storage | High I/O wait makes adding CPU ineffective. |
| Database time, slow queries, and connection count | Whether PostgreSQL is the limiting tier | Connection exhaustion and lock waits can look like web-server slowness. |
| Cache hits and misses | Whether cached work is actually being reused | A low hit rate may mean poor keys, short expirations, or process-local caches. |
| Background queue depth and task age | Whether asynchronous work is keeping up | A growing oldest-task age means workers or dependencies need attention. |
Keep the workload definition with the results: endpoints, authentication state, payload sizes, database fixture size, concurrency, and test duration. Without those conditions, a requests-per-second number cannot be compared meaningfully.
Stage 3: Fix Django and PostgreSQL inefficiency first
Find expensive query patterns
Inspect Django’s generated queries and PostgreSQL execution plans. Common causes of avoidable load include N+1 queries in templates or serializers, selecting columns that are never used, fetching unbounded result sets, repeating the same lookup inside a loop, and sorting or filtering without a suitable index.
Use select_related() for appropriate single-valued relationships and prefetch_related() for collections when they reduce repeated queries. Paginate large lists instead of returning every row. Select only the fields needed by a particular code path when reducing row width is material. Confirm each change with query counts, execution plans, and end-to-end latency rather than assuming that fewer ORM calls always means a faster request.
Add indexes for real access paths
Index columns used frequently in filters, joins, and ordering, including combinations that match actual query predicates. Verify the plan after creating an index. Indexes consume storage and add work to inserts, updates, and deletes, so indexing every column can make a write-heavy application worse. Django’s documentation notes that index decisions depend on both the database and the application.
Rank #3
Control result and transaction size
Use bounded queries and stream or batch large exports. Keep transactions no longer than necessary; long transactions can retain locks and prevent PostgreSQL from reclaiming space efficiently. Separate administrative reports and bulk jobs from latency-sensitive web requests when their resource use competes with user traffic.
How many Gunicorn workers and threads should you run?
There is no honest universal formula. A worker that handles CPU-heavy Python code consumes a different amount of CPU and memory from one that mostly waits for a database. Payload size, middleware, imported modules, request mix, and VPS limits all change the result.
Start conservatively, measure, and increase capacity only while CPU, memory, and database connections retain headroom. Record the resident memory of one worker after the application is fully loaded; multiply it by the number of processes and add PostgreSQL, Nginx, monitoring, and operating-system overhead. If the total approaches available RAM, adding workers will cause swapping rather than useful concurrency.
Threads and database connections must be budgeted together. Django documents that each thread can maintain a database connection. Therefore, calculate the maximum possible connections across all Gunicorn workers and threads, account for management or migration processes, and keep the total below PostgreSQL’s configured connection limit with safety margin. CONN_MAX_AGE changes how long connections remain reusable; it does not remove the need to respect the database limit.
| Change | Potential benefit | Risk to watch |
|---|---|---|
| More worker processes | More simultaneous requests and isolation between processes | Higher memory use, CPU contention, and database connections |
| More threads per worker | Useful for workloads that spend time waiting on I/O | Additional connections and contention; little help for CPU-bound Python code |
| Longer persistent database connections | Less connection setup overhead | More connections held open and greater pressure on PostgreSQL |
| More VPS CPU or RAM | Headroom without changing application topology | Does not fix inefficient queries, slow storage, or a single-host failure domain |
Change one variable at a time. Compare p95 and p99 latency, error rate, memory, CPU, and database connection usage under the same workload. If latency worsens after adding workers, the database or memory budget—not the web tier—may be the constraint.
When to add Redis or Memcached
Django’s default local-memory cache is process-local. Two Gunicorn workers can hold different values, and two VPS instances will certainly not share them. Use a shared cache such as Redis or Memcached when workers or hosts need a common view of cached data.
Choose what belongs in the cache
- Cache expensive, repeatable reads with a defined expiration.
- Cache rendered fragments or computed permissions only when invalidation is understood.
- Use shared sessions when requests can reach different application instances.
- Do not cache user-specific or security-sensitive data under keys that can collide.
Design invalidation and failure behaviour
Define key names, expiration times, and invalidation triggers before enabling caching. Decide whether a cache outage should fail open by recomputing data or fail closed for a particular feature. Monitor hit rate, memory use, evictions, command latency, and stale-data incidents.
Keep Redis or Memcached off the public internet, restrict access with network rules and authentication where supported, and use encryption when traffic crosses an untrusted network. A shared cache can also act as a Celery broker, but broker traffic and web caching should be monitored separately because a queue surge can evict useful cache entries or consume the service’s memory.
When to introduce Celery and background workers
Move slow, retryable, or non-interactive work out of the request path. Typical examples include email delivery, image processing, video or data exports, scheduled jobs, and calls to unreliable external services. The web request should enqueue a task and return a clear status rather than hold a Gunicorn worker while the operation runs.
- Define a task boundary and the input it needs; pass identifiers rather than large mutable objects where practical.
- Choose and secure a broker, commonly Redis or another supported message service.
- Run Celery workers under a supervisor and separate scheduled-task scheduling from ordinary workers.
- Make tasks idempotent so a retry does not duplicate an email, charge, or database mutation.
- Set retry limits, backoff, timeouts, and a dead-letter or failure-review process.
- Alert on queue depth, oldest-task age, task failure rate, and worker availability.
Worker concurrency is workload-specific. CPU-heavy image processing needs a different setting from I/O-heavy API calls. Increasing concurrency can starve PostgreSQL or the web tier, so include worker CPU, memory, and database usage in the same capacity budget.
Best Value
Keep PostgreSQL, Redis, and the web tier together—or separate them?
Co-location is often the right starting point for a small application: it minimises network latency, monthly cost, and operational overhead. Separation becomes worthwhile when measurements show contention or when one host’s failure domain is unacceptable.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →| Topology | Best fit | Trade-offs to evaluate |
|---|---|---|
| One VPS for web, PostgreSQL, cache, and workers | Early production, modest traffic, and a team that values simple operations | Lowest network complexity and cost, but shared CPU, RAM, disk, and a single-host outage |
| One VPS with logically separated services | Applications needing clearer process limits without another host | Still one failure domain; requires resource controls and careful monitoring |
| Web VPS plus separate PostgreSQL host or managed database | Database CPU, I/O, storage, backups, or connections dominate | Network latency, connection limits, backup and point-in-time recovery features, failover behaviour, egress cost, and provider dependence |
| Web VPS plus separate Redis or cache service | Cache or broker contention affects web requests, or cache availability must be isolated | Additional network path, credentials, monitoring, and service cost |
| Several app VPS instances behind a load balancer | One host cannot provide required concurrency or availability | Shared sessions and cache, deployment coordination, database connection multiplication, health checks, and capacity during rolling changes |
Move PostgreSQL when database resource use, storage growth, backup windows, or connection pressure is the measured bottleneck—not merely because a second server sounds more scalable. Move Redis when its memory, eviction, broker, or command latency affects request handling. Keep static and media files on storage or CDN infrastructure when local disk capacity or outbound bandwidth limits the application host.
Scaling to multiple application VPS instances
A second application host improves web concurrency and can support rolling capacity changes, but it changes application assumptions.
- Store sessions in a shared backend or use a deliberately configured affinity strategy; local in-process sessions do not follow a user between hosts.
- Use shared cache data when a value must be consistent across instances.
- Keep uploaded media and collected static files on shared or object storage rather than one host’s filesystem.
- Make deployments repeatable so every instance runs the same code, migrations, settings, and static assets.
- Configure load-balancer health checks that test meaningful readiness without performing destructive work.
- Account for every instance’s database connections; doubling web hosts can double connection pressure even when request volume is unchanged.
Use graceful draining during deployments so existing requests finish before an instance is removed. Verify that WebSocket or long-lived ASGI connections are handled by the proxy and load balancer you select.
Static files, media, and the disk bottleneck
Serve versioned static assets directly from Nginx or storage/CDN infrastructure instead of consuming Gunicorn workers for them. User media requires a separate policy: public assets can be cached aggressively, while private files need authenticated delivery or signed, time-limited access.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWatch disk space, inode use, read/write latency, database WAL growth, log volume, and outbound bandwidth. A VPS with abundant CPU can still fail when a full disk prevents PostgreSQL or logging from writing. Rotate logs, set retention, and alert before capacity is exhausted.
Operational safety while scaling
- Apply security updates and restart services through a documented procedure.
- Keep configuration in version control without committing secrets.
- Use separate credentials and least-privilege database roles for the application, migrations, and administration.
- Restrict PostgreSQL, Redis, and management interfaces to private networks or explicit allowlists.
- Track deployment version, migration status, process restarts, and configuration changes alongside performance metrics.
- Test rollback as well as deployment. A failed migration or incompatible worker can overload every instance at once.
A practical decision framework
- Users report slow pages: check p95/p99 latency and error logs first. Identify whether time is spent in Django, PostgreSQL, cache, external services, or queueing.
- CPU is high but database time is low: profile Python and template or serialization work, then test a modest worker increase only if memory remains safe.
- Memory is low or swap is active: reduce process and thread counts, investigate per-request and per-worker memory, or add RAM before adding concurrency.
- Database connections or lock waits are high: reduce the application connection budget, inspect transaction duration and queries, and consider a separate or managed database if PostgreSQL resources are the sustained limit.
- Repeated reads dominate: optimise the query first, then introduce shared caching with explicit expiration and invalidation.
- Requests wait on exports, email, or processing: enqueue idempotent Celery tasks and monitor queue age.
- One host cannot meet concurrency or availability goals: add a second application instance behind a load balancer only after sessions, cache, media, deployment, and database-connection behaviour are designed for multiple hosts.
Common scaling mistakes and their fixes
| Symptom | Likely mistake | Fix |
|---|---|---|
| Different users see inconsistent cached values | Relying on Django’s local-memory cache across workers | Use a shared cache and define invalidation. |
| Adding workers makes latency worse | CPU, RAM, storage, or PostgreSQL connections were already saturated | Return to the measured budget and remove the actual bottleneck. |
| Requests time out during exports | Long work runs inside the request process | Move it to a supervised Celery task with progress or completion status. |
| Static files consume application capacity | Gunicorn serves assets that Nginx or a CDN could deliver | Collect and serve static content separately. |
| Two app hosts lose sessions or uploads | State is stored on one instance’s filesystem or memory | Use shared sessions, cache, and media storage. |
| Backups exist but recovery fails | No restore test or documented recovery sequence | Perform scheduled restore drills and record recovery objectives. |
Do you need Kubernetes?
Not for most Django applications that are still scaling a single VPS. Kubernetes adds networking, image management, health probes, secrets, persistent storage, observability, upgrades, and cluster operations. Those capabilities can be valuable for a large platform with multiple services and a team prepared to operate them, but they do not fix an N+1 query, an undersized database, or an unbounded export. A well-monitored VPS or a small number of VPS instances is usually the simpler and more reliable next step.
Bottom line
Start with Nginx, a production WSGI or ASGI server, supervised processes, secure Django settings, backups, and observability. Measure the complete request path, fix query and indexing problems, and treat worker, thread, and database connections as one budget. Add shared Redis or Memcached, Celery, separate services, or multiple VPS instances only when the metrics show that the current topology is the constraint. That approach increases capacity without turning avoidable complexity into an outage.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




