Microservice Communication Patterns
Comparing synchronous vs asynchronous communication patterns for microservices.
The most consequential architectural decision in a microservice system isn’t which language to use or how to deploy — it’s how services communicate. Get this wrong, and you’ll spend months untangling cascading failures and debugging timeout chains.
After running 30+ services in production for two years, here’s our decision framework for choosing between synchronous and asynchronous communication.
Synchronous ←────────────────────────────────→ Asynchronous REST gRPC Message Queue Event Streaming (HTTP/JSON) (Protobuf) (RabbitMQ/SQS) (Kafka/PubSub) High latency Low latency Decoupled Fully decoupled Tight coupling Tight coupling Loose coupling No couplingUse synchronous communication when the caller needs a response to continue.
REST is the default choice for external-facing APIs and service-to-service calls where developer experience matters more than raw performance.
# Service A calls Service B via RESTasync def get_user_orders(user_id: str) -> list[Order]: async with httpx.AsyncClient() as client: response = await client.get( f"{ORDER_SERVICE_URL}/api/v1/users/{user_id}/orders", timeout=5.0, ) response.raise_for_status() return [Order.from_dict(o) for o in response.json()]When to use REST:
- External APIs consumed by third parties
- Services with different technology stacks
- When human-readable debugging matters
- CRUD-style operations
When to avoid REST:
- High-throughput internal communication (overhead is significant)
- Strict latency requirements (JSON parsing adds 1-5ms)
- Strongly-typed contracts (JSON Schema is no substitute for Protobuf)
gRPC is our default for internal service-to-service communication. The Protobuf contract enforces type safety, and HTTP/2 multiplexing handles high concurrency efficiently.
service OrderService { rpc GetUserOrders(GetUserOrdersRequest) returns (GetUserOrdersResponse); rpc StreamOrderUpdates(StreamOrderUpdatesRequest) returns (stream OrderUpdate);}
message GetUserOrdersRequest { string user_id = 1; int32 page_size = 2;}
message Order { string id = 1; OrderStatus status = 2; int64 total_cents = 3;}gRPC advantages in production:
- 5-10x lower latency than REST for internal calls
- Automatic code generation in 12+ languages
- Built-in streaming (unary, server, client, bidirectional)
- Protocol buffers are 3-10x smaller than JSON
gRPC gotchas:
- Browser support requires grpc-web (adds complexity)
- Debugging requires special tools (not curl-friendly)
- Load balancing needs L7-aware proxies (Envoy, Linkerd)
Use asynchronous communication when the caller doesn’t need an immediate response or when you need to decouple service lifecycles.
Message queues are ideal for task distribution — one producer, one consumer per message.
import pika
def publish_payment_request(payment: PaymentRequest): connection = pika.BlockingConnection( pika.ConnectionParameters('rabbitmq') ) channel = connection.channel() channel.queue_declare(queue='payment.processing', durable=True)
channel.basic_publish( exchange='', routing_key='payment.processing', body=payment.to_json(), properties=pika.BasicProperties( delivery_mode=2, # Persistent content_type='application/json', ) ) connection.close()When to use message queues:
- Background job processing (email sending, report generation)
- Work distribution with guaranteed delivery
- Rate limiting (queue acts as a buffer)
- When exactly-once processing matters
Event streaming is for event distribution — one producer, many consumers, with replay capability.
from kafka import KafkaProducerimport json
producer = KafkaProducer( bootstrap_servers=['kafka-1:9092', 'kafka-2:9092'], value_serializer=lambda v: json.dumps(v).encode('utf-8'), key_serializer=lambda k: k.encode('utf-8'),)
# Publish order event — all interested services can consumeproducer.send( 'orders.events', key='ord_123', # Same key = same partition = ordering value={ 'event_type': 'order.created', 'order_id': 'ord_123', 'customer_id': 'cust_456', 'total_cents': 9999, 'timestamp': '2026-04-15T10:30:00Z', })producer.flush()When to use event streaming:
- Multiple services need to react to the same event
- Event replay is valuable (new service onboarding, debugging)
- You need ordered processing per entity
- Building an event-driven architecture
| Scenario | Pattern | Reasoning |
|---|---|---|
| External API | REST | Universal, human-readable |
| Internal service call (needs response) | gRPC | Low latency, type safety |
| Background job | Message Queue | Guaranteed delivery, work distribution |
| Multi-service event notification | Event Streaming | Fan-out, replay, ordering |
| Real-time data feed | Event Streaming | High throughput, partitioning |
| Simple notification | Message Queue | Fire-and-forget, no replay needed |
In production, you’ll use all of these. Our order processing flow looks like:
Client → REST → API Gateway → gRPC → Order Service → Kafka → [ → Notification Service (email/SMS) → Inventory Service (stock reservation) → Analytics Service (event tracking) → Fraud Service (risk scoring)]The key insight: synchronous for the critical path, asynchronous for everything else. The order creation itself is synchronous (client needs confirmation), but downstream effects are asynchronous (notifications, analytics, fraud checks).
- Default to async — if the caller doesn’t need the response immediately, don’t make them wait
- Add timeouts everywhere — synchronous calls without timeouts will cascade failures
- Idempotency is mandatory — both sync retries and async redelivery will cause duplicates
- Monitor queue depth — growing queues are the canary in the coal mine for downstream failures
- Don’t mix patterns casually — a service that accepts both REST and Kafka messages for the same operation will confuse everyone
Questions about microservice communication? Find me on GitHub or Twitter.
Related Posts
System Design: Real-Time Payment Processing at Scale
A deep dive into the architecture behind processing millions of payment transactions per day with sub-second latency and 99.99% availability.
Event Sourcing & CQRS Pattern
Deep dive into event sourcing and CQRS patterns for building scalable systems.
Monolith to Event-Driven Architecture
Step-by-step guide to migrating from a monolith to an event-driven architecture.