You spent weeks building a beautiful distributed Actor system. Actors communicate, route messages, sync files, and even distribute tasks. The code runs smoothly in local testing—everything feels perfect.

Then Monday morning hits, and production throws you a “surprise”—a worker node drops offline due to network jitter.

The result? Dozens of tasks stuck in limbo, clients waiting indefinitely, logs flooded with timeout errors. Your boss storms over: “Why is this system so fragile?”

Don’t panic. It’s not that your system is weak—the real world is just this brutal.

Murphy’s Law: What Can Go Wrong Will Go Wrong

In distributed systems, failures aren’t a matter of “if”—they’re a matter of “when.” It’s like driving: you can’t assume no one will ever cut you off; you need to be ready to brake at all times.

Common failures include:

Worker node crashes - Process killed, out of memory, host suddenly powers off

Network issues - Latency spikes to seconds, packet loss soars, entire data centers go dark

Task execution timeouts - A node gets stuck and never returns results

Message loss - Messages in transit when WebSocket disconnects and reconnects

Facing these scenarios, your system needs to be like an experienced driver—knowing when to brake, when to change lanes, when to retry.

Today, we’re going to install this “active safety system” for your infrastructure.

What We’re Building

Simply put, we want the system to learn these tricks:

Retry queues - Automatically retry failed tasks, like a courier reattempting delivery

Failure detection - Quickly identify which nodes are down, stop assigning them work

Automatic cleanup - Remove dead nodes from the cluster, don’t let them take up space

Supervisor mechanism - Set up a “monitor” that watches for problematic tasks

Master these, and your system can behave like those unkillable services that recover automatically even when things go wrong.

Step 1: Build a Retry Queue

First, let’s create a “pending retry task list.” Like a courier company’s “failed delivery log,” it tracks which tasks haven’t completed, who they were assigned to, and how many times they’ve been retried.

use std::collections::HashMap;
use std::time::{Instant, Duration};
use tokio::sync::Mutex;
use std::sync::Arc;

type JobId = String;

#[derive(Clone)]
struct PendingJob {
    job: Job,                // The task itself
    assigned_to: String,     // Who it's assigned to
    sent_at: Instant,        // When it was sent
    retries: u32,            // How many retries so far
}

type RetryQueue = Arc<Mutex<HashMap<JobId, PendingJob>>>;

This structure is simple—just a HashMap storing all “in-flight” tasks. Each task carries a timestamp and retry count, making it easy to decide whether to resend.

Step 2: Monitor and Retry Failed Tasks

Next, we need a background task that periodically checks this list to see if any tasks have timed out and need retry. Like a courier station taking inventory each evening of “packages that didn’t go out today”:

async fn retry_loop(retry_queue: RetryQueue, cluster: ClusterRouter) {
    loop {
        {
            let mut q = retry_queue.lock().await;
            let now = Instant::now();

            for (job_id, pending) in q.clone() {
                // Over 5 seconds with no result? Something's wrong
                if now.duration_since(pending.sent_at) > Duration::from_secs(5) {
                    // Already tried 3 times? Give up
                    if pending.retries >= 3 {
                        println!("Task {} failed after 3 retries, giving up", job_id);
                        q.remove(&job_id);
                        continue;
                    }

                    println!("Task {} timed out, retrying attempt {}", job_id, pending.retries + 1);

                    // Resend the task
                    let payload = serde_json::to_string(&pending.job).unwrap();
                    cluster.send(
                        &format!("{}@{}", "worker", pending.assigned_to),
                        &payload
                    ).await;

                    // Update retry record
                    q.insert(
                        job_id.clone(),
                        PendingJob {
                            retries: pending.retries + 1,
                            sent_at: Instant::now(),
                            ..pending
                        },
                    );
                }
            }
        }
        // Check every 2 seconds
        tokio::time::sleep(Duration::from_secs(2)).await;
    }
}

This loop is straightforward: every two seconds, wake up and check which tasks have been out for more than 5 seconds with no response, then resend. If a task has been tried 3 times and still fails, it’s truly hopeless—mark it as failed.

It’s like a courier: first attempt fails, try again; second attempt fails, try once more; third attempt fails, just return the package.

Step 3: Cleanup on Task Success

When a task completes and returns a result, we need to remove it from the “pending retry list” to prevent unnecessary retries:

// When receiving a JobResult message
retry_queue.lock().await.remove(&msg.id);

This one line is simple but crucial. Like marking a package “delivered” after signature—it won’t be dispatched again next time.

Step 4: Detect Dead Nodes

Retrying tasks isn’t enough. If a node is completely down, no amount of retrying will help—better to kick it out and stop wasting time.

We need a “health check” mechanism that periodically checks which nodes have disconnected:

async fn health_check(cluster_map: ClusterMap, cluster: ClusterClient) {
    loop {
        let peers = cluster.peers.read().unwrap().clone();

        for (id, conn) in peers.iter() {
            // Check if connection is still alive
            if conn.is_disconnected().await {
                println!("Node {} is offline, removing from cluster", id);

                // Remove from cluster map
                cluster_map.write().unwrap().remove(id);

                // Remove from connection pool
                cluster.peers.write().unwrap().remove(id);
            }
        }

        // Check every 10 seconds
        tokio::time::sleep(Duration::from_secs(10)).await;
    }
}

This is like taking attendance at a morning meeting—notice someone’s been absent for days, cross them off the roster. This way, when assigning tasks later, you won’t assign to these “missing persons.”

A more refined approach is to add a “last active time” or “heartbeat timestamp” to each node for more accurate judgment.

Step 5: Set Up a Supervisor Actor

In real production environments, when tasks fail, you might need to do many things: send alert emails, log events, trigger backup processes, even auto-restart certain services.

If all this logic goes into the retry loop, the code becomes a mess. A better approach is to use a dedicated “Supervisor Actor” to handle it:

struct Supervisor {
    retry_queue: RetryQueue,
}

#[async_trait::async_trait]
impl Actor for Supervisor {
    type Message = String;

    async fn handle(&mut self, msg: String) {
        println!("Supervisor received failure report: {}", msg);

        // You can do many things here:
        // - Send alerts to Slack or email
        // - Log to monitoring systems
        // - Trigger backup recovery processes
        // - Automatically adjust resource allocation
    }
}

When a task fails after 3 retries, don’t just print logs—notify this Supervisor Actor. It can take different actions based on the failure type.

Like having a quality inspector on a production line who, when noticing a step repeatedly failing, doesn’t just discard defects but analyzes causes, adjusts processes, even halts the entire line.

Summary of the Complete Mechanism

Now our system has these protections:

MechanismPurpose
RetryQueueRecords all in-flight tasks and retry states
retry_loopAutomatically retries timed-out or failed tasks
health_checkDetects and removes offline nodes
Supervisor ActorReceives failure events and takes action
JobResult cleanupMarks tasks complete, avoids duplicate processing

This mechanism is like equipping your system with “airbags, ABS brakes, lane departure warnings”—active safety features. You may not notice them normally, but they save lives at critical moments.

Further Optimization Ideas

The code above is functional, but if you want to be more professional, consider these enhancements:

Exponential backoff strategy - First retry waits 1 second, second waits 2, third waits 4, avoiding “avalanche retries”

Metrics collection - Track retry counts, failure rates, average latency using tools like Prometheus

Circuit breaker pattern - After consecutive failures from a node, temporarily stop assigning it tasks, give it breathing room

Alert notifications - When failures exceed thresholds, auto-send emails or call on-call engineers

Persistent retry queue - Store the retry queue in Redis or disk, so even if the main node restarts, tasks aren’t lost

Complete supervisor tree - Reference Erlang/OTP’s Supervisor pattern, build multi-level failure monitoring and recovery

Dynamic retry strategy adjustment - Automatically adjust timeout periods and retry counts based on current system load and network conditions

You Did It

At this point, your distributed Actor system is quite complete:

  • Local Actor communication
  • WebSocket-based cross-node communication
  • Cluster routing and message forwarding
  • Gossip-based node discovery
  • Command and control layer
  • File synchronization and hot reloads
  • Distributed task queues
  • Fault tolerance and self-healing

This isn’t just a learning project—it’s a foundation you can use for real work. You can build:

Game servers - Players jump between zones, state auto-syncs

Chat systems - Messages route across nodes, offline messages auto-retry

AI Agent clusters - Multiple AI nodes collaborate on tasks, automatically transfer when one goes down

Robot swarms - Multiple robots coordinate actions, tolerate individual robot disconnections

Edge computing platforms - Distribute computing tasks to edge nodes, auto-retry on network jitter

The core logic for these scenarios is now in your hands. What remains is just tuning and optimizing for specific needs.

Systems always fail, but a good system knows how to handle failures gracefully. That’s the true meaning of “high availability”—not never having problems, but surviving when problems occur.

Now, your system has that capability.


If this article helped you, feel free to follow “Dream Beast Programming” for more practical insights on Rust and distributed systems. Questions and discussions are always welcome!