PHP Timeout umgehen — Große Datenbanken sicher exportieren

PHP-Timeout-Probleme bei großen Datenbank-Backups lösen

Du klickst auf „Export“ und nach 30 Sekunden: Abbruch. Weiße Seite. Timeout. Das ist das Problem schlechthin auf Shared Hosting. PHP hat ein Zeitlimit, und wenn dein Datenbank-Export länger braucht, wird der Prozess gnadenlos abgewürgt. Hier sind vier Wege, das Problem zu lösen — vom schnellen Workaround bis zur sauberen Dauerlösung.

Das Problem verstanden

PHP hat eine eingebaute Sicherheitsbremse: max_execution_time. Die ist auf den meisten Servern auf 30 Sekunden eingestellt. Manche Hoster erlauben 60 oder 120 Sekunden, aber das Prinzip bleibt: Wenn dein Script länger läuft, wird es beendet.

Für eine kleine WordPress-Datenbank mit 5 MB ist das kein Problem. Aber sobald du einen Shop mit 50.000 Produkten, ein Forum mit Jahren an Beiträgen oder eine Datenbank mit 200+ MB exportieren willst, reicht die Zeit nicht.

Den aktuellen Wert kannst du mit einer einfachen PHP-Datei prüfen:

<?php
echo "max_execution_time: " . ini_get('max_execution_time') . " Sekunden\n";
echo "memory_limit: " . ini_get('memory_limit') . "\n";
echo "upload_max_filesize: " . ini_get('upload_max_filesize') . "\n";
echo "post_max_size: " . ini_get('post_max_size') . "\n";
?>

Lösung 1: set_time_limit() im Script

Die einfachste Methode — wenn der Hoster es erlaubt. Du setzt das Zeitlimit direkt im PHP-Script hoch oder auf 0 (unbegrenzt):

<?php
// Zeitlimit auf 300 Sekunden (5 Minuten) setzen
set_time_limit(300);

// Oder: Kein Limit
set_time_limit(0);

// Dein Export-Code hier...
?>

Wann funktioniert das?

Auf vielen Hostern ist set_time_limit() erlaubt. Auf Shared Hosting wird es allerdings häufig blockiert — dann hat der Aufruf einfach keinen Effekt.

Prüfen ob es funktioniert

<?php
set_time_limit(0);
$new_limit = ini_get('max_execution_time');
echo "Neues Limit: $new_limit Sekunden\n";

if ($new_limit == 0) {
    echo "set_time_limit funktioniert!";
} else {
    echo "set_time_limit wird vom Hoster blockiert.";
}
?>

Vorteile

  • Einfachste Lösung — eine Zeile Code
  • Kein Serverzugang nötig

Nachteile

  • Auf Shared Hosting oft deaktiviert
  • Löst nicht das Memory-Problem
  • Unsauber — der Server blockiert andere Prozesse

Lösung 2: php.ini / .user.ini anpassen

Falls du Zugriff auf die PHP-Konfiguration hast, kannst du das Zeitlimit dort setzen:

Variante A: php.ini (Root-Server)

; /etc/php/8.1/apache2/php.ini
max_execution_time = 300
memory_limit = 512M
post_max_size = 256M
upload_max_filesize = 256M

Danach Apache/PHP-FPM neu starten:

sudo systemctl restart apache2
# oder bei PHP-FPM:
sudo systemctl restart php8.1-fpm

Variante B: .user.ini (Shared Hosting)

Viele Hoster erlauben eine .user.ini im Webroot. Erstelle die Datei im Verzeichnis deines Export-Scripts:

; .user.ini
max_execution_time = 300
memory_limit = 256M

Achtung: Die .user.ini wird gecacht (Standard: 300 Sekunden). Änderungen greifen nicht sofort.

Variante C: .htaccess (Apache)

# .htaccess
php_value max_execution_time 300
php_value memory_limit 256M

Funktioniert nur bei Apache mit mod_php. Bei PHP-FPM oder Nginx hat die .htaccess keinen Effekt auf PHP-Werte.

Was die Hoster erlauben

Hoster php.ini ändern .user.ini .htaccess PHP
Strato Nein Ja (begrenzt) Nein
IONOS Über Panel Ja Teilweise
all-inkl Über Panel Ja Ja
Hetzner Ja (VPS) Ja Ja

Lösung 3: Chunked Export (der MySQLDumper-Ansatz)

Das ist die eleganteste Lösung und der Grund, warum MySQLDumper existiert. Statt den gesamten Export in einem einzigen PHP-Aufruf zu machen, wird der Prozess in kleine Teile aufgesplittet.

Das Prinzip

  1. Script exportiert z.B. 2.000 Zeilen einer Tabelle
  2. Speichert den aktuellen Fortschritt (Tabelle, Offset)
  3. Leitet den Browser per HTTP-Redirect auf sich selbst um
  4. Der nächste Aufruf macht die nächsten 2.000 Zeilen
  5. Wiederholen bis alles fertig ist

Jeder einzelne Aufruf bleibt unter dem Zeitlimit. Ob deine Datenbank 10 MB oder 10 GB groß ist — spielt keine Rolle.

Vereinfachtes Beispiel

<?php
session_start();

$host = 'localhost';
$user = 'root';
$pass = 'passwort';
$dbname = 'meine_datenbank';
$chunk_size = 2000;
$output_file = 'backup.sql';

$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $user, $pass);

// Fortschritt aus Session laden
$table_index = $_SESSION['table_index'] ?? 0;
$row_offset = $_SESSION['row_offset'] ?? 0;

// Alle Tabellen holen
$tables = $pdo->query("SHOW TABLES")->fetchAll(PDO::FETCH_COLUMN);

if ($table_index >= count($tables)) {
    // Fertig!
    echo "Export abgeschlossen!";
    session_destroy();
    exit;
}

$table = $tables[$table_index];

// Chunk exportieren
$stmt = $pdo->query("SELECT * FROM `$table` LIMIT $row_offset, $chunk_size");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

// In Datei schreiben (append)
$fh = fopen($output_file, 'a');
foreach ($rows as $row) {
    $values = array_map(function($v) use ($pdo) {
        return $v === null ? 'NULL' : $pdo->quote($v);
    }, $row);
    fwrite($fh, "INSERT INTO `$table` VALUES (" . implode(',', $values) . ");\n");
}
fclose($fh);

// Fortschritt speichern
if (count($rows) < $chunk_size) {
    // Tabelle fertig → nächste Tabelle
    $_SESSION['table_index'] = $table_index + 1;
    $_SESSION['row_offset'] = 0;
} else {
    $_SESSION['row_offset'] = $row_offset + $chunk_size;
}

// Nächsten Chunk starten
$progress = round(($table_index / count($tables)) * 100);
echo "Exportiere $table... ($progress%)
"; echo '<meta http-equiv="refresh" content="0">'; ?>

Hinweis: Das ist eine vereinfachte Version zur Illustration. MySQLDumper macht das deutlich robuster — mit Fehlerbehandlung, Komprimierung, CREATE TABLE Statements und korrektem Escaping.

Vorteile

  • Funktioniert auf jedem Shared Hosting
  • Keine Serverkonfiguration nötig
  • Keine Datenbankgröße-Beschränkung
  • Fortschrittsanzeige im Browser

Nachteile

  • Langsamer als ein direkter Export
  • Browser muss offen bleiben (bei Browser-basiertem Redirect)
  • Konsistenz kann bei gleichzeitigen Schreibvorgängen leiden

Lösung 4: SSH und mysqldump

Wenn du SSH-Zugang hast, ist das Timeout-Problem keins. mysqldump läuft als eigenständiger Prozess auf dem Server — PHP ist nicht involviert.

mysqldump --single-transaction -u root -p meine_datenbank | gzip > backup.sql.gz

Falls du den Dump im Hintergrund laufen lassen willst:

nohup mysqldump --single-transaction -u root -p meine_datenbank | gzip > backup.sql.gz &

nohup sorgt dafür, dass der Prozess weiterläuft, auch wenn du die SSH-Verbindung schließt. Alternativ mit screen oder tmux:

# Screen-Session starten
screen -S backup

# Dump starten
mysqldump --single-transaction -u root -p meine_datenbank | gzip > backup.sql.gz

# Session trennen: Ctrl+A, dann D
# Später wieder verbinden:
screen -r backup

Vorteile

  • Kein Timeout — der Prozess läuft so lange er braucht
  • Volle Kontrolle über den Server
  • Schnellste Methode
  • Konsistente Backups mit --single-transaction

Nachteile

  • Braucht SSH-Zugang (auf Shared Hosting meist nicht verfügbar)
  • Kommandozeilen-Erfahrung nötig

Welche Lösung für welche Situation?

Situation Empfehlung
Shared Hosting, kein SSH Chunked Export (MySQLDumper)
Shared Hosting, set_time_limit erlaubt set_time_limit + phpMyAdmin
VPS/Root-Server SSH + mysqldump
Managed Hosting mit Panel php.ini über Panel + phpMyAdmin
Datenbank > 500 MB SSH + mysqldump, kein Weg dran vorbei

Bonus: Import-Timeout lösen

Das Timeout-Problem existiert auch beim Import. Wenn du einen 200-MB-Dump über phpMyAdmin importieren willst, hast du dasselbe Problem. Die Lösungen sind analog:

  • MySQLDumper macht auch den Restore chunked
  • BigDump (bigdump.php) ist ein spezialisiertes Import-Script mit Chunk-Funktion
  • SSH: mysql -u root -p datenbank < backup.sql — kein Timeout

Mein Rat: Wenn du regelmäßig mit Datenbanken arbeitest, die größer als 50 MB sind, lohnt sich die Investition in einen Hoster mit SSH-Zugang. Das spart dir auf Dauer Stunden an Workarounds. Einen Überblick über alle Backup-Methoden und ihre Vor- und Nachteile findest du im Hauptartikel.