Python web apps have specific resource patterns that don't match Node, PHP, or Go. The Global Interpreter Lock means each Python worker uses exactly one core. Database query patterns matter more than CPU speed. And the deploy story (gunicorn behind Nginx, with systemd or supervisor managing processes) is well-established and well-supported. This guide covers what Django, Flask, and FastAPI apps actually need from a VPS, how to size workers, and the configuration that scales smoothly from one user to a few thousand.

What's in this guide

  1. What Python web apps actually need
  2. Sizing by app and traffic level
  3. Worker counts and the GIL
  4. WSGI vs ASGI (Django, Flask vs FastAPI)
  5. The production Python stack
  6. Database hosting (same VPS or separate?)
  7. Region selection
  8. Common mistakes
  9. FAQ

What Python web apps actually need

Python web app resource profile:

The pattern: Python web apps need balanced specs (RAM + CPU + storage IOPS), not extreme-anything. A modest VPS with all three balanced typically beats an unbalanced bigger one.

Sizing by app and traffic level

Django app with reasonable database size and typical traffic:

Traffic levelRAMvCPUDiskWorkersPlan
Hobby / dev (under 1k visitors/day)1GB1 vCPU20GB2-3Starter
Small production (1k-10k/day)2GB1-2 vCPU40GB3-5Pro
Mid (10k-50k/day)4GB2 vCPU80GB5-9Premium
Real (50k-200k/day)8GB4 vCPU160GB9-17Higher tier
Heavy (200k+/day)16GB+4-8 vCPU320GB+Multiple processes / scale-outMulti-VPS or dedicated

FastAPI/uvicorn apps tend to handle slightly more concurrency per worker than Django/gunicorn (because of async I/O). Apps with heavy ML model loading or image processing need more RAM than the table suggests.

Worker counts and the GIL

Python's Global Interpreter Lock (GIL) means a single Python process executes Python bytecode on one thread at a time. To use multiple CPU cores, you run multiple worker processes.

The standard formula:

# Gunicorn worker count
workers = (2 * num_cores) + 1

So a 2 vCPU VPS gets 5 workers; a 4 vCPU VPS gets 9 workers. The "+1" overlaps slightly to avoid blocking when a worker waits on I/O.

Each worker uses ~100-200MB RAM. Five workers on a 2GB VPS: 500MB-1GB workers + 200MB OS + 300-500MB Postgres = within limits. Nine workers on a 2GB VPS would crowd things — bump to 4GB for that worker count.

For async ASGI apps (FastAPI, Django Channels), each worker can handle many concurrent connections. You typically run fewer worker processes (one per core or so) because each one is more efficient.

WSGI vs ASGI (Django, Flask vs FastAPI)

Two protocols, two server stacks:

WSGI (Django, Flask, traditional)

Synchronous request/response. Each worker processes one request fully before moving to the next. Server: gunicorn (most common) or uwsgi.

Resource use: predictable. Each worker is a full Python process holding state.

Best for: traditional CRUD apps, content sites, admin interfaces. The vast majority of Django apps.

ASGI (FastAPI, Starlette, Django async views)

Asynchronous. Each worker handles many concurrent requests via the event loop. Server: uvicorn, optionally with gunicorn as a process manager.

Resource use: each worker handles more concurrent connections. Better for I/O-bound workloads (lots of API calls, database queries with high latency).

Best for: WebSocket apps, high-concurrency APIs, real-time apps.

Modern Django supports ASGI but most existing Django code is sync. Switching is straightforward for new projects, painful for legacy code.

The production Python stack

The standard production architecture:

Internet
   ↓
Nginx (reverse proxy, SSL, static files)
   ↓
Gunicorn / Uvicorn (WSGI/ASGI server)
   ↓
Your Django/Flask/FastAPI app
   ↓
PostgreSQL (database, on same VPS or separate)

Setup outline:

  1. Install Python 3.11+ and your app's dependencies (use a virtualenv, not system Python).
  2. Install Nginx as the reverse proxy with SSL.
  3. Install gunicorn (or uvicorn for ASGI).
  4. Use systemd to manage the gunicorn/uvicorn process (auto-restart, start on boot).
  5. Install PostgreSQL or MariaDB, tune the buffer pool to your RAM.
  6. Use Redis for caching and async task queues (Celery).

Our Nginx reverse proxy guide covers the front end; the full Python deployment recipe is its own future tutorial post.

Database hosting (same VPS or separate?)

For most Python apps under 50k daily visits, hosting the database on the same VPS as the app is fine. Postgres on the same box as Django queries via the local socket — fastest possible communication, no network overhead.

Move the database to a separate VPS when:

For small-to-medium apps, a single 4GB VPS comfortably runs Django + Postgres + Redis without splitting them. Don't pre-split before you need to.

Region selection

Latency-to-user matters for Python web apps the same way it does for any web app. Pick the region closest to your audience:

For globally distributed audiences, pick the largest user concentration's region and put a CDN in front of static assets. All OliveVPS regions →

Python apps that scale gracefully

NVMe storage for fast database queries, dedicated CPU for predictable response times, 20 regions to put the app close to users. Starting at $3.99/mo.

See VPS Plans →

Common mistakes

Running too many workers on too little RAM. The (2 × cores) + 1 formula assumes adequate RAM. If each worker is 200MB and you have 1GB total, you can't run 5 workers — you'll OOM. Either reduce workers or upgrade RAM.

Using Django's runserver in production. python manage.py runserver is for development only. It's single-threaded, has no graceful shutdown, and explicitly says "do not use in production" in the docs. Use gunicorn.

Skipping database connection pooling. Each Django request opens a database connection. At high concurrency, the database runs out of connections. Use pgbouncer (for Postgres) or set CONN_MAX_AGE in Django settings to enable connection reuse.

Not caching anything. Django + Redis cache wins free performance. The Django cache framework caches view results, querysets, or arbitrary data. A trivial setup change can 5x your throughput on read-heavy pages.

Mishandling static files. Don't serve static files through Django in production — let Nginx serve them directly. collectstatic + Nginx location /static/ block is standard.

Forgetting about Celery memory. If you're using Celery for background tasks, those workers are additional Python processes consuming RAM. Plan for them.

FAQ

Can I run Django on a 1GB VPS?

Small Django apps (hobby projects, low-traffic sites): yes, with 2-3 gunicorn workers and Postgres on the same box. The 1GB plan starts feeling tight if your app loads ML models or has many heavy dependencies. For real production, 2GB is more comfortable.

Should I use uWSGI or Gunicorn?

Gunicorn is the modern default. Simpler config, better community support, just-as-good performance for typical workloads. uWSGI is more configurable and slightly faster for niche cases, at the cost of complexity. New projects: gunicorn.

What about Cloudflare workers / serverless Python?

Different deployment model. Serverless suits stateless API endpoints with bursty traffic. For traditional Django apps with persistent connections to a database, traditional VPS hosting is simpler and usually cheaper. Many Python projects migrate from serverless to VPS once they get steady traffic.

Do I need separate VPS for Postgres?

Not for most apps. A single 4GB VPS runs Django + Postgres + Redis comfortably for sites up to 50k daily visits. Split when the database working set crowds out app workers, or when you need horizontal scaling on the app tier.

Is Python slower than Node.js for web?

For pure CPU work in a single thread: Node is somewhat faster (V8 vs CPython). For web apps where the database is the bottleneck (as is typical): negligible difference. Pick the language you know — implementation quality matters more than language choice for typical web workloads.

🐱
The OliveVPS Team

We host plenty of Django and FastAPI apps. The patterns are remarkably consistent — and easy to get right with a good VPS.