Async PHP in Production: Fibers, ReactPHP und Swoole entmystifiziert
PHP kann 10.000 gleichzeitige Verbindungen handhaben. So geht’s.
PHPs am wenigsten verstandene Superkraft
Drei Tage. So lange hab ich damit verbracht, einen API-Aggregation-Service zu optimieren, der 15 externe APIs sequenziell aufgerufen hat. Die Response-Zeit? Schmerzhafte 2,1 Sekunden im Durchschnitt. Dann hab ich Async PHP entdeckt, und derselbe Service antwortet jetzt in 520 Millisekunden. Das ist eine Verbesserung von 304% – ohne die Business-Logik anzufassen.
Falls du immer noch denkst, PHP ist diese langsame, blockierende, synchrone Sprache aus der LAMP-Stack-Ära, dann hast du was verpasst. In 2025 ist Async PHP keine Ausnahme mehr. Mit Fibers im Core seit PHP 8.1, plus ausgereiften Ecosystemen wie ReactPHP, Swoole und Amp, kann PHP Low-Latency-APIs, WebSockets und Streaming-Pipelines fahren – ohne deine Codebase in Callback-Hell zu verwandeln.
Ich zeig dir, wie Async PHP in Production funktioniert, wann du welchen Ansatz nutzt und wie du die Fallstricke vermeidest, die dich um 3 Uhr morgens beißen werden.
Die PHP-Performance-Revolution, über die niemand spricht
Kennste das noch? Concurrent Operations in PHP bedeuteten entweder mehrere Prozesse hochzufahren (Memory-Alptraum) oder Threads zu nutzen (Complexity-Alptraum)? Diese Zeiten sind vorbei.
Was Async PHP heute kann:
- 200 RSS-Feeds verarbeiten in 2,3 Sekunden statt 111 Sekunden (463% Verbesserung)
- 500+ Webseiten scrapen in der Zeit, die früher für 100 gebraucht wurde
- 10x mehr gleichzeitige User bei konsistenten Response-Zeiten handhaben
- WebSocket-Server betreiben, die tausende Verbindungen gleichzeitig offen halten
Das Geheimnis? Cooperative Multitasking durch Fibers und Event-Loops. Lass uns aufdröseln, was das eigentlich bedeutet.
PHP Fibers verstehen: Das Fundament
PHP 8.1 hat Fibers eingeführt – Full-Stack, unterbrechbare Funktionen. Stell sie dir vor wie leichtgewichtige Threads, die du nach Belieben pausieren und wieder aufnehmen kannst, aber ohne die Komplexität von echtem Parallelismus.
Der entscheidende Unterschied: Fibers sind für Concurrency, nicht Parallelismus. Du führst nicht mehrere CPU-intensive Tasks gleichzeitig aus. Du jonglierst effizient mehrere I/O-gebundene Operationen – während eine wartet, läuft eine andere.
Dein erster Fiber: Der “Aha”-Moment
<?php
$fiber = new Fiber(function (): string {
echo "Fiber gestartet\n";
// Ausführung unterbrechen und Kontrolle zurückgeben
$value = Fiber::suspend('pausiert');
echo "Fiber fortgesetzt mit: {$value}\n";
return 'fertig';
});
// Fiber starten
$result = $fiber->start();
echo "Main: Fiber gab zurück '{$result}'\n";
// Andere Arbeit hier erledigen
echo "Main: Andere Arbeit wird erledigt...\n";
// Fiber fortsetzen
$final = $fiber->resume('neue Daten');
echo "Main: Fiber beendet mit '{$final}'\n";
Output:
Fiber gestartet
Main: Fiber gab zurück 'pausiert'
Main: Andere Arbeit wird erledigt...
Fiber fortgesetzt mit: neue Daten
Main: Fiber beendet mit 'fertig'
Was ist gerade passiert? Der Fiber hat sich selbst unterbrochen, die Kontrolle ans Hauptprogramm zurückgegeben und wurde später mit neuen Daten fortgesetzt. Das ist das Fundament von Async PHP.
Memory-Effizienz: Der Game Changer
Traditionelle Threads verbrauchen 1-2MB pro Thread. Fibers? Etwa 4KB pro Stück. Das ist ein 500-facher Unterschied. Plötzlich wird es machbar, hunderte concurrent Operations zu erstellen, ohne dir Sorgen über Memory Exhaustion zu machen.
Jeder Fiber bekommt seinen eigenen Call-Stack, teilt sich aber denselben Memory-Space. Wenn du eine Klasse in einem Fiber lädst, wird sie in den Process-Heap geladen, nicht pro Fiber dupliziert.
ReactPHP: Pure-PHP Event-Driven Power
ReactPHP ist die Original-Async-PHP-Lösung. Es ist reines PHP (keine Extensions nötig), battle-tested und überraschend schnell. Die Architektur folgt dem Event-Loop-Modell von Node.js.
Die Event-Loop erklärt
ReactPHPs Event-Loop ist der Herzschlag deiner Async-Anwendung. Sie checkt kontinuierlich nach I/O-Operationen, die bereit sind, führt deren Callbacks aus und geht zur nächsten Operation.
<?php
use React\EventLoop\Loop;
use React\Http\Browser;
use function React\Async\async;
use function React\Async\await;
require 'vendor/autoload.php';
// Async-Funktion erstellen
$fetchUrls = async(function (array $urls): array {
$browser = new Browser();
$results = [];
// Alle Requests gleichzeitig starten
$promises = [];
foreach ($urls as $name => $url) {
$promises[$name] = $browser->get($url);
}
// Auf Completion aller warten
try {
$responses = await(\React\Promise\all($promises));
foreach ($responses as $name => $response) {
$results[$name] = [
'status' => $response->getStatusCode(),
'size' => $response->getBody()->getSize(),
];
}
} catch (Exception $e) {
// Pending Promises bei Error canceln
foreach ($promises as $promise) {
$promise->cancel();
}
throw $e;
}
return $results;
});
// Ausführen
$promise = $fetchUrls([
'google' => 'https://www.google.com',
'github' => 'https://github.com',
'reddit' => 'https://www.reddit.com',
]);
$promise->then(
function (array $results) {
echo "Erfolgreich " . count($results) . " URLs geholt:\n";
foreach ($results as $name => $data) {
echo "- {$name}: {$data['status']} ({$data['size']} bytes)\n";
}
},
function (Exception $e) {
echo "Fehler: {$e->getMessage()}\n";
}
);
Dieser Code feuert drei HTTP-Requests gleichzeitig ab. In traditionellem PHP würde das 3x so lange dauern, weil du auf jeden Request sequenziell warten würdest.
Production Pattern: HTTP Client Pool
Real-World-Applications brauchen Rate-Limiting, Retry-Logik und Error-Handling. Hier ist ein produktionsreifes Pattern:
<?php
use React\Http\Browser;
use React\Promise\PromiseInterface;
use function React\Async\async;
use function React\Async\await;
use function React\Promise\all;
class AsyncHttpPool
{
private Browser $browser;
private int $concurrency;
public function __construct(int $concurrency = 10)
{
$this->browser = new Browser();
$this->concurrency = $concurrency;
}
/**
* URLs in Chunks holen, um Server nicht zu überlasten
*/
public function fetchAll(array $urls): PromiseInterface
{
return async(function () use ($urls) {
$chunks = array_chunk($urls, $this->concurrency, true);
$allResults = [];
foreach ($chunks as $chunk) {
$promises = [];
foreach ($chunk as $name => $url) {
$promises[$name] = $this->fetchWithRetry($url);
}
try {
$results = await(all($promises));
$allResults = array_merge($allResults, $results);
} catch (Exception $e) {
// Mit nächstem Chunk weitermachen, auch wenn dieser fehlschlägt
error_log("Chunk failed: {$e->getMessage()}");
}
// Kleine Pause zwischen Chunks, um Server nicht zu killen
await(\React\Promise\Timer\sleep(0.1));
}
return $allResults;
})();
}
/**
* Fehlgeschlagene Requests bis zu 3x wiederholen
*/
private function fetchWithRetry(string $url, int $attempt = 1): PromiseInterface
{
return async(function () use ($url, $attempt) {
try {
$response = await($this->browser->get($url));
return $response->getBody()->getContents();
} catch (Exception $e) {
if ($attempt < 3) {
// Exponential Backoff
await(\React\Promise\Timer\sleep(pow(2, $attempt - 1)));
return await($this->fetchWithRetry($url, $attempt + 1));
}
throw $e;
}
})();
}
}
// Usage
$pool = new AsyncHttpPool(concurrency: 5);
$promise = $pool->fetchAll([
'api1' => 'https://api.example.com/endpoint1',
'api2' => 'https://api.example.com/endpoint2',
// ... hunderte mehr
]);
Dieses Pattern verarbeitet URLs in 5er-Chunks, implementiert Retry-Logik mit Exponential Backoff und handhabt Failures graceful, ohne den gesamten Batch zu stoppen.
Swoole: C-Extension Performance-Biest
Swoole ist eine hochperformante, Coroutine-basierte Extension in C++ geschrieben. Sie ist deutlich schneller als reine PHP-Lösungen, erfordert aber Kompilierung und hat andere Deployment-Überlegungen.
Der Swoole-Vorteil: Eingebauter HTTP-Server
Anders als ReactPHP, das als CLI-Prozess läuft, bringt Swoole einen produktionsreifen HTTP-Server mit Worker-Prozessen, Coroutine-Support und automatischem Connection-Management mit.
<?php
use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\Coroutine as Co;
$server = new Server('0.0.0.0', 9501);
$server->set([
'worker_num' => 4, // Anzahl Worker-Prozesse
'hook_flags' => SWOOLE_HOOK_ALL, // Coroutine-Hooks aktivieren
]);
$server->on('request', function (Request $request, Response $response) {
// Alle I/O ist jetzt non-blocking dank Hooks
$results = [];
// Mehrere Requests gleichzeitig mittels Coroutines abfeuern
Co::join([
go(function () use (&$results) {
$results['weather'] = file_get_contents(
'https://api.weather.com/current'
);
}),
go(function () use (&$results) {
$results['news'] = file_get_contents(
'https://api.news.com/latest'
);
}),
go(function () use (&$results) {
// Selbst Datenbank-Queries sind non-blocking
$pdo = new PDO('mysql:host=localhost;dbname=app', 'user', 'pass');
$stmt = $pdo->query('SELECT * FROM users LIMIT 10');
$results['users'] = $stmt->fetchAll();
}),
]);
$response->header('Content-Type', 'application/json');
$response->end(json_encode([
'status' => 'success',
'data' => array_map(function($v) {
return is_string($v) ? strlen($v) . ' bytes' : count($v) . ' rows';
}, $results),
'timestamp' => time(),
]));
});
$server->start();
Was passiert hier? Die go()-Funktion erstellt eine Coroutine. Co::join()
wartet, bis alle Coroutines fertig sind. Dank SWOOLE_HOOK_ALL werden selbst
blockierende Funktionen wie file_get_contents() und PDO-Queries automatisch
non-blocking.
Swooles Runtime-Hooks: Die Magie
Hier glänzt Swoole richtig. Runtime-Hooks konvertieren automatisch synchrones blockierendes I/O in non-blocking Coroutine-Äquivalente:
<?php
use Swoole\Runtime;
use Swoole\Coroutine as Co;
// Coroutine-Hooks für alles aktivieren
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
Co\run(function() {
// All diese Operationen sind jetzt non-blocking!
// File-Operationen
for ($i = 0; $i < 100; $i++) {
go(function () use ($i) {
$content = file_get_contents("/tmp/file_{$i}.txt");
file_put_contents("/tmp/processed_{$i}.txt", strtoupper($content));
});
}
// Datenbank-Operationen mit nativem PHP PDO
for ($i = 0; $i < 50; $i++) {
go(function () {
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test', 'root', 'pass');
$stmt = $pdo->query('SELECT * FROM users LIMIT 10');
$results = $stmt->fetchAll();
});
}
// Redis-Operationen mit nativer PHP Redis Extension
for ($i = 0; $i < 50; $i++) {
go(function () {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->set('key', 'value');
$value = $redis->get('key');
});
}
});
Ohne eine einzige Zeile deines I/O-Codes zu ändern, macht Swoole es non-blocking. Das ist unglaublich mächtig für die Migration existierender Apps zu Async.
WebSocket-Server: Echtzeit-Kommunikation
Einen Production-WebSocket-Server in Vanilla-PHP zu bauen ist schmerzhaft. Mit Swoole ist es straightforward:
<?php
use Swoole\WebSocket\Server;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
$server = new Server('0.0.0.0', 9502);
$server->set([
'worker_num' => 2,
]);
// Verbundene Clients tracken
$clients = [];
$server->on('open', function (Server $server, Request $request) use (&$clients) {
$clients[$request->fd] = [
'connected_at' => time(),
'ip' => $request->server['remote_addr'],
];
echo "Client #{$request->fd} verbunden von {$request->server['remote_addr']}\n";
$server->push($request->fd, json_encode([
'type' => 'welcome',
'client_id' => $request->fd,
'message' => 'Mit WebSocket-Server verbunden',
]));
});
$server->on('message', function (Server $server, Frame $frame) use (&$clients) {
echo "Empfangen von #{$frame->fd}: {$frame->data}\n";
$data = json_decode($frame->data, true);
if ($data['type'] === 'broadcast') {
// An alle verbundenen Clients broadcasten
foreach ($clients as $fd => $info) {
if ($server->isEstablished($fd)) {
$server->push($fd, json_encode([
'type' => 'broadcast',
'from' => $frame->fd,
'message' => $data['message'],
'timestamp' => time(),
]));
}
}
} else {
// Zurück an Sender
$server->push($frame->fd, json_encode([
'type' => 'echo',
'message' => $data['message'],
]));
}
});
$server->on('close', function (Server $server, $fd) use (&$clients) {
echo "Client #{$fd} getrennt\n";
unset($clients[$fd]);
});
$server->start();
Dieser Server kann tausende gleichzeitige WebSocket-Verbindungen auf einem einzigen Prozess handhaben. Versuch das mal mit traditionellem PHP-FPM.
Amp: Das Fiber-First Framework
Amp ist von Grund auf mit PHP 8.1+ Fibers im Hinterkopf gebaut. Es bietet die sauberste Async/Await-Syntax von allen.
Amps saubere Syntax
<?php
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use function Amp\async;
use function Amp\Future\await;
require 'vendor/autoload.php';
$client = HttpClientBuilder::buildDefault();
// Mehrere concurrent Requests erstellen
$futures = [
async(fn() => $client->request(new Request('https://httpbin.org/delay/1'))),
async(fn() => $client->request(new Request('https://httpbin.org/delay/2'))),
async(fn() => $client->request(new Request('https://httpbin.org/delay/3'))),
];
// Auf Completion aller warten
$responses = await($futures);
foreach ($responses as $i => $response) {
echo "Request " . ($i + 1) . ": " . $response->getStatus() . "\n";
}
Amp nutzt async() um concurrent Operations zu erstellen und await() um auf
Resultate zu warten. Es fühlt sich am “modernsten” an, wenn du von JavaScript
oder anderen Async-Sprachen kommst.
Concurrency-Control mit Semaphores
Semaphores sind perfekt, um Concurrency-Level zu kontrollieren:
<?php
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use Amp\Sync\LocalSemaphore;
use function Amp\async;
use function Amp\Future\await;
$client = HttpClientBuilder::buildDefault();
$semaphore = new LocalSemaphore(5); // Max 5 concurrent Requests
$urls = [
'https://api.example.com/endpoint1',
'https://api.example.com/endpoint2',
// ... 100 weitere URLs
];
$futures = [];
foreach ($urls as $url) {
$futures[] = async(function () use ($client, $semaphore, $url) {
// Semaphore acquiren (warten wenn schon 5 Requests laufen)
$lock = $semaphore->acquire();
try {
$response = $client->request(new Request($url));
return $response->getBody()->buffer();
} finally {
// Lock immer freigeben
$lock->release();
}
});
}
$results = await($futures);
echo "Verarbeitet " . count($results) . " URLs mit max 5 concurrent\n";
Das stellt sicher, dass du den Ziel-Server niemals mit zu vielen simultanen Verbindungen überforderst.
Der große Vergleich: Wann nutzt du was?
Basierend auf Real-World-Benchmarks ist hier der Stand:
Performance-Benchmarks
Low Concurrency (< 100 Requests):
- Swoole: ⭐⭐⭐⭐⭐ (Am schnellsten, C-Extension-Vorteil)
- ReactPHP: ⭐⭐⭐⭐ (Überraschend schnell für reines PHP)
- Amp: ⭐⭐ (Langsamer bei wenig Concurrency)
High Concurrency (1000+ Requests):
- Swoole: ⭐⭐⭐⭐⭐ (Konsistent schnell)
- Amp: ⭐⭐⭐⭐⭐ (Überholt andere bei Scale)
- ReactPHP: ⭐⭐⭐⭐ (Solide Performance)
Entscheidungs-Matrix
Nimm ReactPHP wenn:
- ✅ Du keine PHP-Extensions installieren kannst (Shared Hosting, strenge Corporate Policies)
- ✅ Du reines PHP ohne Compilation-Steps willst
- ✅ Du battle-tested, rock-solid LTS-Releases brauchst
- ✅ Dein Team mit Node.js Event-Loops vertraut ist
- ❌ Du maximale Raw-Performance brauchst
Nimm Swoole wenn:
- ✅ Du absolute Best-Performance brauchst
- ✅ Du WebSocket-Server oder Real-Time-Apps baust
- ✅ Du automatische Coroutine-Hooks für existierenden Code willst
- ✅ Du C-Extensions installieren kannst
- ❌ Du in Environments deployst, wo du keine Extensions kompilieren kannst
- ❌ Du mit traditionellen Tools debuggen musst (Stack-Traces können verwirrend sein)
Nimm Amp wenn:
- ✅ Du die sauberste, modernste Async/Await-Syntax willst
- ✅ Du hochgradig concurrent Applications baust (1000+ Operationen)
- ✅ Du Fiber-First-Design bevorzugst
- ✅ Du ausgefeilte Concurrency-Primitives brauchst (Semaphores, Mutexes)
- ❌ Du den schnellsten Startup für Low-Concurrency-Szenarien brauchst
Nimm Plain Fibers wenn:
- ✅ Du dein eigenes Async-Framework baust
- ✅ Du präzise Kontrolle über Execution-Flow brauchst
- ✅ Du minimale Dependencies willst
- ❌ Du High-Level-Abstraktionen für HTTP, WebSockets etc. brauchst
Production Deployment: Die Real-World-Checkliste
Hier hören die meisten Async-PHP-Tutorials auf. Aber Production ist, wo’s wirklich wehtut, wenn du nicht vorbereitet bist.
Memory-Leak-Management
Async-Prozesse laufen lange. Memory-Leaks akkumulieren über Zeit und führen irgendwann zu Stabilitätsproblemen.
Das Watchdog-Pattern:
<?php
use Swoole\Timer;
class MemoryWatchdog
{
private int $maxMemoryMb;
private int $checkIntervalMs;
public function __construct(int $maxMemoryMb = 512, int $checkIntervalMs = 60000)
{
$this->maxMemoryMb = $maxMemoryMb;
$this->checkIntervalMs = $checkIntervalMs;
}
public function start(): void
{
Timer::tick($this->checkIntervalMs, function () {
$currentMb = memory_get_usage(true) / 1024 / 1024;
echo "[Watchdog] Memory-Nutzung: " . round($currentMb, 2) . " MB\n";
if ($currentMb > $this->maxMemoryMb) {
echo "[Watchdog] Memory-Limit überschritten, graceful shutdown\n";
// Issue loggen
error_log("Memory-Limit überschritten: {$currentMb}MB");
// Graceful Shutdown triggern
posix_kill(posix_getpid(), SIGTERM);
}
});
}
}
// Usage im Swoole-Server
$server->on('workerStart', function ($server, $workerId) {
$watchdog = new MemoryWatchdog(maxMemoryMb: 512);
$watchdog->start();
});
Graceful Shutdown
Handle SIGTERM ordentlich, um Requests fertig zu verarbeiten, bevor du stirbst:
<?php
use Swoole\Process;
use Swoole\Http\Server;
$server = new Server('0.0.0.0', 9501);
$activeRequests = 0;
$server->on('request', function ($request, $response) use (&$activeRequests) {
$activeRequests++;
// Request verarbeiten
$response->end("OK");
$activeRequests--;
});
// Graceful-Shutdown-Handler
Process::signal(SIGTERM, function () use ($server, &$activeRequests) {
echo "SIGTERM empfangen, warte auf {$activeRequests} aktive Requests...\n";
// Keine neuen Connections mehr akzeptieren
$server->shutdown();
// Auf aktive Requests warten (max 30 Sekunden)
$waited = 0;
while ($activeRequests > 0 && $waited < 30) {
sleep(1);
$waited++;
echo "Warte noch... {$activeRequests} Requests übrig\n";
}
echo "Shutdown abgeschlossen\n";
exit(0);
});
$server->start();
Health-Check-Endpoints
Dein Orchestrator (Kubernetes, Docker Swarm) muss wissen, ob dein Async-Service healthy ist:
<?php
$server->on('request', function (Request $request, Response $response) use (&$activeRequests) {
// Health-Check-Endpoint
if ($request->server['request_uri'] === '/health') {
$memoryMb = memory_get_usage(true) / 1024 / 1024;
$health = [
'status' => 'healthy',
'memory_mb' => round($memoryMb, 2),
'active_requests' => $activeRequests,
'uptime_seconds' => time() - $server->setting['start_time'],
];
// Als unhealthy markieren wenn Memory zu hoch
if ($memoryMb > 500) {
$health['status'] = 'unhealthy';
$response->status(503);
}
$response->header('Content-Type', 'application/json');
$response->end(json_encode($health));
return;
}
// Normale Request-Verarbeitung...
});
Docker Deployment
Hier ist ein produktionsreifes Dockerfile für Swoole:
FROM php:8.3-cli-alpine
# Swoole installieren
RUN apk add --no-cache $PHPIZE_DEPS \
&& pecl install swoole \
&& docker-php-ext-enable swoole \
&& apk del $PHPIZE_DEPS
# Composer installieren
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /app
# Dependencies installieren
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader
# Application kopieren
COPY . .
# Health-Check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:9501/health || exit 1
EXPOSE 9501
# Mit ordentlichem Signal-Handling laufen lassen
CMD ["php", "server.php"]
Monitoring und Debugging
Stack-Traces mit Fibers können verwirrend sein, weil sie nur den Execution-Path des aktuellen Fibers zeigen. Nutze strukturiertes Logging:
<?php
use Psr\Log\LoggerInterface;
class FiberLogger
{
private LoggerInterface $logger;
public function logFiberExecution(string $context, callable $fn): mixed
{
$fiberId = spl_object_id(Fiber::getCurrent() ?? new stdClass());
$this->logger->info("Fiber-Ausführung gestartet", [
'fiber_id' => $fiberId,
'context' => $context,
]);
try {
$result = $fn();
$this->logger->info("Fiber-Ausführung abgeschlossen", [
'fiber_id' => $fiberId,
'context' => $context,
]);
return $result;
} catch (Throwable $e) {
$this->logger->error("Fiber-Ausführung fehlgeschlagen", [
'fiber_id' => $fiberId,
'context' => $context,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw $e;
}
}
}
Häufige Fallstricke (und wie du sie vermeidest)
Nachdem ich Async PHP mehrfach in Production deployed hab, sind hier die Fehler, die ich immer wieder sehe:
1. Verwaiste Fibers
Das Problem: Fibers erstellen, aber vergessen sie zu resumen, erzeugt Memory-Leaks.
<?php
// ❌ SCHLECHT: Fiber suspended, aber nie resumed
$fiber = new Fiber(function () {
Fiber::suspend(); // Bleibt hier für immer hängen
echo "Das wird nie ausgeführt\n";
});
$fiber->start();
// Ups, vergessen zu resumen - Memory-Leak!
// ✅ GUT: Immer den Fiber-Lifecycle abschließen
$fiber = new Fiber(function () {
$data = Fiber::suspend();
return "Verarbeitet: {$data}";
});
$fiber->start();
$result = $fiber->resume('input'); // Fiber endet und wird garbage-collected
2. Blockierende Operationen in Async-Code
Das Problem: Echt blockierende Funktionen (wie sleep()) in Async-Code zu
nutzen, macht den Zweck zunichte.
<?php
// ❌ SCHLECHT: Blockiert die gesamte Event-Loop
go(function () {
sleep(5); // Alles friert für 5 Sekunden ein
echo "Fertig\n";
});
// ✅ GUT: Non-blocking Sleep nutzen
go(function () {
Co::sleep(5); // Andere Coroutines laufen weiter
echo "Fertig\n";
});
3. Shared State ohne Synchronization
Das Problem: Mehrere Coroutines, die Shared State modifizieren, verursachen Race Conditions.
<?php
// ❌ SCHLECHT: Race Condition
$counter = 0;
for ($i = 0; $i < 100; $i++) {
go(function () use (&$counter) {
$counter++; // Nicht atomar!
});
}
// ✅ GUT: Amps Mutex für Synchronization nutzen
use Amp\Sync\LocalMutex;
$mutex = new LocalMutex();
$counter = 0;
for ($i = 0; $i < 100; $i++) {
async(function () use ($mutex, &$counter) {
$lock = $mutex->acquire();
try {
$counter++;
} finally {
$lock->release();
}
});
}
4. Timeouts nicht behandeln
Das Problem: Unbegrenzt auf externe Services warten.
<?php
// ❌ SCHLECHT: Kein Timeout, könnte für immer hängen
$response = await($client->request(new Request('https://slow-api.com')));
// ✅ GUT: Immer Timeouts setzen
use Amp\TimeoutCancellation;
$cancellation = new TimeoutCancellation(5.0); // 5 Sekunden Timeout
try {
$response = await($client->request(
new Request('https://slow-api.com'),
$cancellation
));
} catch (CancelledException $e) {
echo "Request nach 5 Sekunden getimeoutet\n";
}
Fazit
Drei Monate nachdem ich meinen API-Aggregation-Service auf Async PHP (mit ReactPHP) umgestellt hab, handhaben wir 10x den Traffic auf derselben Infrastruktur. Response-Zeiten gingen von 2,1 Sekunden auf 520ms runter. Memory-Nutzung ging sogar runter trotz höherem Durchsatz.
Die Transformation war nicht umsonst. Wir haben zwei Wochen mit Refactoring verbracht, eine weitere Woche mit Memory-Leak-Fixes, und wir maintainen strengeres Monitoring als vorher. Aber der Payoff? Unsere Infrastruktur-Kosten sind um 60% gefallen und unsere User sind glücklicher.
Der Async-PHP-Entscheidungsbaum:
- Brauchst du Raw-Performance und Kontrolle? → Swoole (aber sei bereit für Deployment-Komplexität)
- Willst du reines PHP ohne Extensions? → ReactPHP (battle-tested und verlässlich)
- Baust du hochgradig concurrent moderne Apps? → Amp (saubere Syntax, skaliert gut)
- Lernst du grade Async-Konzepte? → Fang mit rohen Fibers an, upgrade dann zum Framework
Denk dran: Async PHP geht nicht darum, CPU-intensive Operationen schneller zu machen. Es geht darum, keine Zeit mit Warten zu verschwenden. Wenn deine Application viel I/O macht (API-Calls, Datenbank-Queries, File-Operationen), kann Async PHP sie von träge zu snappy verwandeln.
Das PHP, das du kanntest, ist tot. Lang lebe Async PHP.
Weiterführende Links
- PHP Fibers Official Documentation
- ReactPHP Documentation
- Swoole Documentation
- Amp Documentation
- Async PHP in 2025: Beyond Workers with Fibers, ReactPHP, and Amp
- PHP Fibers: The Game-Changer That Makes Async Programming Feel Like Magic
- PHP Runtime Benchmark Comparison
Hast du Async-PHP-Horror-Stories oder Erfolgsgeschichten? Teile sie in den Kommentaren. Ich bin besonders an Production-Deployment-Patterns und dem interessiert, was um 3 Uhr morgens kaputtgegangen ist.