{ }
GET
REST API · Webhooks · Polling · Callbacks · Architektur
Webhooks vs. Polling vs. REST Callbacks
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.

15 Min. Lesezeit Webhooks · Polling · Long Polling · SSE · REST Callbacks · Symfony Symfony 6.x / 7.x · PHP 8.2+

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.

10. FAQ: Webhooks vs. Polling vs. REST Callbacks

1Wann ist Polling besser als Webhooks?
Consumer hinter Firewall, seltene Events, begrenzte Implementierungszeit, keine Echtzeit-Anforderung. Polling ist einfacher und oft ausreichend.
2REST Callbacks vs. Webhooks?
REST Callbacks: Callback-URL pro Request, kein Subscription-Management. Webhooks: feste Subscription für alle Events eines Typs, permanente Verwaltung erforderlich.
3Was ist Long Polling?
Server hält HTTP-Request offen bis Event eintritt oder Timeout. Nahezu Echtzeit ohne Webhook-Infrastruktur – aber bindet Server-Ressourcen.
4Polling in Symfony implementieren?
POST → 202 Accepted + Location-Header + Job-ID. GET /api/jobs/{id} → Status mit Retry-After-Header. Bei completed: result_url mitgeben.
5SSRF-Schutz für REST Callbacks?
Callback-URL validieren: keine privaten IPs (10.x, 192.168.x, 127.x), kein localhost, nur HTTPS. Symfony HttpClient mit SSRF-Protection konfigurieren.
6Welche Retry-Strategie für Webhooks?
Exponential Backoff: 30s, 60s, 120s, 240s, 480s. Max. 5 Retries. Danach Dead-Letter-Queue. Delivery-Status im Monitoring sichtbar machen.
7Was sind Server-Sent Events?
HTTP-Standard für Server-zu-Client-Streams (text/event-stream). Browser: EventSource API. Ideal für Dashboards und Broadcasting. Symfony Mercure Bundle für einfache Implementierung.
8Wie viele Webhooks kann Symfony pro Sekunde senden?
Webhook-Dispatch über Symfony Messenger in Queue-Worker. Mit RabbitMQ oder Redis als Transport: hunderte Dispatches pro Sekunde möglich.
9Polling und Webhooks gleichzeitig anbieten?
Ja, verbreitete Praxis für Public APIs. Webhook-Consumer erhalten Echtzeit. Firewall-Consumer pollten denselben Status-Endpunkt. Beide Wege führen zum selben Ergebnis.
10Polling-Endpunkte in OpenAPI dokumentieren?
202-Response mit Location-Header. GET-Endpunkt mit Status-enum. Retry-After in 200-Response. links-Objekt mit operationId des GET-Endpunkts für automatisches Client-Wissen.