REST ist für mich nicht der automatische Standard für jede API. Es ist der Standard, den viele Teams nicht mehr hinterfragen. Sobald zwei Backend-Services miteinander sprechen sollen, entsteht deshalb fast immer dieselbe Architektur: ein HTTP-Endpunkt, JSON hinein, JSON heraus, eine OpenAPI-Datei dazu, fertig.
Das funktioniert. Die interessantere Frage ist, ob es für diesen konkreten Kommunikationsweg die richtige Schnittstelle ist.
Meine Voreinstellung ist inzwischen ziemlich klar: Für eine öffentliche oder breit genutzte API fange ich mit REST an. Für kontrollierte Kommunikation zwischen Backend-Services prüfe ich gRPC zuerst. Nicht weil gRPC moderner ist, sondern weil interne Services andere Anforderungen haben als eine API, die Browser, Partner oder unbekannte Clients benutzen.
Zwei Denkweisen, nicht zwei Formate
Die Diskussion wird oft geführt, als wären REST und gRPC zwei technische Varianten derselben Sache. Das greift zu kurz.
Eine REST-API beschreibt Ressourcen über HTTP. Sie ist für Menschen leicht zu verstehen, mit Standardwerkzeugen aufzurufen und braucht keinen speziellen Client. Ein gRPC-Service beginnt dagegen mit einem ausdrücklichen Vertrag: Methoden und Nachrichten stehen in einer .proto-Datei, daraus entstehen Client und Server für jede verwendete Sprache. Dazu kennt gRPC neben dem einfachen Aufruf auch Client-, Server- und bidirektionales Streaming.
Das verändert, wie ich über die Schnittstelle nachdenke. Bei REST denke ich in Ressourcen:
GET /customers/4711
POST /payments
PATCH /subscriptions/815Bei gRPC denke ich in Fähigkeiten eines Dienstes:
service PaymentService {
rpc AuthorizePayment(AuthorizePaymentRequest) returns (AuthorizePaymentResponse);
rpc CapturePayment(CapturePaymentRequest) returns (CapturePaymentResponse);
rpc RefundPayment(RefundPaymentRequest) returns (RefundPaymentResponse);
}Keines davon ist grundsätzlich besser. Aber je nachdem, wer die Schnittstelle kontrolliert und wie sie benutzt wird, fühlt sich eines davon deutlich natürlicher an.
Der Vorteil ist der Vertrag, nicht die Geschwindigkeit
gRPC wird gern über Geschwindigkeit verkauft. Protocol Buffers sind kompakt, gRPC läuft über HTTP/2, Verbindungen werden wiederverwendet, Streaming gehört zum Modell. Für bestimmte Kommunikationsmuster macht das einen spürbaren Unterschied.
Trotzdem ist Performance für mich selten der erste Grund. Braucht die Serialisierung zwei Millisekunden und der Datenbankzugriff dahinter achtzig, löst ein Protokollwechsel das falsche Problem.
Der größere Vorteil liegt woanders: Der Vertrag zwischen den Services wird härter. Nehmen wir einen Service, der vom anderen Informationen über einen Kunden braucht. Mit REST sieht die Antwort ungefähr so aus:
{
"id": 4711,
"status": "active",
"limit": 5000
}Was bedeutet limit? Cent oder Euro? Tageslimit, verfügbares Limit, Kreditrahmen? Natürlich lässt sich das dokumentieren: mit OpenAPI, mit JSON Schema, mit generierten Clients und Contract Tests. Mache ich auch. Bei gRPC beginnt die Schnittstelle aber schon mit dem Schema:
enum CustomerStatus {
// Pflicht in proto3 und bewusst ohne Bedeutung: Ein fehlendes
// Feld liest sich so als "nicht gesetzt", nicht als "aktiv".
CUSTOMER_STATUS_UNSPECIFIED = 0;
CUSTOMER_STATUS_ACTIVE = 1;
CUSTOMER_STATUS_BLOCKED = 2;
}
message Customer {
int64 id = 1;
CustomerStatus status = 2;
int64 credit_limit_cents = 3;
}Der Unterschied ist nicht, dass REST so etwas nicht könnte. Der Unterschied ist, dass ich bei gRPC sehr viel schwerer darum herumkomme. Wenn zehn Entwickler an mehreren Backend-Systemen arbeiten, will ich nicht, dass jeder Client seine eigene Vorstellung davon entwickelt, was eine Antwort bedeutet.
Noch wichtiger wird das, wenn sich die Schnittstelle ändert. Fehler in verteilten Systemen entstehen selten innerhalb eines Services, sondern dazwischen: Ein Enum bekommt einen neuen Wert, ein Feld wird umbenannt, ein Client liest null anders als der nächste, ein Team aktualisiert den Server und das andere zwei Wochen später den Client. Das ist kein exotischer Fall, das ist normaler Betrieb.
Protocol Buffers zwingen dazu, das als Vertragsänderung zu behandeln. Felder werden über ihre Nummer übertragen, nicht über den Namen. Wer ein Feld ersetzt, vergibt eine neue Nummer und sperrt die alte:
message Customer {
reserved 3; // war: int64 limit. Nie wieder vergeben.
reserved "limit";
int64 id = 1;
CustomerStatus status = 2;
int64 credit_limit_cents = 4;
}Ein Werkzeug wie buf breaking vergleicht die .proto-Dateien in der CI mit dem letzten Stand und meldet, wenn eine Änderung bestehende Clients bricht. Ich kann eine Schnittstelle trotzdem kaputtmachen, kein Werkzeug verhindert schlechte Entscheidungen. Aber die Hürde liegt höher, und bei internen Services ist genau diese Reibung sinnvoll.
Wo die Grenze zu Code wurde
Ich habe in mehreren produktiven Backend-Systemen beides eingesetzt, REST und gRPC, unter anderem in Umgebungen mit Go-Services, PHP-Altbestand, AWS und mehreren Millionen Nutzern.
Gerade beim schrittweisen Herauslösen von Logik aus einem bestehenden System war die Schnittstellengrenze entscheidend. Der neue Service sollte nicht einfach auf dieselben Datenbanktabellen zugreifen. Sonst hätte ich keinen Service gebaut, sondern nur einen Teil des Monolithen in einen anderen Prozess verschoben. Die neue Komponente brauchte einen klaren Vertrag mit dem bestehenden System.
Genau dort ist gRPC stark. Nicht wegen eines Benchmark-Balkens, sondern weil aus „der Payment-Service braucht irgendwie diese Daten" eine ausdrückliche Schnittstelle wird:
rpc GetPaymentState(GetPaymentStateRequest) returns (GetPaymentStateResponse);Die Nachrichtentypen gehören zum Vertrag, Änderungen daran werden sichtbar, die Clients werden daraus erzeugt. Die Grenze wird Code. Wie das in einem Zahlungssystem mit Millionen Nutzern aussah, steht in der Case Study Payment-Backend für 6 Mio. Nutzer.
Eine Einschränkung gehört bei PHP dazu: Als gRPC-Client funktioniert PHP über die grpc-Erweiterung gut. Als gRPC-Server ist es mit PHP-FPM nicht vorgesehen und braucht eine eigene Laufzeit wie RoadRunner. In der Praxis ruft das Altsystem deshalb meistens den neuen Go-Service auf, nicht umgekehrt, und das passt in aller Regel ohnehin zur Richtung der Modernisierung.
Wie Go-Services neben einer bestehenden PHP-Anwendung entstehen, ohne alles umzubauen, steht in Golang in einer PHP-Welt.
Wann ich REST bevorzuge
Ich würde trotzdem nicht anfangen, vorhandene REST-APIs durch gRPC zu ersetzen. In vier Fällen ist REST für mich eindeutig die bessere Wahl.
Öffentliche APIs. Wenn ich nicht kontrolliere, wer die Schnittstelle nutzt, gewinnt REST fast immer. Eine API für Kunden, Integrationspartner oder externe Entwickler sollte möglichst wenig voraussetzen. Eine HTTP-Anfrage kann praktisch jede Plattform senden, JSON kann fast jeder lesen, und ein Entwickler probiert einen Endpunkt mit curl aus, ohne erst Code zu generieren. Bei öffentlichen Schnittstellen ist Zugänglichkeit selbst ein Architekturmerkmal.
Der Browser als direkter Client. gRPC im Browser geht über gRPC-Web, braucht aber in der Regel einen Proxy, der zwischen Browser und klassischem gRPC-Service übersetzt. Wenn ein Frontend einfach Daten laden soll, schiebe ich das nicht automatisch dazwischen. REST ist dort die langweiligere Lösung, und langweilig ist in der Architektur häufig ein Kompliment.
Kleine Systeme. Bei drei Endpunkten zwischen zwei Anwendungen brauche ich keine Schnittstellenbeschreibungssprache, keine Codegenerierung und keine zusätzlichen Build-Schritte. Ich versuche nicht, Architekturprobleme vorwegzunehmen, die ein System noch gar nicht hat.
Schnittstellen, die Menschen regelmäßig untersuchen. REST ist transparent: Anfrage kopieren, curl aufrufen, JSON ansehen. Bei Support, Fehlersuche oder manuellen Integrationen ist das nicht zu unterschätzen. Einen Admin-Endpunkt oder eine Partner-API bewerte ich deshalb anders als einen internen Aufruf, der mehrere Millionen Mal am Tag zwischen zwei Services läuft.
Wann ich gRPC bevorzuge
Die Entscheidung kippt in Richtung gRPC, wenn mehrere Bedingungen zusammenkommen.
Beide Seiten gehören uns. Das ist die wichtigste Voraussetzung. Wenn mein Team oder meine Organisation Client und Server kontrolliert, verliert die universelle Zugänglichkeit von REST stark an Gewicht. Ich muss nicht für jeden denkbaren Nutzer optimieren, sondern baue die Schnittstelle für genau die Systeme, die miteinander sprechen. Dort wird ein strikter Vertrag wertvoll.
Es gibt viele Aufrufe zwischen Services. Ein einzelner REST-Aufruf fällt kaum ins Gewicht. Bei sehr vielen internen Aufrufen werden Verbindungen, Serialisierung, Nutzlastgröße, Timeouts, Wiederholungen und Beobachtbarkeit Teil der Architektur. Für genau diese Service-zu-Service-Kommunikation bringt gRPC viel Infrastruktur mit.
Mehrere Sprachen treffen aufeinander. Das finde ich bei der Legacy-Modernisierung besonders interessant. Das bestehende System ist PHP, neue Komponenten entstehen in Go, später kommt vielleicht ein Python-Service dazu. Dann will ich nicht drei handgeschriebene Umsetzungen desselben Vertrags pflegen. Die .proto-Datei wird zum gemeinsamen Nenner. Nicht PHP entscheidet über die Schnittstelle, nicht Go. Der Vertrag entscheidet.
Streaming gehört wirklich zum Problem. Wenn Daten nicht als einzelne Anfrage mit einzelner Antwort fließen, wird REST unbequem. Das heißt nicht, dass ich Streaming überall einsetze, im Gegenteil: Lang laufende Streams verändern Lastverteilung, Fehlerbehandlung und Fehlersuche, und selbst die gRPC-Dokumentation rät, Streaming nur zu nehmen, wenn es der Anwendung oder der Performance echten Nutzen bringt. Aber wenn es dazugehört, muss ich es bei gRPC nicht auf eine Schnittstelle kleben, die ursprünglich nur Anfrage und Antwort kannte.
Ein gutes REST schlägt ein schlechtes gRPC
Ein Missverständnis räume ich direkt aus: Eine schlechte REST-API ist kein Argument für gRPC.
REST lässt sich sehr sauber bauen. OpenAPI liefert einen formalen Vertrag, daraus entstehen Clients, Schemas werden validiert, Versionierung lässt sich diszipliniert betreiben, und Contract Tests sichern Änderungen ab. Ein gutes REST-System ist besser als ein schlecht gebautes gRPC-System.
Die eigentliche Frage lautet deshalb nicht, welche Technologie mich zu guter Architektur zwingt. Das tut keine. Sie lautet: Welche Technologie macht die gewünschte Arbeitsweise zum natürlichen Weg? Für interne, eng verbundene Service-Kommunikation gefällt mir die Antwort von gRPC häufig besser.
Das Netzwerk bleibt unzuverlässig
Egal welches Protokoll ich wähle, an einer Stelle bleibt die Architektur gleich schwierig. Ein Service kann antworten. Er kann nicht antworten. Er kann antworten, nachdem der Client längst aufgegeben hat. Der Client kann die Verbindung verlieren, obwohl der Server die Operation schon ausgeführt hat. Ein Retry kann deshalb eine zweite Zahlung auslösen.
Daran ändert gRPC nichts. Es bringt zwar Statuscodes, Deadlines, Abbruch und Metadaten als feste Konzepte mit, aber Deadlines müssen bewusst gesetzt werden. Ohne Deadline kann ein Client unbegrenzt auf eine Antwort warten, und das ist die Voreinstellung.
ctx, cancel := context.WithTimeout(ctx, 300*time.Millisecond)
defer cancel()
resp, err := client.AuthorizePayment(ctx, &paymentv1.AuthorizePaymentRequest{
IdempotencyKey: auftrag.ID,
AmountCents: auftrag.BetragCent,
})
if status.Code(err) == codes.DeadlineExceeded {
// Nicht blind wiederholen: Die Zahlung kann trotzdem durchgegangen
// sein. Mit demselben Schlüssel erneut anfragen, der Server erkennt
// den Vorgang und liefert das erste Ergebnis statt einer zweiten Buchung.
}Die eigentliche Arbeit bleibt bei der Anwendung:
- Welche Operation ist idempotent, und was darf wiederholt werden?
- Welche Deadline ist fachlich sinnvoll, und was passiert nach einem Timeout?
- Welche Trace-ID läuft über die Servicegrenze?
- Wie erkenne ich, ob die Operation auf der anderen Seite trotzdem ausgeführt wurde?
Das sind wichtigere Fragen als JSON gegen Protobuf. Wer sie nicht beantwortet, bekommt mit gRPC nur ein schnelleres verteiltes Problem.
Dazu kommt die Sichtbarkeit. Bei REST sehe ich mit einfachen Werkzeugen viel: URL, Methode, Header, JSON, Statuscode. gRPC ist binär und arbeitet über generierte Clients und spezialisierte Werkzeuge wie grpcurl. Im Betrieb ist das kein Problem, wenn die Observability steht, sonst wird es eines. Zu einem gRPC-Service gehören für mich deshalb von Anfang an strukturierte Logs, Tracing über Servicegrenzen, Metriken je Methode mit Latenzen und Fehlerraten und eine klare Zuordnung zwischen aufrufendem und aufgerufenem Service. Je weniger lesbar die Leitung, desto besser muss die Beobachtbarkeit sein.
Wie Tracing über eine PHP- und eine Go-Seite hinweg eingeführt wird, steht in OpenTelemetry in PHP und Go einführen.
Außen REST, innen gRPC
Eine Architektur kann REST und gRPC gleichzeitig verwenden, und das ist häufig meine bevorzugte Lösung. Gerade bei gewachsenen Systemen halte ich wenig davon, eine neue Technologie überall durchzudrücken. Besitzt ein PHP-Monolith eine funktionierende REST-API, ersetze ich sie nicht, nur weil daneben ein Go-Service entsteht. Das wäre Beschäftigung. Ich frage stattdessen, welche neue Grenze entsteht:
Browser / Partner / Kunde
|
REST öffentliche Grenze: Zugänglichkeit
|
PHP-Anwendung
|
gRPC interne Grenze: Vertrag
|
Go Payment Service
|
gRPC
|
Go Account ServiceDas ist keine Inkonsequenz. Es sind zwei Grenzen mit unterschiedlichen Anforderungen. Die öffentliche optimiert auf Kompatibilität und Zugänglichkeit, die interne auf einen ausdrücklichen Vertrag, kontrollierte Clients und verlässliche Kommunikation. Das bestehende System behält seine Außenwelt, während innen eine neue, klarere Servicegrenze entsteht.
Das ist für mich wesentlich näher an sinnvoller Modernisierung als ein Projekt mit dem Ziel „Wir stellen jetzt unsere APIs auf gRPC um". Technologie ist kein Modernisierungsziel. Eine bessere Grenze kann eines sein.
Wie eine stabile Außenschnittstelle vor ein Altsystem kommt, steht in Eine API vor den Monolithen setzen.
Die Entscheidung als Tabelle
So würde ich den Vergleich tatsächlich für eine Entscheidung nutzen:
| Frage | REST | gRPC |
|---|---|---|
| Öffentliche API | meist die erste Wahl | nur mit gutem Grund |
| Interne Service-zu-Service-Kommunikation | gut möglich | meist die erste Prüfung |
| Browser als Client | sehr gut | nur über gRPC-Web und Proxy |
| Für Menschen direkt lesbar | sehr gut | schlechter |
| Vertrag | mit OpenAPI sehr gut möglich | zentraler Bestandteil |
| Client-Generierung | möglich | natürlicher Teil des Ablaufs |
| Mehrere Sprachen im Backend | gut | sehr stark |
| Streaming | zusätzliche Konzepte nötig | Teil des Modells |
| Sehr viele interne Aufrufe | funktioniert | häufig attraktiver |
| Fehlersuche mit einfachen Werkzeugen | hervorragend | aufwendiger |
| Anbindung eines Altsystems | als erste Grenze oft leichter | stark zwischen neuen, kontrollierten Services |
| Erfahrung für externe Entwickler | sehr stark | meist eine unnötige Hürde |
Die Tabelle ist trotzdem nur der Anfang. Die Entscheidung fällt an der Systemgrenze, nicht in der Zeile.
Wann ich gRPC ausdrücklich nicht wähle
Auch wenn ich gRPC für interne Backend-Kommunikation mag, gibt es klare Gegenanzeigen. Ich führe es nicht ein, wenn
- praktisch alle Aufrufe aus Browsern kommen,
- externe Entwickler die API nutzen,
- das Team keine Erfahrung mit dem zusätzlichen Werkzeug hat und auch keinen Nutzen daraus zieht,
- nur zwei oder drei einfache Endpunkte existieren,
- die vorhandene REST-Schnittstelle sauber beschrieben und stabil ist,
- der einzige Grund „Performance" heißt, ohne dass jemand ein Performanceproblem gemessen hat.
Der letzte Punkt ist mir wichtig. Eine Architektur soll kein Benchmark gewinnen, sie soll ein Problem lösen.
Die erste Frage
Wenn ich heute einen neuen Backend-Service baue, stelle ich zuerst eine Frage: Wer kontrolliert beide Seiten dieser Schnittstelle?
Lautet die Antwort „wir", wird gRPC interessant, und ich prüfe weiter: Wie häufig wird aufgerufen? Wie viele Clients gibt es, in wie vielen Sprachen? Wie wichtig ist ein harter Vertrag? Brauchen wir Streaming? Und wie sehen Observability und Deployment aus?
Bei einer internen Schnittstelle zwischen kontrollierten Backend-Services, besonders in Go, prüfe ich gRPC heute sehr ernsthaft und wähle es häufig. Bei einer Schnittstelle für Browser, Partner, Kunden oder unbekannte künftige Nutzer bleibe ich meistens bei REST.
Nicht weil REST alt und gRPC neu ist. Nicht weil JSON langsam und Protobuf schnell ist. Sondern weil die Grenzen andere Anforderungen haben. Öffentliche APIs brauchen Offenheit, interne APIs vor allem einen belastbaren Vertrag. Danach wähle ich das Protokoll.
Wer gerade vor genau dieser Grenze steht, findet das Vorgehen auf der Seite zu Go-Services neben PHP und zur Backend-Entwicklung.

