wann welche Strategie – und warum
Nicht jede asynchrone Anforderung braucht Webhooks. Polling ist manchmal die richtigere Wahl. Und REST Callbacks existieren als Hybridansatz, den viele nicht kennen. Wer den Unterschied versteht, trifft bessere Architekturentscheidungen – und baut keine Webhook-Infrastruktur auf, wo ein einfaches 30-Sekunden-Polling die sauberere Lösung wäre.
Inhaltsverzeichnis
- 1. Das Grundproblem: Asynchrone Events in synchronen APIs
- 2. Polling: einfach, kontrolliert, unterschätzt
- 3. Long Polling: Latenz ohne Webhook-Komplexität
- 4. Webhooks: Server-Initiated, event-driven
- 5. REST Callbacks: Webhooks mit strukturiertem Contract
- 6. Server-Sent Events: Stream statt Push
- 7. Entscheidungshilfe: Welche Strategie wann?
- 8. Vergleichsmatrix: Alle Strategien im Überblick
- 9. Zusammenfassung
- 10. FAQ
1. Das Grundproblem: Asynchrone Events in synchronen APIs
REST ist ein synchrones Protokoll: Client sendet Request, Server antwortet. Für lang dauernde Operationen – Zahlungsabwicklung, Bildverarbeitung, Bestellstatus-Updates – ist dieses Modell problematisch. Der Client kann nicht 5 Minuten auf eine HTTP-Response warten. Es braucht einen Mechanismus, der den Client informiert, wenn das Ergebnis vorliegt, ohne dass der Client dauerhaft wartet.
Drei grundsätzliche Ansätze existieren: Der Client fragt regelmäßig nach (Polling). Der Server benachrichtigt den Client (Webhooks, SSE). Oder ein Hybrid: Der Client gibt dem Server eine Callback-URL, und der Server informiert den Client, wenn das Ergebnis fertig ist (REST Callbacks). Jeder Ansatz hat einen Anwendungsfall, für den er die beste Wahl ist – und keiner ist universell überlegen.
2. Polling: einfach, kontrolliert, unterschätzt
Polling hat einen schlechten Ruf, der in vielen Fällen nicht verdient ist. Das Modell: Client sendet eine Anfrage, erhält eine Job-ID zurück (202 Accepted), und fragt in regelmäßigen Abständen GET /jobs/{id} ab, bis der Status completed ist. Das ist einfach zu implementieren, einfach zu testen, einfach zu debuggen und funktioniert hinter jedem Proxy und Firewall.
Polling ist die richtige Wahl, wenn: Die Event-Häufigkeit gering ist (seltener als alle 30 Sekunden), der Consumer sowieso im 5-Minuten-Takt aktiv ist, die Netzwerkinfrastruktur auf der Consumer-Seite keine eingehenden HTTP-Verbindungen erlaubt (Firewalls, NAT), oder wenn keine zuverlässige Webhook-Delivery-Infrastruktur aufgebaut werden soll. In Szenarien mit wenigen Clients und nicht-zeitkritischen Events ist Polling oft die wartbarste und kosteneffizienteste Lösung.
<?php
// src/Controller/Api/JobController.php
declare(strict_types=1);
namespace App\Controller\Api;
use App\Service\JobService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
/**
* Job status controller implementing the polling pattern.
* Clients poll GET /api/jobs/{id} until status is 'completed' or 'failed'.
*/
#[Route('/api/jobs', name: 'api_jobs_')]
final class JobController extends AbstractController
{
public function __construct(
private readonly JobService $jobService,
) {}
/**
* Start a long-running job and return 202 with job ID.
* Client polls GET /api/jobs/{id} for status updates.
*/
#[Route('', name: 'create', methods: ['POST'])]
public function create(): JsonResponse
{
$jobId = $this->jobService->dispatch();
return $this->json([
'job_id' => $jobId,
'status' => 'queued',
'poll_url' => '/api/jobs/' . $jobId,
'poll_interval_seconds' => 5,
], Response::HTTP_ACCEPTED, [
'Location' => '/api/jobs/' . $jobId,
]);
}
/**
* Poll job status. Returns 200 with current status.
* Clients should stop polling when status is 'completed' or 'failed'.
*/
#[Route('/{id}', name: 'status', methods: ['GET'])]
public function status(string $id): JsonResponse
{
$job = $this->jobService->get($id);
$response = [
'job_id' => $job->id,
'status' => $job->status->value, // queued|processing|completed|failed
'created_at' => $job->createdAt->format(\DateTimeInterface::ATOM),
'updated_at' => $job->updatedAt->format(\DateTimeInterface::ATOM),
];
if ($job->isCompleted()) {
$response['result_url'] = '/api/jobs/' . $id . '/result';
}
if ($job->isFailed()) {
$response['error'] = $job->errorMessage;
}
// Hint: retry after X seconds while pending
$headers = $job->isPending()
? ['Retry-After' => '5']
: [];
return $this->json($response, Response::HTTP_OK, $headers);
}
}
3. Long Polling: Latenz ohne Webhook-Komplexität
Long Polling ist ein Mittelweg zwischen Polling und Webhooks: Der Client sendet eine HTTP-Anfrage, und der Server hält die Verbindung offen, bis ein Event eintritt oder ein Timeout erreicht wird. Dann antwortet der Server mit dem Event oder mit 304 Not Modified bei Timeout, und der Client sendet sofort die nächste Anfrage. Das ergibt nahezu Echtzeit-Latenz bei Serverinfrastruktur, die nur eingehende Verbindungen benötigt – keine outbound HTTP-Requests an Consumer-Endpoints.
Long Polling hat in modernen Symfony-Anwendungen einen Nachteil: Es bindet einen PHP-Worker-Prozess für die gesamte Wartezeit. Bei vielen gleichzeitigen Long-Polling-Clients können PHP-FPM-Worker erschöpft sein, bevor tatsächliche Events verarbeitet werden. Das macht Long Polling in PHP-Umgebungen weniger praktikabel als in Node.js oder Go. Eine Alternative: Mercure Protocol (implementiert über Symfony's Mercure Component) nutzt Server-Sent Events für dasselbe Ergebnis mit besser geeigneter Serverarchitektur.
4. Webhooks: Server-Initiated, event-driven
Webhooks kehren das Client-Server-Verhältnis um: Der Consumer registriert eine URL, und der API-Server sendet bei jedem Event einen HTTP-POST an diese URL. Das ermöglicht nahezu Echtzeit-Reaktion ohne kontinuierliche Polling-Last. Die Latenz ist gering, die Infrastruktur-Last auf Server-Seite skaliert linear mit der Event-Häufigkeit, nicht mit der Anzahl wartender Clients.
Die Komplexität liegt auf der Consumer-Seite und in der Infrastruktur: Der Consumer braucht einen öffentlich erreichbaren HTTPS-Endpunkt. Firewalls müssen eingehende Verbindungen erlauben. Der Server braucht eine Retry-Logik, einen Dead-Letter-Queue für nicht zustellbare Events, Monitoring der Zustellungsrate und eine Subscription-Verwaltung. Das ist erheblich mehr Infrastruktur als Polling. Webhooks sind die richtige Wahl, wenn Events zeitkritisch sind, die Consumer-Infrastruktur eingehende Verbindungen unterstützt, und die Event-Last hoch genug ist, dass Polling unwirtschaftlich wäre.
<?php
// src/Service/WebhookDispatchService.php
declare(strict_types=1);
namespace App\Service;
use App\Entity\WebhookSubscription;
use App\Repository\WebhookSubscriptionRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* Dispatches webhook events to registered consumer endpoints.
* Uses exponential backoff for failed deliveries.
*/
final class WebhookDispatchService
{
private const MAX_RETRIES = 5;
public function __construct(
private readonly WebhookSubscriptionRepository $subscriptionRepo,
private readonly LoggerInterface $logger,
) {}
/**
* Dispatch an event to all subscriptions matching the event type.
*
* @param string $eventType e.g. 'order.created'
* @param array<string, mixed> $payload Event payload data
*/
public function dispatch(string $eventType, array $payload): void
{
$subscriptions = $this->subscriptionRepo->findActiveByEventType($eventType);
$client = HttpClient::create(['timeout' => 10]);
foreach ($subscriptions as $subscription) {
$body = json_encode($payload, JSON_THROW_ON_ERROR);
$signature = 'sha256=' . hash_hmac('sha256', $body, $subscription->getSecret());
try {
$response = $client->request('POST', $subscription->getUrl(), [
'headers' => [
'Content-Type' => 'application/json',
'X-Mironsoft-Signature' => $signature,
'X-Mironsoft-Event-Type' => $eventType,
'X-Mironsoft-Event-Id' => $payload['event_id'],
],
'body' => $body,
]);
if ($response->getStatusCode() >= 400) {
$this->scheduleRetry($subscription, $eventType, $payload, 1);
}
} catch (TransportExceptionInterface $e) {
$this->logger->error('Webhook delivery failed', [
'url' => $subscription->getUrl(),
'event_type' => $eventType,
'error' => $e->getMessage(),
]);
$this->scheduleRetry($subscription, $eventType, $payload, 1);
}
}
}
private function scheduleRetry(
WebhookSubscription $subscription,
string $eventType,
array $payload,
int $attempt,
): void {
if ($attempt > self::MAX_RETRIES) {
$this->logger->critical('Webhook delivery permanently failed', [
'subscription_id' => $subscription->getId(),
'event_type' => $eventType,
]);
// Move to dead letter queue / disable subscription
return;
}
// Exponential backoff: 30s, 60s, 120s, 240s, 480s
$delaySeconds = 30 * (2 ** ($attempt - 1));
// Schedule via Symfony Messenger with a delay
$this->logger->info(sprintf(
'Scheduling webhook retry #%d in %ds',
$attempt,
$delaySeconds
));
}
}
5. REST Callbacks: Webhooks mit strukturiertem Contract
REST Callbacks sind ein Hybridansatz: Der Client gibt beim initialen Request eine Callback-URL mit (X-Callback-URL: https://client.example.com/hook), und der Server sendet das Ergebnis per POST an diese URL, wenn die Operation abgeschlossen ist. Das ist flexibler als feste Webhook-Subscriptions, weil jede einzelne Operation eine eigene Callback-URL definiert. Es ist auch in OpenAPI 3.0+ mit dem callbacks-Keyword dokumentierbar.
REST Callbacks passen gut für langläufige Einzeloperationen: PDF-Generierung, Video-Encoding, Report-Erstellung. Der Aufrufer braucht keine permanente Webhook-Subscription, nur für diesen Request einen erreichbaren Endpunkt. Der Nachteil: Der Server muss die Callback-URL validieren (SSRF-Schutz) und ist auf die Erreichbarkeit des Consumer-Endpunkts zum Zeitpunkt der Completion angewiesen – anders als bei Polling, wo der Consumer selbst den Zeitpunkt der Abfrage bestimmt.
6. Server-Sent Events: Stream statt Push
Server-Sent Events (SSE) ist eine weniger bekannte Alternative: Der Client öffnet eine einzige HTTP-Verbindung (text/event-stream), und der Server sendet Events über diese Verbindung, solange sie offen bleibt. Im Gegensatz zu WebSockets ist SSE unidirektional (Server → Client) und HTTP-nativ – kein Protokoll-Upgrade nötig. Browser unterstützen SSE nativ über die EventSource-API.
Symfony bietet mit dem Mercure Protocol eine vollständige SSE-Implementierung über den symfony/mercure-Bundle. Ein Hub-Server (kostenlose Open-Source-Version: Caddy + Mercure-Modul) empfängt Events vom Symfony-Backend und verteilt sie über SSE an alle verbundenen Clients. Das skaliert gut für Broadcasting-Szenarien, ist aber für Private-Events an einzelne Consumer weniger geeignet als Webhooks.
7. Entscheidungshilfe: Welche Strategie wann?
Die Wahl der richtigen Strategie hängt von mehreren Faktoren ab. Die wichtigsten Fragen in der Entscheidungsfindung:
Latenzanforderung: Ist eine Reaktion innerhalb von Sekunden nötig? Dann Webhooks oder SSE. Innerhalb von Minuten? Polling mit kurzen Intervallen reicht. Tage oder Stunden? Polling mit langen Intervallen oder E-Mail-Notification.
Consumer-Infrastruktur: Kann der Consumer eingehende HTTP-Anfragen empfangen? Nein (Firewall, NAT, keine öffentliche IP)? Dann Polling oder Long Polling. Ja? Webhooks oder REST Callbacks.
Event-Häufigkeit: Wenige Events pro Tag? Polling ist einfacher. Hunderte Events pro Stunde pro Client? Polling wird zur Last auf dem Server, Webhooks sind effizienter.
Implementierungskomplexität: Webhooks brauchen Retry-Logik, Dead-Letter-Queues, Subscription-Management und HMAC-Signaturprüfung. Polling braucht einen Job-Status-Endpunkt und einen 202 Accepted-Response. Bei knapper Entwicklungszeit ist Polling oft die bessere Wahl.
8. Vergleichsmatrix: Alle Strategien im Überblick
| Strategie | Latenz | Consumer braucht | Implementierungskomplexität | Ideal für |
|---|---|---|---|---|
| Polling | Hoch (Intervall) | Outbound HTTP | Niedrig | Nicht-zeitkritische Jobs, Firewall-Umgebungen |
| Long Polling | Niedrig (Sekunden) | Outbound HTTP | Mittel | Zeitkritisch, keine Webhook-Infrastruktur |
| Webhooks | Sehr niedrig | Öffentlicher HTTPS-Endpunkt | Hoch | Viele Clients, hohe Event-Häufigkeit, Echtzeit |
| REST Callbacks | Niedrig | Erreichbarer HTTPS-Endpunkt | Mittel | Langläufige Einzeloperationen pro Request |
| Server-Sent Events | Sehr niedrig | Browser / EventSource | Mittel (Hub nötig) | Browser-Clients, Broadcasting, Dashboards |
Mironsoft
REST API Architektur, Webhook-Systeme und asynchrone Kommunikation
Asynchrone API-Architektur richtig aufsetzen?
Wir analysieren eure spezifischen Anforderungen und empfehlen die passende Strategie – Polling, Webhooks oder REST Callbacks – und implementieren die Lösung mit vollständiger Retry-Logik, Monitoring und OpenAPI-Dokumentation.
Architektur-Review
Analyse bestehender asynchroner Kommunikation und Empfehlung der optimalen Strategie
Webhook-Implementierung
HMAC-Signatur, Retry-Logik, Dead-Letter-Queue und Subscription-Management in Symfony
Dokumentation
OpenAPI 3.1 Webhook-Dokumentation mit vollständigen Payload-Schemas und Sicherheitshinweisen
9. Zusammenfassung
Webhooks sind mächtig, aber nicht universell die beste Lösung. Polling ist in vielen Szenarien die pragmatisch bessere Wahl: einfacher zu implementieren, einfacher zu debuggen, kein öffentlicher HTTPS-Endpunkt auf Consumer-Seite nötig. Long Polling schließt die Lücke bei Latenzanforderungen, wenn Webhook-Infrastruktur zu komplex ist. REST Callbacks passen für langläufige Einzeloperationen, bei denen jede Operation ihre eigene Callback-URL mitbringt. Server-Sent Events sind ideal für Browser-Clients und Broadcasting-Szenarien.
Die Entscheidung sollte auf Basis von Latenzanforderungen, Consumer-Infrastruktur, Event-Häufigkeit und verfügbarer Entwicklungszeit getroffen werden – nicht auf Basis von Trend oder Komplexitätspräferenz. Webhooks zu bauen, wo Polling gereicht hätte, ist technische Überentwicklung. Polling zu benutzen, wo Echtzeit-Events nötig wären, ist ein Produktfehler.
Webhooks vs. Polling vs. Callbacks — Das Wichtigste auf einen Blick
Polling wählen wenn
Events selten sind, Consumer hinter Firewall sitzt, Implementierungszeit begrenzt ist oder Events nicht zeitkritisch sind.
Webhooks wählen wenn
Echtzeit-Reaktion nötig ist, Consumer öffentlichen HTTPS-Endpunkt hat und Event-Häufigkeit Polling unwirtschaftlich macht.
REST Callbacks wählen wenn
Langläufige Einzeloperationen mit individuellem Callback-Endpunkt. Kein Subscription-Management nötig, aber SSRF-Schutz erforderlich.
SSE / Mercure wählen wenn
Browser-Clients, Broadcasting an viele Empfänger oder Dashboard-Updates in Echtzeit. Symfony Mercure Bundle macht das einfach.