Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Cloud / Hosting 06.11.2026 · 13 min Lesezeit Geprüft 06.11.2026

Wann bricht der Server ein? Lasttest mit k6 auf einem kleinen VPS

Ein Lasttest mit k6 zeigt, wie viele Seitenaufrufe ein kleiner VPS mit 2 Kernen schafft: 2.802 pro Sekunde für eine statische Seite, 16 für WordPress ohne Cache und 299 mit Cache Enabler. Mit Messaufbau, Skript und der Falle beim Cache-Test.

Werbung Mit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Symbolbild: Server mit Messkurve, die an einer roten Grenzlinie einbricht

Wie viele Besucher hält ein kleiner Server aus, bevor die Seite hängt? Diese Anleitung misst es nach. Ein Lastgenerator schickt mit k6 stufenweise mehr Seitenaufrufe an einen netcup VPS 500 G12.5 mit 2 Kernen und 4 GB RAM, bis Fehler und lange Wartezeiten auftreten. Getestet werden eine statische Website, WordPress ohne Cache und dasselbe WordPress mit Cache-Plugin. Der Server schreibt dabei jede Sekunde seine CPU-Last mit, so zeigt sich auch, woran es scheitert.

Dies ist Messung 1 der Reihe „Grenzlast“. Die Reihe misst, wo ein kleiner VPS an seine Grenzen kommt. Der Server ist derselbe wie in der Reihe „Server im Netz“, auf ihm laufen nebenher Mail, Matrix, Forgejo und weitere Dienste in 18 Containern. Vor dem Test lag die CPU-Last dadurch bei rund 6 %. Die Werte gelten also für einen Server im echten Betrieb, nicht für eine leere Maschine.

Voraussetzungen

  • Ein eigener Server als Ziel. Lasttests gehören nur auf Systeme, die Ihnen gehören, und sie sollten kurz bleiben, weil sie sich für andere wie ein Angriff anfühlen.
  • Ein zweiter Rechner mit Docker als Lastgenerator, möglichst nicht auf demselben Server. Läuft k6 auf dem Ziel, teilen sich Lastgenerator und Webserver die CPU, und die Messung verschiebt sich.
  • Eine Leitung, die mehr schafft als der Server ausliefert. Hier stand der Lastgenerator im Heimnetz mit 267 Mbit/s im Download.
  • Zugang per SSH zum Ziel, dort vmstat (Paket procps, bei Ubuntu vorinstalliert).
  • Grundwissen zu Docker, siehe Docker Compose unter Linux.
EckdatenWert
Servernetcup VPS 500 G12.5, 2 Kerne, 4 GB RAM, kein Swap, Ubuntu 24.04
Lastgeneratork6 v2.3.0 im Container, im Heimnetz, IPv4, HTTP/2
WebserverCaddy 2.11.7 mit TLS, ohne Komprimierung
WordPress7.1.4 mit PHP 8.3, MariaDB 11.4
CacheCache Enabler 1.8.17
Stufenje 45 Sekunden konstante Last, 20 bis 45 Sekunden Pause, Timeout 10 Sekunden

Schritt 1: Ein eigenes Testziel ohne Schutzschicht

Auf dem Server der Reihe steht vor der Website ein Schutzwall aus CrowdSec mit Web-Firewall. Er kann den Lastgenerator als Angreifer sperren, für alle Dienste des Servers. Deshalb bekommt der Test eigene Subdomains, ohne CrowdSec und ohne Zugriffsprotokoll, aus dem CrowdSec lesen könnte. Die statische Seite liefert Caddy direkt aus dem Ordner der Website aus, WordPress läuft als eigener Docker-Stack auf Port 18100. Ersetzen Sie die Beispielnamen durch eigene Subdomains.

last.example.com {
	root * /srv/site
	file_server
}

wp-last.example.com {
	@gesperrt path /xmlrpc.php /wp-login.php /wp-admin*
	respond @gesperrt 403
	reverse_proxy 127.0.0.1:18100
}

Caddy läuft hier im Host-Netz, deshalb erreicht es WordPress unter 127.0.0.1. Läuft Caddy in einem eigenen Docker-Netz, tragen Sie dort den Namen des WordPress-Containers ein. Der Anmeldebereich ist von außen gesperrt, eingerichtet wird mit WP-CLI. Die ersten Scanner fragten die neue Adresse 2 bis 4 Sekunden nach Ausstellung des Zertifikats ab, vermutlich über die öffentlichen Zertifikatslogs.

Damit ein Absturz im Test nicht die anderen Dienste mitreißt, bekommt WordPress feste Grenzen: höchstens 1 GB RAM für den Webserver, 512 MB für die Datenbank, und Apache startet höchstens 40 Prozesse statt der 150 aus dem Image. Die Passwörter liegen als Dateien neben der compose.yaml und gelangen als Compose-Secrets in die Container.

services:
  db:
    image: mariadb:11.4
    restart: unless-stopped
    mem_limit: 512m
    environment:
      MARIADB_DATABASE: wp
      MARIADB_USER: wp
      MARIADB_PASSWORD_FILE: /run/secrets/db_pw
      MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_pw
    secrets: [db_pw, db_root_pw]
    volumes: [./db:/var/lib/mysql]
  wp:
    image: wordpress:php8.3-apache
    restart: unless-stopped
    mem_limit: 1g
    depends_on: [db]
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: wp
      WORDPRESS_DB_USER: wp
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_pw
    secrets: [db_pw]
    volumes:
      - ./html:/var/www/html
      - ./mpm.conf:/etc/apache2/mods-available/mpm_prefork.conf:ro
    ports: ["127.0.0.1:18100:80"]
secrets:
  db_pw: {file: ./.db-pw}
  db_root_pw: {file: ./.db-root-pw}
<IfModule mpm_prefork_module>
	StartServers		 5
	MinSpareServers		 5
	MaxSpareServers		10
	MaxRequestWorkers	 40
	MaxConnectionsPerChild   0
</IfModule>

Der zweite Block ist die Datei mpm.conf. Die Datei .db-pw muss für den Webserver im Container lesbar sein (chmod 444), sonst meldet WordPress Database Error und im Log steht file_get_contents(/run/secrets/db_pw): Failed to open stream: Permission denied. Für den Inhalt legt WP-CLI 20 Beiträge mit je sechs Absätzen an, eine Beitragsseite ist rund 70 KB groß, die Startseite 92 KB.

Verifizieren: curl -s -o /dev/null -w "%{http_code} %{time_total}\n" https://wp-last.example.com/ liefert 200, vom Server selbst aufgerufen nach rund 0,12 Sekunden, von außen etwas später. /wp-login.php liefert 403.

Schritt 2: Das Lastskript für k6

Mit festen virtuellen Nutzern wartet jeder Nutzer auf die Antwort, bevor er neu fragt. Wird der Server langsam, sinkt die Last von selbst, und die Messung beschönigt. Der Executor constant-arrival-rate schickt dagegen eine feste Zahl von Anfragen pro Sekunde, egal wie schnell der Server antwortet. So verhalten sich echte Besucher: Sie warten nicht aufeinander.

import http from 'k6/http';
const ZIEL = __ENV.ZIEL;
const PFADE = (__ENV.PFADE || '/').split(',');
export const options = {
  discardResponseBodies: true,
  scenarios: { last: { executor: 'constant-arrival-rate', rate: Number(__ENV.RATE), timeUnit: '1s',
    duration: '45s', preAllocatedVUs: Math.min(50, Number(__ENV.RATE)), maxVUs: 600 } },
  summaryTrendStats: ['avg', 'med', 'p(90)', 'p(95)', 'p(99)', 'max'],
};
export default function () {
  const p = PFADE[Math.floor(Math.random() * PFADE.length)];
  http.get(ZIEL + p, { timeout: '10s', headers: { 'User-Agent': 'grenzlast-messung/1 k6' } });
}

Jede Anfrage holt eine zufällige Seite aus einer Liste, bei der statischen Seite 8 Seiten aus der sitemap.xml, bei WordPress die Startseite und die 20 Beiträge. k6 lädt nur das HTML, keine Bilder und Stylesheets. Beim ersten Besuch lädt ein Browser diese zusätzlich, danach meist aus seinem Cache.

Verifizieren: docker run --rm grafana/k6:latest version zeigt die Version, ein Probelauf mit RATE=2 endet mit http_req_failed bei 0 %.

Schritt 3: Stufen fahren und den Server mitschreiben

Der Test steigert die Last in Stufen und speichert für jede Stufe eine Zusammenfassung als JSON. Zwischen den Stufen liegen 20 Sekunden Pause, damit sich der Server erholt. Die Reihe bricht ab, sobald mehr als 10 % der Anfragen scheitern oder 95 % der Antworten länger als 5 Sekunden brauchen. Das Skript läuft in bash.

#!/bin/bash
k6() { docker run --rm -u 0 -v "$PWD":/w -w /w grafana/k6:latest "$@"; }
WPP="/,$(seq -s, 1 20 | sed 's#\([0-9]*\)#/messbeitrag-\1/#g')"
for r in 2 5 10 12 14 16 18 20; do
  k6 run -q --summary-export /w/erg-wp-$r.json \
    -e ZIEL=https://wp-last.example.com -e PFADE="$WPP" -e RATE=$r last.js
  jq -e '.metrics.http_req_failed.value > 0.1 or .metrics.http_req_duration["p(95)"] > 5000' \
    erg-wp-$r.json >/dev/null && { echo "Abbruch bei $r/s"; break; }
  sleep 20
done

Auf dem Server läuft währenddessen vmstat und schreibt jede Sekunde eine Zeile mit Zeitstempel. Die Spalten us und sy zeigen die Rechenzeit für Programme und Kernel, st die Steal-Time, also Zeit, die der Wirt der virtuellen Maschine vorenthält. Die Spalte r zählt Prozesse, die rechnen oder auf einen Kern warten.

nohup timeout 3600 vmstat -n -t 1 > vmstat.log 2>&1 < /dev/null &

Verifizieren: jq '.metrics.http_reqs.rate, .metrics.http_req_failed.value' erg-wp-10.json zeigt die erreichte Rate und den Fehleranteil als Wert zwischen 0 und 1, tail -3 vmstat.log neue Zeilen im Sekundentakt.

Schritt 4: Das Ergebnis im Überblick

Ergebnis-Kacheln des Lasttests: statische Seite 2.802, WordPress mit Cache 299, WordPress ohne Cache 16 fehlerfreie Seitenaufrufe pro Sekunde
Höchster fehlerfreier Durchsatz je Variante, 95 % der Antworten unter 1 Sekunde

Als „sauber“ gilt eine Stufe, wenn unter 1 % der Anfragen scheitern und 95 % der Antworten in unter einer Sekunde kommen. Ab etwa einer Sekunde nehmen Besucher eine Verzögerung deutlich wahr. Die Kacheln zeigen den höchsten sauberen Durchsatz. Hochgerechnet auf einen Tag wirken die Zahlen riesig, doch Besucher kommen in Spitzen, etwa nach einem Newsletter. Entscheidend ist, was der Server in der Spitze hält.

Diagramm: angefragte gegen fehlerfrei beantwortete Seitenaufrufe pro Sekunde für statische Seite, WordPress mit und ohne Cache, mit markierten Kipppunkten
Ring: bester sauberer Wert, Kreuz: hier kippt es. Beide Achsen logarithmisch

Die gestrichelte Linie zeigt den Idealfall, jede angefragte Seite wird beantwortet. Solange die Kurven darauf liegen, kommt der Server mit. Das Kreuz markiert die Stufe, ab der es kippt. Die CPU-Werte im Text sind Mittelwerte je Stufe ohne die ersten Sekunden Anlauf.

Verifizieren: In jeder sauberen Stufe liegt http_req_failed unter 0,01 und p(95) unter 1000 Millisekunden.

Schritt 5: Die statische Seite

Bis 1.600 Aufrufe pro Sekunde antwortete Caddy in 23 bis 30 Millisekunden, gemessen beim Lastgenerator samt Weg durchs Internet. Bei 3.200 angefragten Aufrufen pro Sekunde kamen 2.802 an, alle ohne Fehler, 95 % in höchstens 366 Millisekunden. Gebremst haben hier zwei Dinge gleichzeitig: Die CPU des Servers lag im Mittel bei 90 %, und k6 meldete Insufficient VUs, reached 600 active VUs and cannot initialize more, weil alle 600 virtuellen Nutzer des Lastgenerators beschäftigt waren. Die Grenze der statischen Seite dürfte also knapp unter 3.000 Aufrufen pro Sekunde liegen. Genau messen ließe sie sich nur mit einem stärkeren Lastgenerator. Die Leitung war mit 180 Mbit/s nicht voll.

CPU-Verlauf des Servers bei der statischen Seite über die Laststufen 50 bis 3.200 Aufrufe pro Sekunde
Statische Seite: Bei 3.200 angefragten Aufrufen pro Sekunde liegt die CPU bei rund 90 %

Verifizieren: In vmstat.log steigen us plus sy mit jeder Stufe, bei 800 Aufrufen pro Sekunde auf 36 %, bei 1.600 auf 60 %.

Schritt 6: WordPress ohne Cache

Ohne Cache baut WordPress jede Seite neu: PHP lädt Theme und Plugins, fragt die Datenbank ab und setzt das HTML zusammen. Bei wenig Last kam die Antwort nach rund 150 Millisekunden beim Lastgenerator an. Bei 10 Aufrufen pro Sekunde arbeitete die CPU zur Hälfte, bei 14 blieben 95 % der Antworten unter 200 Millisekunden. Dann ging es schnell: Bei 16 Aufrufen brauchten 5 % der Anfragen 0,9 Sekunden und länger, bei 18 über 2 Sekunden, und die Hälfte wartete über eine Sekunde. Bei 20 Aufrufen pro Sekunde lag die CPU bei 96 %, über 30 Prozesse rechneten oder warteten auf einen Kern, und 13,6 % der Anfragen brachen nach 10 Sekunden ab.

CPU-Verlauf des Servers bei WordPress ohne Cache: 10 Aufrufe pro Sekunde rund 50 Prozent, 20 Aufrufe pro Sekunde fast 100 Prozent
WordPress ohne Cache: Bei 20 Aufrufen pro Sekunde ist die CPU voll

Der Arbeitsspeicher war dabei kein Problem, der Webserver-Container brauchte unter Volllast 393 MB und wurde nicht vom Kernel beendet. Der Engpass ist die Rechenzeit für PHP. Mehr Apache-Prozesse dürften bei voller CPU nicht helfen, sie verlängern nur die Warteschlange.

Verifizieren: docker inspect grenzlast-wp-wp-1 | grep OOMKilled zeigt false, die Spalte r in vmstat.log steigt in der kritischen Stufe weit über die Zahl der Kerne.

Schritt 7: WordPress mit Cache Enabler

Ein Seiten-Cache speichert das fertige HTML einer Seite und liefert es beim nächsten Aufruf aus, ohne Datenbank und ohne Theme. Cache Enabler ist ein schlankes, kostenloses Plugin dafür. Es wird mit WP-CLI installiert und aktiviert, Einstellungen braucht es für den Test nicht.

wp plugin install cache-enabler --activate
curl -s -o /dev/null -D - -H "Accept: text/html" https://wp-last.example.com/messbeitrag-3/ | grep -i x-cache-handler

Hier liegt die Falle dieses Tests. Ein schneller Blick mit curl zeigt keinen Cache, egal wie oft Sie fragen. Cache Enabler lässt den Cache aus, sobald eine Anfrage einen Accept-Header ohne text/html mitschickt, und curl sendet Accept: */*. Browser senden bei normalen Seitenaufrufen text/html mit, k6 sendet gar keinen Accept-Header, beide bekommen die Seite aus dem Cache. Mit dem passenden Header kam der zweite Aufruf nach 41 statt 167 Millisekunden, und die Antwort trägt den Header X-Cache-Handler: cache-enabler-engine. Im Ordner wp-content/cache lagen danach 21 Dateien, eine je Seite. Unter Last fiel die CPU-Last der Datenbank auf nahe 0 %.

CPU-Verlauf des Servers bei WordPress mit Cache Enabler über die Laststufen 50 bis 400 Aufrufe pro Sekunde
WordPress mit Cache: Die Stufen 250 bis 350 stammen aus dem zweiten Lauf

Mit Cache hielt WordPress 299 Aufrufe pro Sekunde, 95 % der Antworten kamen in 182 Millisekunden. Bei 350 kippte es mit 6,6 % Abbrüchen. Die Stufen 250 bis 350 entstanden in einem zweiten Lauf nach der Stufe 400, deshalb stehen sie im Bild rechts. Wieder war die CPU der Engpass, nicht die Leitung: Cache Enabler liefert die gespeicherten Seiten weiter über PHP und Apache aus, nur eben ohne Datenbank, und auch die Verschlüsselung in Caddy kostet Rechenzeit. Die erste Stufe mit 50 Aufrufen zeigt kurz höhere Werte, vermutlich weil Apache nach der Pause erst weitere Prozesse starten musste.

Verifizieren: find wp-content/cache -name "*.html" | wc -l zählt die gespeicherten Seiten, docker stats zeigt die Datenbank unter Last nahe 0 %. Prüfen Sie nicht mit curl -I: Cache Enabler bedient nur GET-Anfragen, eine HEAD-Anfrage geht am Cache vorbei.

Schritt 8: Ab wann Besucher warten

Diagramm: 95. Perzentil der Antwortzeit je Laststufe, alle drei Varianten springen nach der Grenze über eine Sekunde
95. Perzentil der Antwortzeit beim Lastgenerator, rote Linie bei 1 Sekunde

Die Antwortzeit kippt bei allen drei Varianten schlagartig: Eine Stufe nach der Grenze stauen sich die Anfragen, und die Wartezeit springt von Millisekunden auf Sekunden. Planen Sie deshalb Abstand zur Grenze ein. Wer bei WordPress ohne Cache regelmäßig 10 Aufrufe pro Sekunde erwartet, hat für Spitzen nur noch wenig Luft, denn ab 16 wird es knapp.

Verifizieren: In den JSON-Zusammenfassungen springt p(95) von http_req_duration zwischen zwei Stufen um mehr als das Vierfache.

Troubleshooting

SymptomUrsache und Lösung
Insufficient VUs, reached 600 active VUs and cannot initialize moreDer Lastgenerator hat zu wenige virtuelle Nutzer für die Rate. maxVUs erhöhen oder die Grenze als Grenze des Lastgenerators werten, nicht des Servers.
Hohe Zahl bei dropped_iterationsk6 konnte Anfragen nicht starten, weil keine freie VU bereitstand, etwa solange k6 neue VUs anlegt. Bei kleinen Werten (70 von 36.000) unkritisch.
Cache wirkt im Test mit curl nichtcurl sendet Accept: */*, das umgeht Cache Enabler. curl mit -H "Accept: text/html" aufrufen, kein -I.
Gemessene Datenrate nahe an der eigenen LeitungDann misst der Test die Leitung. Die Seiten kamen hier unkomprimiert an, 200 WordPress-Seiten pro Sekunde waren 115 Mbit/s. Lastgenerator mit schnellerer Anbindung nutzen.
Eigene IP ist nach dem Test gesperrtCrowdSec oder Fail2Ban hat den Lastgenerator erkannt. Testziel ohne Schutzschicht und ohne ausgewertetes Log betreiben, wie in Schritt 1.

Häufige Fragen

Wie viele Besucher sind 16 Seitenaufrufe pro Sekunde?

Das hängt davon ab, wie oft ein Besucher klickt. Liest jeder im Schnitt 30 Sekunden pro Seite, entsprechen 16 Aufrufe pro Sekunde rund 480 Besuchern gleichzeitig. Für eine Firmenwebsite reicht das meist, für einen Shop in der Rabattaktion nicht.

Hat der Nachbar auf dem Wirt die Messung verfälscht?

Nein. Die Steal-Time lag in allen Stufen bei höchstens 2 %. Wie Sie die Steal-Time selbst über längere Zeit beobachten, zeigt Steal-Time messen.

Warum nicht einfach ab oder wrk?

Beide arbeiten mit einer festen Zahl gleichzeitiger Verbindungen (Option -c). Wird der Server langsam, sinkt die Last, und die Überlast bleibt unsichtbar. k6 mit fester Ankunftsrate zeigt den Kipppunkt so, wie echte Besucher ihn erleben.

Reicht ein Cache-Plugin oder braucht es einen größeren Server?

Ein größerer Tarif war nicht Teil der Messung. Da bei WordPress die CPU der Engpass war, hängt die Grenze vor allem an der Rechenleistung, und der Cache verschob sie um den Faktor 19. Für angemeldete Besucher und Seiten wie Warenkorb oder Kasse greift der Seiten-Cache aber nicht, dort zählt wieder die Rechenleistung.

Fazit

Der VPS mit 2 Kernen und 4 GB RAM liefert eine statische Website mit fast 3.000 Aufrufen pro Sekunde aus und hat damit für fast jede Firmenseite Reserve. WordPress ohne Cache ist dagegen schon bei 16 Aufrufen pro Sekunde am Ende, und der Übergang von schnell zu unbenutzbar dauert eine einzige Laststufe. Ein Cache-Plugin ist deshalb keine Feinarbeit, sondern Pflicht: Es hob die Grenze auf 299 Aufrufe pro Sekunde. Wer seinen eigenen Server kennen will, misst mit diesem Aufbau in rund einer Stunde selbst nach. Messung 2 der Reihe klärt, wie viele Dienste in 4 GB RAM passen.

Weiterführende Anleitungen und Quellen

GrenzlastLasttestk6WordPressCacheVPS