Loading learning content...
At the heart of every distributed system lies a deceptively simple question: How do processes that don't share memory communicate with each other?
In a single-machine operating system, processes can share data through shared memory segments, communicate via pipes, or signal each other using kernel-mediated mechanisms. But the moment we cross machine boundaries—the moment we enter the realm of distributed systems—these conveniences vanish. There is no shared memory between machines. There is no kernel that spans multiple hosts. There are only messages.
Message passing is not merely one communication option among many—it is the foundational primitive upon which all distributed communication is built. Whether you're making an HTTP request, invoking a remote procedure, publishing to a message queue, or streaming data in real-time, at the fundamental level, you're sending messages across a network.
By the end of this page, you will understand the foundations of message passing communication: the distinction between synchronous and asynchronous models, message delivery semantics, ordering guarantees, and how these primitives are implemented in real operating systems. You'll see why every distributed protocol, from TCP to gRPC, ultimately reduces to message passing.
Message passing represents a fundamental shift from the shared-memory model of communication that dominates single-machine programming. Understanding this paradigm shift is essential for developing correct, efficient, and robust distributed systems.
The Core Abstraction
In the message passing model, processes communicate by explicitly sending and receiving discrete units of data called messages. The fundamental operations are remarkably simple:
Despite this apparent simplicity, the semantics of these operations hide extraordinary complexity. What happens if the network fails mid-transmission? What if the receiver isn't ready? What if messages arrive out of order? These questions define the challenges of distributed systems.
| Characteristic | Shared Memory | Message Passing |
|---|---|---|
| Communication mechanism | Read/write to shared address space | Explicit send/receive operations |
| Coupling | Tight (processes share state) | Loose (processes share nothing) |
| Synchronization | Implicit (via memory barriers, locks) | Explicit (message exchange patterns) |
| Failure model | Process crash affects shared state | Independent failures, message loss possible |
| Scalability | Limited by shared memory architecture | Scales across network boundaries |
| Latency | Nanoseconds (cache/memory access) | Microseconds to milliseconds (network) |
| Complexity | Race conditions, memory visibility | Partial failures, network partitions |
Every distributed communication mechanism—from raw sockets to high-level RPC frameworks—is built upon message passing. Understanding this foundation allows you to reason about any distributed protocol, debug communication failures, and design systems that gracefully handle the inherent challenges of distributed computing.
The most fundamental design decision in any message passing system is whether communication is synchronous or asynchronous. This choice profoundly impacts system design, performance characteristics, and failure handling.
Synchronous Message Passing
In synchronous (or blocking) communication, the sender is blocked until the receiver has accepted the message. This creates a rendezvous between the communicating processes:
send()receive()Synchronous communication provides implicit synchronization—by the time send() returns, you know the message has been received. This simplifies reasoning about program correctness but introduces performance bottlenecks when communicating parties operate at different speeds.
123456789101112131415161718192021222324252627282930
// Synchronous message passing pseudocode// The sender blocks until receiver accepts the message void sender_process() { message_t msg; prepare_message(&msg, "Request data"); // BLOCKS until receiver calls receive() sync_send(RECEIVER_ID, &msg); // When we reach here, we KNOW receiver got the message printf("Message delivered and received\n");} void receiver_process() { message_t msg; // BLOCKS until sender calls send() sync_receive(SENDER_ID, &msg); // Both parties synchronized at the handoff point process_message(&msg);} // Timeline visualization://// Sender: ----[send]----X-----block------X----[continue]---->// Receiver: --------[block]------X----[receive]----[process]--->// ^// Rendezvous pointAsynchronous Message Passing
In asynchronous (or non-blocking) communication, the sender continues immediately after initiating transmission. Messages are buffered somewhere in the system:
send(), message is copied to a bufferreceive() and gets buffered messageAsynchronous communication enables pipeline parallelism—sender can continue generating messages while previous messages are in flight. However, it introduces complexity: How large should buffers be? What happens when buffers overflow? How do you know a message was successfully delivered?
1234567891011121314151617181920212223242526272829303132333435
// Asynchronous message passing pseudocode// Sender doesn't wait for receiver void sender_process() { message_t msg1, msg2, msg3; // All sends return immediately - messages queued async_send(RECEIVER_ID, &msg1); // Returns immediately async_send(RECEIVER_ID, &msg2); // Returns immediately async_send(RECEIVER_ID, &msg3); // Returns immediately // All three messages now "in flight" // But we don't know if they've been received yet do_other_work(); // Can proceed without waiting} void receiver_process() { message_t msg; // May block if no messages buffered, or return immediately if available async_receive(ANY_SENDER, &msg); process_message(&msg);} // Timeline visualization (3 messages)://// Sender: --[s1]-[s2]-[s3]--[other_work]---->// | | |// v v v (messages in flight)// Network: ===m1===m2===m3==================>// | | |// v v v // Receiver: --------[r1]-[process]-[r2]-[process]-[r3]---->Asynchronous systems must decide: Who buffers the message, and how much? Options include sender-side buffering, receiver-side buffering, or kernel/network buffering. Each choice has implications for memory usage, message delivery guarantees, and system behavior under load. Unbounded buffers consume unlimited memory; bounded buffers can cause senders to block eventually—making them pseudo-synchronous.
In distributed systems, the network can fail in complex ways: messages may be lost, duplicated, reordered, or corrupted. A critical design decision is defining what delivery guarantees the message passing system provides. These semantics profoundly affect both correctness and performance.
The Fundamental Challenge
Consider sending a message over an unreliable network:
What happened? Either:
From the sender's perspective, these are indistinguishable. This fundamental ambiguity underlies the famous Two Generals' Problem and shapes all message delivery guarantees.
| Semantics | Guarantee | Implementation Complexity | Use Cases |
|---|---|---|---|
| At-most-once | Message delivered 0 or 1 time | Lowest - no retries, just send and forget | Metrics, logs, unimportant updates |
| At-least-once | Message delivered 1 or more times | Medium - retry until acknowledged | Idempotent operations, database writes with dedup |
| Exactly-once | Message delivered exactly 1 time | Highest - requires deduplication + transactions | Financial transactions, critical systems |
At-Most-Once Delivery
The simplest semantics: send the message and don't worry about whether it arrives. No retries, no acknowledgments. If the message is lost, it's lost.
Sender: send(msg) → done
Network: might deliver, might drop
Receiver: might receive, might not
This is appropriate for non-critical data where occasional loss is acceptable—streaming metrics, logging, or heartbeats where the next update will carry fresh information anyway.
At-Least-Once Delivery
Retransmit until acknowledged. This ensures delivery but may cause duplicates if the acknowledgment is lost:
Applications using at-least-once delivery must be idempotent—processing the same message multiple times must produce the same result as processing it once.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
// At-least-once delivery with retry logic// Messages may be delivered multiple times typedef struct { uint64_t message_id; // Unique identifier for deduplication char payload[MAX_PAYLOAD];} message_t; int send_at_least_once(int socket, message_t *msg, int max_retries) { int retries = 0; while (retries < max_retries) { // Send the message if (send(socket, msg, sizeof(*msg), 0) < 0) { retries++; continue; } // Wait for acknowledgment with timeout struct timeval timeout = {.tv_sec = 1, .tv_usec = 0}; setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); ack_t ack; ssize_t received = recv(socket, &ack, sizeof(ack), 0); if (received > 0 && ack.message_id == msg->message_id) { return 0; // Success - acknowledged } // Timeout or wrong ack - retry retries++; printf("Retry %d for message %lu\n", retries, msg->message_id); } return -1; // Failed after max retries} // Receiver side - idempotent processing required!void receive_at_least_once(int socket) { // Track seen message IDs to detect duplicates hashset_t seen_messages; message_t msg; while (recv(socket, &msg, sizeof(msg), 0) > 0) { // Check for duplicate if (hashset_contains(&seen_messages, msg.message_id)) { printf("Duplicate message %lu ignored\n", msg.message_id); } else { // First time seeing this message hashset_add(&seen_messages, msg.message_id); process_message(&msg); } // Always send acknowledgment (even for duplicates) ack_t ack = {.message_id = msg.message_id}; send(socket, &ack, sizeof(ack), 0); }}Exactly-Once Delivery
The holy grail of message passing: ensure each message is delivered and processed exactly once. This is fundamentally impossible to achieve using messages alone over an unreliable network (see: Two Generals' Problem), but we can achieve effective exactly-once semantics by combining at-least-once delivery with receiver-side deduplication.
The key insight: exactly-once delivery is really at-least-once delivery + idempotent processing.
Modern stream processing systems like Apache Kafka achieve exactly-once semantics through:
When building distributed systems, assume messages may be delivered multiple times and design your processing logic to be idempotent. This is more robust than trying to guarantee exactly-once delivery at the transport layer. Operations like 'set X = 5' are naturally idempotent; 'increment X by 1' is not (but can be made so with proper message deduplication).
Beyond delivery guarantees, distributed systems must consider ordering—when multiple messages are sent, in what order are they received? The answer depends on the guarantees provided by the communication system.
Why Ordering Matters
Consider a distributed database receiving two updates:
SET balance = 100SET balance = 200If these arrive out of order, the final state is wrong. Similarly, if WRITE data arrives before OPEN file, the operation fails. Many correctness properties depend on message ordering.
FIFO (Point-to-Point) Ordering
The weakest useful guarantee: messages from sender A to receiver B are delivered in the order sent. This says nothing about messages from different senders or to different receivers.
Causal Ordering
Preserves cause-and-effect relationships: if message M1 causally precedes M2 (M1 was sent before M2, and the sender of M2 had seen M1), then M1 is delivered before M2 at all receivers. This is stronger than FIFO and captures intuitive notions of "happened before."
| Ordering Type | Guarantee | Overhead | Implementation |
|---|---|---|---|
| No ordering | Messages may arrive in any order | Lowest | Raw UDP, basic message queues |
| FIFO ordering | Per-sender order preserved | Low | Sequence numbers per channel |
| Causal ordering | Causally related messages ordered | Medium | Vector clocks or similar |
| Total ordering | All messages delivered in same global order | High | Centralized sequencer or consensus |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
// FIFO ordering implementation using sequence numbers// Ensures messages from same sender arrive in order sent typedef struct { uint32_t sender_id; uint32_t sequence_num; // Per-sender sequence number char payload[MAX_PAYLOAD];} ordered_message_t; // Sender maintains per-destination sequence countertypedef struct { uint32_t next_sequence[MAX_DESTINATIONS];} sender_state_t; void fifo_send(sender_state_t *state, int dest_id, const char *payload) { ordered_message_t msg = { .sender_id = MY_ID, .sequence_num = state->next_sequence[dest_id]++, }; strncpy(msg.payload, payload, MAX_PAYLOAD); network_send(dest_id, &msg, sizeof(msg));} // Receiver tracks expected sequence number from each sendertypedef struct { uint32_t expected_sequence[MAX_SENDERS]; // Buffer for out-of-order messages ordered_message_t buffer[MAX_SENDERS][BUFFER_SIZE]; int buffer_count[MAX_SENDERS];} receiver_state_t; void fifo_receive(receiver_state_t *state, ordered_message_t *msg) { uint32_t sender = msg->sender_id; if (msg->sequence_num == state->expected_sequence[sender]) { // In-order message - deliver immediately deliver_message(msg); state->expected_sequence[sender]++; // Check buffer for now-deliverable messages check_buffered_messages(state, sender); } else if (msg->sequence_num > state->expected_sequence[sender]) { // Out-of-order - buffer for later buffer_message(state, sender, msg); } // Ignore if sequence_num < expected (duplicate or old message)} void check_buffered_messages(receiver_state_t *state, uint32_t sender) { // Deliver any buffered messages that are now in sequence while (1) { ordered_message_t *buffered = find_in_buffer( state, sender, state->expected_sequence[sender] ); if (!buffered) break; deliver_message(buffered); remove_from_buffer(state, sender, buffered); state->expected_sequence[sender]++; }}Total Ordering
The strongest guarantee: all receivers see all messages in exactly the same order. This is essential for replicated state machines—if all replicas process the same messages in the same order, they maintain identical state.
Achieving total ordering requires coordination, typically through:
The cost of total ordering is significant: it typically requires network round-trips proportional to the number of participants, limiting throughput and increasing latency.
Strong ordering guarantees (especially total ordering) conflict with availability and partition tolerance. During a network partition, you cannot provide total ordering without blocking—some messages simply cannot be ordered relative to others until the partition heals. This is one manifestation of the CAP theorem: you cannot have strong Consistency, Availability, and Partition tolerance simultaneously.
Operating systems provide the fundamental infrastructure for message passing. Understanding how the OS supports message passing reveals the layers of abstraction between application code and network hardware.
Unix/Linux: The Socket Abstraction
The socket API is the primary message passing interface for network communication on Unix-like systems. Sockets abstract the complexities of network protocols behind a familiar file-descriptor interface:
UDP sockets: Direct mapping to the message passing model. Each sendto()/recvfrom() transfers one discrete message. No ordering or reliability guarantees.
TCP sockets: Stream abstraction built on messages. The kernel handles segmentation, ordering, reliability, and flow control. Applications see a reliable byte stream, but underneath TCP uses message passing with sequence numbers and acknowledgments.
Kernel Message Buffers
The kernel manages buffers for both sending and receiving:
Send buffer: When an application calls send(), data is copied to a kernel buffer. The call returns when data is buffered (not when delivered). The kernel transmits in the background.
Receive buffer: Incoming data is buffered by the kernel until the application calls recv(). If the buffer fills (receiver slower than sender), different behaviors occur depending on protocol—TCP exerts back-pressure; UDP drops packets.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
// UDP socket-based message passing// Each sendto()/recvfrom() is a complete message #include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h> typedef struct { uint32_t type; uint32_t sequence; char payload[1024];} app_message_t; // Initialize UDP socket for message passingint create_message_socket(int port) { int sock = socket(AF_INET, SOCK_DGRAM, 0); // SOCK_DGRAM = UDP if (sock < 0) return -1; struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(port), .sin_addr.s_addr = INADDR_ANY, }; if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { close(sock); return -1; } return sock;} // Send a message to a specific destinationint send_message(int sock, const char *dest_ip, int dest_port, app_message_t *msg) { struct sockaddr_in dest = { .sin_family = AF_INET, .sin_port = htons(dest_port), }; inet_pton(AF_INET, dest_ip, &dest.sin_addr); // sendto() transmits the complete message atomically // Either the whole message goes out, or none of it ssize_t sent = sendto(sock, msg, sizeof(*msg), 0, (struct sockaddr*)&dest, sizeof(dest)); return (sent == sizeof(*msg)) ? 0 : -1;} // Receive a message from any senderint receive_message(int sock, app_message_t *msg, struct sockaddr_in *sender) { socklen_t sender_len = sizeof(*sender); // recvfrom() returns one complete message // Message boundaries are preserved in UDP ssize_t received = recvfrom(sock, msg, sizeof(*msg), 0, (struct sockaddr*)sender, &sender_len); return (received == sizeof(*msg)) ? 0 : -1;} // Example usagevoid example_sender() { int sock = create_message_socket(0); // Ephemeral port app_message_t msg = { .type = MSG_TYPE_REQUEST, .sequence = 1, }; strcpy(msg.payload, "Hello, distributed world!"); send_message(sock, "192.168.1.100", 5000, &msg);}Local Inter-Process Communication
For processes on the same machine, operating systems provide optimized message passing mechanisms:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
// POSIX Message Queue - kernel-managed local message passing// Provides discrete messages with priority #include <mqueue.h>#include <fcntl.h> #define QUEUE_NAME "/example_queue"#define MAX_MSG_SIZE 1024#define MAX_MSGS 10 // Create or open message queuemqd_t create_message_queue() { struct mq_attr attr = { .mq_flags = 0, .mq_maxmsg = MAX_MSGS, // Max messages in queue .mq_msgsize = MAX_MSG_SIZE, // Max message size .mq_curmsgs = 0, }; mqd_t mq = mq_open(QUEUE_NAME, O_CREAT | O_RDWR, 0644, &attr); return mq;} // Send message with priority (0-31, higher = higher priority)int send_with_priority(mqd_t mq, const char *message, unsigned priority) { return mq_send(mq, message, strlen(message) + 1, priority);} // Receive highest-priority messageint receive_priority(mqd_t mq, char *buffer, size_t bufsize, unsigned *priority) { return mq_receive(mq, buffer, bufsize, priority);} // Example: multi-process communicationvoid producer_process() { mqd_t mq = create_message_queue(); // Send messages with different priorities send_with_priority(mq, "Low priority task", 5); send_with_priority(mq, "Critical task", 31); // Delivered first! send_with_priority(mq, "Normal task", 15); mq_close(mq);} void consumer_process() { mqd_t mq = mq_open(QUEUE_NAME, O_RDONLY); char buffer[MAX_MSG_SIZE]; unsigned priority; while (mq_receive(mq, buffer, sizeof(buffer), &priority) != -1) { printf("Received (priority %u): %s\n", priority, buffer); } mq_close(mq); mq_unlink(QUEUE_NAME); // Remove queue}Traditional message passing requires copying data: application → kernel → network. Modern systems employ zero-copy techniques: the kernel directly maps application buffers for DMA (Direct Memory Access) by the network card. Linux's sendfile(), splice(), and io_uring enable this. For high-throughput systems, avoiding copies is essential—a single copy at 100 Gbps costs significant CPU time.
Understanding the theoretical foundations prepares us for real-world application. Let's examine how message passing principles manifest in production systems.
Case Study: Microservices Communication
Modern microservice architectures are fundamentally message passing systems. Services communicate via:
HTTP/REST: Request-response message passing over TCP. Synchronous by nature (client waits for response).
Message queues (Kafka, RabbitMQ): Asynchronous message passing with persistence. Enables decoupling and reliable delivery.
gRPC: Binary message passing with strong typing. Can be synchronous, streaming, or unary.
Each choice reflects trade-offs between the properties we've discussed: synchrony, delivery semantics, and ordering.
| Technology | Sync/Async | Delivery | Ordering | Use Case |
|---|---|---|---|---|
| HTTP/REST | Synchronous | Application-level | Request-response | APIs, web services |
| TCP sockets | Both | At-least-once (ACK) | FIFO per connection | Custom protocols |
| UDP sockets | Asynchronous | At-most-once | None | Streaming, gaming, DNS |
| Apache Kafka | Asynchronous | At-least-once | Per-partition FIFO | Event streaming |
| RabbitMQ | Asynchronous | Configurable | FIFO per queue | Task queues, RPC |
| ZeroMQ | Both | Configurable | Per-socket | High-performance messaging |
Failure Handling Patterns
Real systems must handle the failures inherent in message passing:
Retry with exponential backoff: When a send fails, retry with increasing delays (1s, 2s, 4s, 8s...). Prevents overwhelming a struggling system.
Dead letter queues: Messages that can't be delivered after N retries go to a special queue for manual inspection.
Circuit breakers: After repeated failures, stop trying to send to a destination for a cooling-off period.
Idempotency keys: Include unique identifiers in messages so receivers can deduplicate safely.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
# Resilient message sending with common patterns import timeimport randomfrom functools import wraps class CircuitBreaker: """Prevents overwhelming a failing service""" def __init__(self, failure_threshold=5, reset_timeout=60): self.failure_count = 0 self.failure_threshold = failure_threshold self.reset_timeout = reset_timeout self.last_failure_time = 0 self.state = "closed" # closed = normal, open = blocking def record_failure(self): self.failure_count += 1 self.last_failure_time = time.time() if self.failure_count >= self.failure_threshold: self.state = "open" def record_success(self): self.failure_count = 0 self.state = "closed" def can_proceed(self): if self.state == "closed": return True # Check if reset timeout has elapsed if time.time() - self.last_failure_time > self.reset_timeout: return True # Allow one attempt (half-open state) return False def retry_with_backoff(max_retries=5, base_delay=1.0, max_delay=60.0): """Decorator for retry with exponential backoff""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): last_exception = None for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception as e: last_exception = e if attempt < max_retries - 1: # Exponential backoff with jitter delay = min(base_delay * (2 ** attempt), max_delay) jitter = random.uniform(0, delay * 0.1) time.sleep(delay + jitter) raise last_exception return wrapper return decorator class ResilientMessageSender: """Message sender with retry, circuit breaker, and idempotency""" def __init__(self, connection): self.connection = connection self.circuit_breakers = {} # Per-destination circuit breakers def get_circuit_breaker(self, destination): if destination not in self.circuit_breakers: self.circuit_breakers[destination] = CircuitBreaker() return self.circuit_breakers[destination] @retry_with_backoff(max_retries=5) def send_message(self, destination, message, idempotency_key=None): """Send with retry, circuit breaker, and idempotency support""" cb = self.get_circuit_breaker(destination) if not cb.can_proceed(): raise CircuitOpenError(f"Circuit open for {destination}") try: # Include idempotency key for receiver-side deduplication envelope = { "idempotency_key": idempotency_key or generate_uuid(), "timestamp": time.time(), "payload": message, } result = self.connection.send(destination, envelope) cb.record_success() return result except Exception as e: cb.record_failure() raiseEvery message operation needs a timeout. Without timeouts, a sender waiting for acknowledgment that never comes will wait forever. Set timeouts based on expected latency plus safety margin. Make them configurable—what works in a data center (10ms) fails across continents (100ms+).
Beyond point-to-point communication, many distributed systems require sending the same message to multiple recipients. This is group communication or multicast, and it introduces additional complexity.
Types of Multicast
Best-effort multicast: No delivery guarantees. A message might reach some recipients and not others.
Reliable multicast: All correct (non-crashed) recipients receive the message if any correct recipient receives it.
Atomic multicast (Total order multicast): All messages are delivered in the same order at all recipients. Required for replicated state machines.
The Challenge of Reliable Multicast
Simply sending the same message to each recipient isn't reliable—some might receive it, others might not. Achieving reliability requires coordination:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
# Reliable multicast with acknowledgment tracking from threading import Thread, Lockimport time class ReliableMulticast: """ Implements reliable multicast using sender-based acknowledgment tracking. Guarantees: if any correct recipient receives the message, all correct recipients eventually receive it. """ def __init__(self, group_members, send_func): self.members = group_members self.send = send_func self.pending_acks = {} # message_id -> set of members not yet acked self.lock = Lock() self.max_retries = 5 self.retry_interval = 1.0 # seconds def multicast(self, message): """Send message to all group members reliably""" message_id = generate_unique_id() with self.lock: # Track which members need to acknowledge self.pending_acks[message_id] = set(self.members) # Send to all members in parallel for member in self.members: self._send_to_member(member, message_id, message) # Start background thread to handle retransmissions retransmitter = Thread( target=self._retransmission_loop, args=(message_id, message) ) retransmitter.daemon = True retransmitter.start() return message_id def _send_to_member(self, member, message_id, message): """Send message to a single member""" envelope = { 'message_id': message_id, 'payload': message, 'requires_ack': True, } self.send(member, envelope) def receive_ack(self, member, message_id): """Process acknowledgment from a member""" with self.lock: if message_id in self.pending_acks: self.pending_acks[message_id].discard(member) # All members acknowledged? if not self.pending_acks[message_id]: del self.pending_acks[message_id] return True # Fully acknowledged return False def _retransmission_loop(self, message_id, message): """Retransmit to members who haven't acknowledged""" retries = 0 while retries < self.max_retries: time.sleep(self.retry_interval) with self.lock: if message_id not in self.pending_acks: return # All acknowledged pending = list(self.pending_acks[message_id]) # Retransmit to non-acknowledging members for member in pending: self._send_to_member(member, message_id, message) retries += 1 # Max retries exceeded - log/alert about unreachable members with self.lock: if message_id in self.pending_acks: failed = self.pending_acks[message_id] log_delivery_failure(message_id, list(failed))IP Multicast vs. Application-Level Multicast
IP Multicast uses special network addresses (224.0.0.0 - 239.255.255.255) where routers duplicate packets to all subscribers. It's efficient but:
Application-level multicast builds multicast on top of unicast TCP/UDP connections. Less efficient (sender transmits N times instead of once) but works anywhere and can implement stronger guarantees.
The virtual synchrony model guarantees that all group members see the same sequence of messages and membership changes. If a member crashes, all surviving members agree on which messages the crashed member received. This is crucial for consistent replication and is implemented by systems like Isis2, Spread, and JGroups.
Message passing is the universal primitive underlying all distributed communication. Mastering its foundations enables you to understand, design, and debug any distributed system.
What's Next:
With message passing foundations established, we'll explore Remote Procedure Calls (RPC)—an abstraction that makes distributed communication look like local function calls. You'll see how RPC builds on message passing while hiding much of its complexity behind a familiar programming model.
You now understand the fundamental paradigm of message passing in distributed systems. From synchronous rendezvous to asynchronous queuing, from delivery semantics to ordering guarantees, you have the conceptual foundation to analyze and design distributed communication systems.