Django vs Flask op een kleine VPS: geheugengebruik
Ontdek het werkelijke RAM-verbruik per Gunicorn worker voor Django en Flask. Wij berekenen hoeveel gelijktijdige workers uw 1 GB of 2 GB VPS daadwerkelijk kan ondersteunen.
Wat Django en Flask kosten op een kleine VPS
De keuze tussen Django en Flask op een kleine VPS is in de eerste plaats een kwestie van geheugengebruik. Django laadt bij elk worker-proces dat u start de object-relational mapper (ORM), de migratietools en, indien ingeschakeld, de admin-interface. Flask laadt enkel een router en een request-object. Op een server met 1 GB RAM bepaalt dit verschil hoeveel workers er in het geheugen passen, en het aantal workers bepaalt hoeveel verzoeken u gelijktijdig kunt afhandelen.
Deze kosten zijn alleen nadelig voor Django als u de functionaliteit die het biedt niet zelf hoeft te bouwen. Een applicatie met gebruikersaccounts, sessies en een admin-paneel vraagt om Django: het RAM-verbruik per worker is de prijs voor code die u niet zelf hoeft te schrijven. Een JSON API voor een bestaande datastore vraagt om Flask, omdat de extra functionaliteiten van Django in dat geval ongebruikt zouden blijven. Het is een kwestie van geschiktheid. De onderstaande metingen laten zien waar uw applicatie onder valt.
Hoeveel geheugen verbruikt één gunicorn worker?
The data behind this chart
[
{
"label": "Bare Python 3.12 process",
"rss_mb": 14,
"pss_mb": 9
},
{
"label": "Flask, one route",
"rss_mb": 42,
"pss_mb": 26
},
{
"label": "Flask + SQLAlchemy",
"rss_mb": 58,
"pss_mb": 38
},
{
"label": "Django, admin disabled",
"rss_mb": 78,
"pss_mb": 47
},
{
"label": "Django, admin enabled",
"rss_mb": 96,
"pss_mb": 58
}
]Dit zijn gangbare gepubliceerde cijfers voor een 'hello world'-applicatie van elk type op Ubuntu 24.04 met Python 3.12, drie gunicorn workers en ingeschakelde preload. Beschouw deze als een ondergrens, aangezien uw eigen imports hier nog bovenop komen. Een Django-worker met de admin ingeschakeld verbruikt 96 MB resident geheugen, terwijl het proportionele aandeel in het geheugen 58 MB bedraagt. Het verschil tussen deze twee getallen is het onderwerp van de volgende sectie.
Voer dezelfde meting uit op uw eigen systeem.
sudo apt update && sudo apt install -y python3-venv
python3 -m venv /srv/site1/.venv
/srv/site1/.venv/bin/pip install django gunicorn setproctitleInstalleer setproctitle. Wanneer dit aanwezig is, hernoemt gunicorn zijn processen naar gunicorn: master [site1] en gunicorn: worker [site1], waardoor de volgende commando's de workers op naam kunnen vinden in plaats van op basis van aannames.
pgrep -af gunicorn
ps -o pid,rss,args -p $(pgrep -d, -f 'gunicorn: worker')De kolom rss staat voor de resident set size in kilobytes: elke geheugenpagina die het proces momenteel in het RAM vasthoudt. Het optellen hiervan over alle workers geeft een te hoog getal, omdat een geforkte worker pagina's deelt met de parent en de siblings; dezelfde pagina wordt dus meerdere keren geteld. Vraag de kernel in plaats daarvan om de proportional set size (PSS), die elke gedeelde pagina verdeelt over de processen die deze toewijzen.
for pid in $(pgrep -f 'gunicorn: worker'); do awk -v p="$pid" '/^Pss:/ {printf "%s %d MB\n", p, $2/1024}' /proc/$pid/smaps_rollup; doneVoer dit uit als de gebruiker die de workers bezit, of met sudo. PSS is de kolom die u moet gebruiken voor uw budgettering, omdat PSS correct optelt en RSS niet.
Django is groter vanwege wat django.setup() doet. Het importeert elk item in INSTALLED_APPS, bouwt het applicatieregister op en instantieert elke modelklasse samen met een Python-object voor elk veld daarin. Het toevoegen van django.contrib.admin voert admin-autodiscovery uit, wat de admin-module van elke app importeert en de forms- en template-lagen mee naar binnen trekt. Een Flask-worker importeert Werkzeug en Jinja2, en stopt daar.
Eén eerlijke kanttekening: het framework vormt vaak het kleinste deel. Een worker die een cloud-SDK of numerieke bibliotheken importeert, draagt daar meer van bij dan van Django. Meet uw eigen applicatie voordat u concludeert dat het framework het probleem is.
Copy-on-write en waarom preload het aantal wijzigt
Het master-proces van Gunicorn splitst (forkt) de worker-processen. Direct na fork() deelt het child-proces elke geheugenpagina met het parent-proces, en de kernel kopieert een pagina pas wanneer een van beide partijen erin schrijft. Of het model-register van Django dus één of vier keer op de server aanwezig is, hangt af van aan welke kant van de fork het is opgebouwd.
Wanneer preload_app is uitgeschakeld, importeert elke worker uw applicatie nadat deze is geforkt, waardoor elk proces een eigen privékopie opbouwt. Wanneer het is ingeschakeld, importeert het master-proces de applicatie één keer en erven de workers die geheugenpagina's over.
import gc
bind = "unix:/run/site1/gunicorn.sock"
umask = 0o007
workers = 3
timeout = 30
preload_app = True
max_requests = 500
max_requests_jitter = 50
def when_ready(server):
gc.freeze()CPython werkt copy-on-write tegen. Elke object-header bevat een referentieteller, en het aanraken van een object schrijft naar die header. Gedeelde pagina's worden daarom één voor één gekopieerd wanneer de garbage collector door de heap loopt. gc.freeze() verplaatst alles wat tot dan toe is toegewezen naar een permanente generatie die de collector niet langer bezoekt, waardoor meer van die pagina's gedeeld blijven. when_ready is de juiste hook omdat deze wordt uitgevoerd na de preload en voordat de eerste worker wordt geforkt. Meet de PSS voor en nadat u dit toevoegt, aangezien de besparing afhangt van hoeveel van uw applicatie uit import-tijd-status bestaat.
Preload heeft één nadeel dat mensen tijdens de uitrol verrast. systemctl reload verstuurt HUP, en het gedocumenteerde gedrag van Gunicorn bij HUP is het herladen van de configuratie en het starten van nieuwe workers. Wanneer de applicatie is gepreload, wordt uw code niet opnieuw geïmporteerd, waardoor uw nieuwe release niet draait, ook al zijn de worker-processen nieuw. Gebruik systemctl restart na een codewijziging, of de reeks USR2 gevolgd door WINCH als u wilt dat de oude workers eerst hun taken afronden.
Hoeveel workers kan een 1 GB VPS realistisch draaien?
The data behind this chart
[
{
"label": "Ubuntu 24.04 base",
"ram_mb": 190
},
{
"label": "nginx",
"ram_mb": 12
},
{
"label": "PostgreSQL, default config",
"ram_mb": 120
},
{
"label": "Headroom you must leave",
"ram_mb": 150
},
{
"label": "Left for gunicorn workers",
"ram_mb": 550
}
]Dit zijn cijfers voor een systeem in rust dat niets bedient. Er blijft ongeveer 550 MB over voor applicatieworkers, en dat is nog voordat het eerste verzoek binnenkomt.
Deel dit nu door het verbruik, en wees pessimistisch. Een verzoek kost geheugen terwijl het wordt uitgevoerd: een queryset die enkele duizenden rijen laadt, gevolgd door het renderen van een template. De piek per worker ligt meestal rond het dubbele van het verbruik in rust, dus reken met dat dubbele. Django met de admin-interface op 58 MB in rust geeft u vier workers op deze server. Flask met SQLAlchemy op 38 MB geeft u er zeven.
De suggestie van (2 x cores) + 1 voor Gunicorn gaat ervan uit dat CPU de schaarse hulpbron is en RAM niet. Op een kleine VPS is dat onjuist. Eén gedeelde vCPU levert u bovendien minder dan de rekenkracht van één volledige core op wanneer de host druk is. Het is nuttig dit te begrijpen voordat u uw code de schuld geeft: CPU steal time door een drukke buurman is zichtbaar in top als de st-waarde.
Als uw views voornamelijk wachten op een database of een upstream API, zijn threads hier in het voordeel. --worker-class gthread --workers 2 --threads 4 biedt acht gelijktijdige verzoeken voor de geheugenkosten van twee workers, omdat threads één geladen kopie van de interpreter en het framework delen. Door de global interpreter lock helpen threads niet bij een view die veel CPU verbruikt.
Voorzie de server van swap. Een 1 GB VPS zonder swap verandert een geheugenpiek in een beëindigd proces, terwijl een swapfile dezelfde piek verandert in een traag verzoek.
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness = 10' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --systemBegrens vervolgens de applicatie zelf. MemoryMax=600M in de gunicorn-unit zorgt ervoor dat de kernel het geheugen terugneemt van de cgroup van uw app in plaats van een willekeurig proces op de hele server te beëindigen. Zo zorgt een uit de hand gelopen verzoek er niet voor dat u uw SSH-sessie verliest.
Koudestart- en herstartgedrag
The data behind this chart
[
{
"label": "Flask, one route",
"cold_start_ms": 90
},
{
"label": "Flask + SQLAlchemy",
"cold_start_ms": 260
},
{
"label": "Django, admin disabled",
"cold_start_ms": 480
},
{
"label": "Django, admin enabled",
"cold_start_ms": 720
}
]De opstartkosten worden tweemaal betaald: bij elke deploy en bij elke automatische herstart na een crash. Een minimale Flask-applicatie is gereed in ongeveer 90 ms, en Django met de admin-interface ingeschakeld duurt ongeveer 720 ms op dezelfde gedeelde vCPU. Beide zijn representatieve gepubliceerde cijfers. Meet uw eigen waarden, aangezien uw dependencies de grootste invloed hebben.
cd /srv/site1
DJANGO_SETTINGS_MODULE=site1.settings /srv/site1/.venv/bin/python -X importtime -c "import django; django.setup()" 2>&1 | tail -20De laatste regels tonen de traagste imports met cumulatieve microseconden. Voer voor Flask dezelfde flag uit op uw module: python -X importtime -c "import app".
Met preload ingeschakeld betaalt de master deze kosten eenmalig en start elke geforkte worker direct. Met preload uitgeschakeld betaalt elke worker deze kosten, en de timeout van gunicorn dekt zowel het opstarten als het verzoek. Een worker die niet binnen timeout seconden reageert, wordt beëindigd en vervangen. Hierdoor kan een zware applicatie op een trage gedeelde vCPU in een herstartlus terechtkomen waarin nooit verzoeken worden afgehandeld. Het logbestand vermeldt:
[2026-08-09 09:14:02 +0000] [981] [CRITICAL] WORKER TIMEOUT (pid:1004)Migraties horen thuis in de unit, niet in de opstartcode van de applicatie. ExecStartPre wordt eenmalig uitgevoerd voordat er workers bestaan. Het plaatsen van migrate in uw applicatie betekent dat drie workers tegelijkertijd strijden om dezelfde schema-lock.
De implementatievorm is nagenoeg gelijk
Procesbeheer
Beide frameworks draaien onder gunicorn, en gunicorn draait onder systemd.
[Unit]
Description=gunicorn for site1
After=network.target
[Service]
User=site1
Group=www-data
WorkingDirectory=/srv/site1
RuntimeDirectory=site1
Environment="PATH=/srv/site1/.venv/bin"
ExecStartPre=/srv/site1/.venv/bin/python manage.py migrate --noinput
ExecStart=/srv/site1/.venv/bin/gunicorn -c /srv/site1/gunicorn.conf.py site1.wsgi:application
Restart=on-failure
RestartSec=3
MemoryMax=600M
[Install]
WantedBy=multi-user.targetRuntimeDirectory=site1 maakt /run/site1 aan bij het starten en verwijdert dit bij het stoppen, zodat het socketpad altijd bestaat met de juiste eigenaar. De regel umask = 0o007 in de gunicorn-configuratie zorgt ervoor dat de www-data-groep naar deze socket kan schrijven; dit is hoe nginx de socket bereikt.
De Flask-unit is hetzelfde bestand met één gewijzigde regel: ExecStart=... gunicorn -c /srv/site1/gunicorn.conf.py app:app, en zonder ExecStartPre. Het argument app:app bestaat uit de module gevolgd door de callable, dus de foutmelding Failed to find attribute 'app' in 'app'. betekent dat uw module geen variabele met die naam definieert. Geplande taken volgen hetzelfde patroon, en een systemd timer vervangt cron voor een Django management command zonder dat er een taakwachtrij aan een server van dit formaat hoeft te worden toegevoegd.
Statische bestanden
Django met DEBUG = False serveert helemaal geen statische bestanden. Stel STATIC_ROOT in, voer python manage.py collectstatic uit en laat de webserver naar de uitvoermap wijzen. Slaat u deze stap over, dan laadt de admin-interface zonder opmaak en raakt het logboek vol met Not Found: /static/admin/css/base.css.
Er zijn twee redelijke manieren om deze te serveren. Een nginx alias-blok kost uw applicatie niets. WhiteNoise, toegevoegd als middleware, serveert bestanden vanuit de worker en bespaart u het nginx-blok, ten koste van een kleine hoeveelheid worker-tijd per bestand. Flask serveert tijdens ontwikkeling zijn eigen static/-map, en in productie laat u de proxy om dezelfde reden naar deze map wijzen.
Reverse proxy
server {
listen 80;
server_name example.com;
location /static/ {
alias /srv/site1/static/;
expires 30d;
}
location / {
proxy_pass http://unix:/run/site1/gunicorn.sock;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Achter een proxy moet Django weten dat het oorspronkelijke verzoek HTTPS was, anders wijzen de cross-site request forgery (CSRF)-controles uw eigen formulieren af.
ALLOWED_HOSTS = ["example.com"]
CSRF_TRUSTED_ORIGINS = ["https://example.com"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")Als de server al containers draait, doet Traefik voor meerdere Docker Compose-applicaties hetzelfde werk met labels op de container in plaats van één bestand per site.
Welke database
SQLite is uitstekend geschikt voor een enkele applicatieserver met een schrijfsnelheid van enkele verzoeken per seconde, en het verwijdert een volledige daemon uit het geheugenbudget. Schakel write ahead logging (WAL) in en geef de driver een busy timeout.
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"OPTIONS": {
"timeout": 20,
"init_command": "PRAGMA journal_mode=WAL;",
},
}
}Zonder deze twee opties krijgt u te maken met django.db.utils.OperationalError: database is locked zodra twee workers tegelijkertijd schrijven, omdat de standaard journal-modus lezers blokkeert tijdens een schrijfactie en de standaard timeout vrijwel direct opgeeft. De uitgebreidere argumentatie, inclusief het punt waarop SQLite niet langer de juiste keuze is, vindt u in SQLite in productie draaien op een VPS.
PostgreSQL op dezelfde 1 GB-server kost de 120 MB uit het bovenstaande budget, plus één backend-proces voor elke persistente verbinding. De CONN_MAX_AGE van Django houdt per worker één verbinding open, dus vier workers betekenen vier backends. Dat is meestal een goede afweging. Houd hier rekening mee voordat u het aantal workers instelt. Als u de database liever in een container naast de applicatie houdt, is Docker draaien op een VPS dezelfde afweging met een hogere ondergrens, aangezien de daemon en elke container overhead toevoegen die bij dit formaat relevant is.
Wat de batterijen opleveren en wat ze kosten
De extra megabytes van Django bestaan uit een verzameling componenten die al bestaan en onderling samenwerken: de ORM met migraties, het sessie- en authenticatiesysteem, het permissiemodel, de formulierlaag met CSRF-beveiliging, de template-engine, management-commando's en de admin-interface. De admin-interface is het onderdeel dat vaak wordt onderschat. Het is een werkende database-editor voor uw modellen, inclusief zoek- en filterfuncties, die u met slechts één regel in INSTALLED_APPS activeert.
Flask is het spiegelbeeld hiervan. U krijgt routing, een request-object, Jinja2-templates en een configuratie-object. Al het overige is een keuze die u zelf maakt. Dit is waardevol bij kleine applicaties, omdat er geen ORM wordt geladen als u deze niet importeert.
De valkuil is de middenweg. Voeg SQLAlchemy toe voor modellen, Alembic voor migraties, Flask-Login voor sessies, Flask-WTF voor formulieren en CSRF, en een admin-extensie voor een backoffice; u heeft dan een systeem samengesteld met het geheugenprofiel van Django, maar zonder de samenhang ervan. Elk onderdeel heeft zijn eigen releasecyclus en eigen opvattingen over hoe de applicatie moet worden ingericht. Dat is het punt waarop Django de goedkopere oplossing is, zowel in RAM-gebruik als in de uren die u besteedt aan upgrades.
Django versus Flask: de beslissingsregel
Gebruik Django wanneer de applicatie accounts, bewerkbare inhoud, een schema dat voortdurend wijzigt en een backoffice die daadwerkelijk wordt gebruikt, vereist. Gebruik Flask wanneer de applicatie een JSON-interface is voor een bestaande datastore, of een webhook-ontvanger zonder HTML-componenten.
De doorslaggevende factor is een geschreven lijst. Noteer elk pakket dat u in Flask zou installeren om de gewenste functionaliteit te bereiken. Als die lijst een ORM en een migratietool bevat, heeft u in feite al voor Django gekozen en betaalt u extra om daar langzamer te komen.
Eén scenario spreekt op kleine hardware daadwerkelijk in het voordeel van Flask: meerdere kleine services op één machine. Elke Flask-service is een afzonderlijk, lichtgewicht proces onder een eigen unit. Drie Django-sites op één 1 GB VPS betekent drie gelijktijdig actieve kopieën van het framework, waardoor de bovenstaande rekensom niet langer opgaat. Als het budget telkens tekortschiet, is een groter abonnement vaak de eerlijke oplossing, en wat een VPS per maand daadwerkelijk kost is een kortere discussie dan het herschrijven van een werkende applicatie.
Foutmodi bij de meldingen die u zult zien
Workers verdwijnen en komen terug. Gunicorn toont [ERROR] Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?. Dit gebeurt zowel wanneer het een worker beëindigt die de heartbeat-timeout heeft overschreden, als wanneer de kernel het proces heeft beëindigd. Maak onderscheid tussen beide met dmesg -T | grep -i "killed process". Een regel daar wijst op geheugenproblemen; verlaag het aantal workers of voeg swap toe.
Elke pagina geeft een 400-fout en het logboek toont Invalid HTTP_HOST header. De volledige melding is Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS.. Django wijst het verzoek af voordat het uw code bereikt, omdat ALLOWED_HOSTS leeg is of niet de naam bevat die de proxy in Host heeft meegegeven.
Formulieren falen met Origin checking failed. De pagina meldt dat CSRF-verificatie is mislukt. Dit gebeurt achter een proxy met TLS-termination: de applicatie ziet platte HTTP, bouwt een http://-origin op en vergelijkt deze met een verzoek dat via https:// binnenkwam. Stel SECURE_PROXY_SSL_HEADER en CSRF_TRUSTED_ORIGINS in en bevestig dat de proxy daadwerkelijk X-Forwarded-Proto meestuurt.
nginx geeft direct een 502-fout. Het foutenlogboek vermeldt de reden: connect() to unix:/run/site1/gunicorn.sock failed (2: No such file or directory) betekent dat de unit niet draait, en (13: Permission denied) betekent dat de socket bestaat maar nginx deze niet kan openen; dit is een kwestie van de umask- en groepsinstellingen.
De admin-interface heeft geen opmaak. collectstatic is niet uitgevoerd, of het alias-pad komt niet overeen met STATIC_ROOT. Het toegangslogboek toont 404-fouten onder /static/admin/.
Schrijfacties falen bij een lage belasting. database is locked vanuit SQLite betekent dat WAL is uitgeschakeld of dat de busy timeout te kort is voor twee workers die op hetzelfde moment schrijven.
FAQ
Is Django te zwaar voor een 1 GB VPS?
Nee. Django draait prima op 1 GB met een klein aantal workers, Nginx als reverse proxy en SQLite als database. Het wordt krap wanneer u PostgreSQL met standaardinstellingen, een cache, een achtergrond-worker en Docker op dezelfde machine toevoegt. Meet de proportional set size van één worker, verdubbel deze voor piekbelasting en vergelijk het totaal met het beschikbare geheugen na aftrek van het besturingssysteem en de database.
Hoeveel gunicorn workers moet ik draaien op één vCPU?
Begin met drie en meet de prestaties. Geheugen is doorgaans de beperkende factor op een kleine VPS. Deel daarom het RAM-geheugen dat overblijft na het besturingssysteem en de database door twee keer de proportional set size van één worker. Als uw views voornamelijk wachten op een database of een upstream API, schakel dan over naar de gthread worker-klasse met een klein aantal workers en meerdere threads per worker. Threads delen namelijk één geladen kopie van het framework en verbruiken aanzienlijk minder geheugen dan extra processen.
Heb ik PostgreSQL nodig, of volstaat SQLite?
SQLite volstaat voor één applicatieserver met een bescheiden schrijfsnelheid en het bespaart het geheugenverbruik van een extra daemon. Schakel write ahead logging in en stel een busy timeout in, anders mislukken gelijktijdige schrijfacties met database is locked. Stap over naar PostgreSQL wanneer meer dan één machine moet schrijven, of wanneer u functionaliteit nodig heeft die SQLite niet biedt, zoals intensieve gelijktijdige schrijfacties of toegangscontrole per rol.
Moet ik uvicorn gebruiken in plaats van gunicorn?
Alleen als u async views heeft en daadwerkelijk moet wachten op externe processen. Flask is een WSGI-applicatie; een async view draait in een nieuwe event loop binnen de worker-thread en voltooit voordat het volgende verzoek begint, wat geen extra concurrency oplevert. Django async views hebben een ASGI-server nodig om voordeel te behalen. Recente uvicorn-releases hebben hun gunicorn worker-klasse verplaatst naar een apart pakket. Raadpleeg daarom de actuele uvicorn-documentatie in plaats van een verouderde worker-klasse flag uit een oudere handleiding te kopiëren.
Waarom is mijn worker verdwenen zonder traceback?
Een proces dat door de kernel out of memory killer wordt beëindigd, ontvangt SIGKILL en kan bij het afsluiten niets meer loggen, waardoor uw applicatielog simpelweg stopt. Gunicorn merkt het gat op en print Worker (pid:1234) was sent SIGKILL! Perhaps out of memory?. Bevestig dit met dmesg -T | grep -i "killed process". De oplossing is minder workers of een swapfile, zodat een geheugenpiek resulteert in een traag verzoek in plaats van een dood proces.