Google acaba de liberar la versión 1.0 de gRPC y en los foros de arquitectura ya se respira la fiebre habitual. Llevamos años pegándonos con REST sobre HTTP/1.1 para conectar servicios internos. Cada salto de red implica serializar diccionarios a cadenas de texto JSON, negociar handshakes TLS y parsear cabeceras repetitivas. Funciona, sí, pero cuando una sola petición de usuario detona una cascada de doce llamadas entre microservicios, el coste de parsear texto plano empieza a devorar ciclos de CPU.
El cambio que plantea gRPC no es cosmético: sustituye el texto legible por paquetes binarios empaquetados con Protocol Buffers y monta todo el transporte sobre conexiones multiplexadas de HTTP/2.
La factura oculta de REST y JSON
El problema de REST en arquitecturas distribuidas no es la semántica de verbos GET o POST. El problema es el formato y el transporte.
JSON es un formato pensado para humanos. Para transmitir un entero de 32 bits con valor 1000000, JSON envía siete bytes ASCII ("1000000"). Si transmites un array de mil registros, estás pagando el peaje de comillas, llaves, nombres de campos repetidos en cada fila y la conversión de cadenas a tipos nativos en memoria.
A nivel de red, HTTP/1.1 empeora el cuadro. Por cada petición concurrente necesitas abrir una conexión TCP independiente o hacer pipelining chapucero que casi ningún proxy soporta bien. Te topas de frente con el bloqueo en cabeza de línea (head-of-line blocking) a nivel de aplicación.
gRPC ataca ambos cuellos de botella:
- Protocol Buffers (Protobuf): Serialización binaria estricta con codificación varint. Los nombres de campo no viajan por el cable; viajan índices numéricos de 1 a 4 bytes.
- HTTP/2: Multiplexación real de tramas sobre una única conexión TCP persistente, compresión de cabeceras con HPACK y soporte nativo para streaming bidireccional.
Petición REST/JSON:
[TCP Handshake] -> [TLS] -> POST /pedidos HTTP/1.1 {"usuario_id": 4821, "monto": 89.5} (Texto plano)
Petición gRPC:
[Conexión TCP única persistente]
└─ Stream 1: [0x08 0xa5 0x25 0x15 0x00 0x00 0xb3 0x42] (Binario Protobuf ~8 bytes)
└─ Stream 2: Tráfico concurrente sin esperar respuesta del Stream 1
El contrato antes que el código: Definiendo el .proto
En gRPC no hay ambigüedad sobre qué parámetros acepta un servicio ni si un campo es entero o flotante. Primero escribes el contrato en un archivo de interfaz (IDL) y luego el compilador (protoc) genera los esqueletos para Go, Python, Java o C++.
Un archivo de definición básico para procesar telemetría de pedidos:
syntax = "proto3";
package telemetria;
service TelemetriaService {
// Llamada unaria clásica (petición/respuesta)
rpc RegistrarEvento (EventoRequest) returns (EventoResponse);
// Streaming desde el cliente para ingesta masiva de métricas
rpc TransmitirLote (stream EventoRequest) returns (LoteSummary);
}
message EventoRequest {
int64 timestamp = 1;
string servicio_origen = 2;
string evento_tipo = 3;
bytes payload = 4;
}
message EventoResponse {
bool recibido = 1;
string mensaje_id = 2;
}
message LoteSummary {
int32 total_procesados = 1;
int64 tiempo_ms = 2;
}
Al compilar este fichero con las herramientas de gRPC para Python:
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. telemetria.proto
Obtienes dos ficheros: telemetria_pb2.py (las clases de datos) y telemetria_pb2_grpc.py (el servidor stub y el cliente).
Implementar el servidor requiere únicamente heredar de la clase base autogenerada:
import grpc
from concurrent import futures
import time
import telemetria_pb2
import telemetria_pb2_grpc
class TelemetriaServicer(telemetria_pb2_grpc.TelemetriaServiceServicer):
def RegistrarEvento(self, request, context):
# Procesar datos binarios recibidos directamente sin parsear strings
msg_id = f"{request.servicio_origen}_{request.timestamp}"
return telemetria_pb2.EventoResponse(recibido=True, mensaje_id=msg_id)
def TransmitirLote(self, request_iterator, context):
contador = 0
inicio = time.time()
for evento in request_iterator:
contador += 1
duracion = int((time.time() - inicio) * 1000)
return telemetria_pb2.LoteSummary(total_procesados=contador, tiempo_ms=duracion)
def serve():
servidor = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
telemetria_pb2_grpc.add_TelemetriaServiceServicer_to_server(TelemetriaServicer(), servidor)
servidor.add_insecure_port('[::]:50051')
servidor.start()
servidor.wait_for_termination()
if __name__ == '__main__':
serve()
La ganancia en throughput frente a un endpoint Flask o Express parseando JSON ronda entre un 40% y un 70% según el tamaño de la carga útil, y el uso de memoria en el proceso cae en picado al evitar alocaciones continuas de cadenas intermedias.
Los problemas reales al pisar producción
Adoptar gRPC tiene contrapartidas muy serias que las diapositivas de marketing suelen omitir:
1. El fin del curl y la depuración visual
En REST abres una terminal, lanzas un curl -v -X POST http://servicio/api y ves el JSON devuelto en texto claro. En gRPC recibes un flujo binario ininteligible. Para inspeccionar tráfico necesitas instalar herramientas como grpcurl o configurar disectores en Wireshark cargando los esquemas .proto. Si el equipo no tiene disciplina de observabilidad, depurar en caliente es un suplicio.
2. El infierno del balanceo de carga en capa 4
Este es el fallo clásico de quien despliega gRPC por primera vez detrás de un balanceador tradicional (como un AWS ELB clásico).
Al usar HTTP/2, el cliente abre una única conexión TCP persistente y canaliza todas las peticiones por ahí. Un balanceador L4 distribuye conexiones TCP, no peticiones HTTP. El resultado: el cliente conecta con un único pod o contenedor del backend, y todas las millones de peticiones siguientes van a la misma máquina mientras los otros nueve servidores permanecen ociosos. Para balancear gRPC necesitas proxies L7 conscientes de HTTP/2 (como Envoy o Nginx moderno) o implementar balanceo del lado del cliente (client-side load balancing).
3. La barrera del navegador web
Los navegadores no permiten controlar directamente tramas HTTP/2 arbitrarias ni cabeceras de bajo nivel requeridas por la especificación gRPC. Si quieres hablar con el frontend, necesitas una capa de traducción intermedia (gRPC-Web) o seguir usando REST hacia fuera y gRPC estrictamente en la red interna (la malla de servicios).
Dónde encaja cada pieza
Si tu aplicación es un monolito con dos servicios satélite, meter gRPC añade una complejidad operacional injustificada, como ya comenté al analizar los riesgos de saltar de monolitos a microservicios.
gRPC tiene sentido cuando la topología de red se densifica: decenas de microservicios hablando entre sí, infraestructuras que necesitan ingesta continua de eventos similar a lo que manejamos con Apache Kafka, o comunicaciones inter-datacenter donde el ancho de banda y la latencia son factores de facturación directa. En ese terreno, cambiar texto por contratos binarios estricto deja de ser un capricho técnico y pasa a ser pura necesidad operativa.