🦊 Foxtive Worker
A production-ready background worker framework for Rust.
Process messages from RabbitMQ, Redis Streams, or any queue system with confidence. Built-in retries, circuit breakers, dead letter queues, and observability—so you can focus on your business logic.
Table of Contents Table of Contents
- Quick Start - Get running in 5 minutes
- User Guide - Step-by-step learning path
- Examples - Real-world use cases
- Configuration Reference
- Resilient Backends - Survive network failures
- Troubleshooting
- Architecture
Quick Start
Get a worker processing messages in under 5 minutes.
1. Add to Cargo.toml
[]
= { = "0.1", = ["rabbitmq"] }
= { = "1", = ["full"] }
= "0.1"
= "1.0"
2. Create a Worker
use ;
use WorkerResult;
use async_trait;
;
3. Run It
use ;
use Arc;
async
That's it! You've got a working message processor. Now let's make it production-ready.
User Guide
Follow this step-by-step guide to go from zero to production.
1. Your First Worker
Let's build something real—an email notification service.
The Problem
You have a queue of emails to send. Each message looks like:
The Solution
use ;
use WorkerResult;
use async_trait;
;
async
Key concepts:
message.ack()- Tells the broker "I processed this successfully"- Return
Err(...)- Tells the broker "This failed, handle it" - Always validate your input—bad messages happen
Try It
async
2. Adding Reliability
Your first worker works, but what happens when:
- The SMTP server is down?
- A message is malformed?
- Your worker crashes?
Let's add safety nets.
Automatic Retries
Messages fail sometimes. Retry them with exponential backoff:
use RetryHandler;
let retry_handler = default
.with_max_retries // Try 3 times total
.with_initial_backoff // Wait 1s after first failure
.with_max_backoff; // Never wait more than 60s
let pool = new
.add_worker
.with_middleware
.build
.unwrap;
What happens:
- First attempt fails → wait 1 second
- Second attempt fails → wait 2 seconds
- Third attempt fails → wait 4 seconds
- All retries exhausted → send to dead letter queue (if configured)
The backoff doubles each time (exponential) with random jitter to prevent thundering herds.
Dead Letter Queues
When retries are exhausted, don't lose the message—save it for later investigation:
use DeadLetterQueueBackend;
// Create a DLQ backend (in production, use Redis or file-based)
let dlq = new;
let retry_handler = default
.with_max_retries
.with_dead_letter_queue;
// Later, inspect failed messages
let failed_messages = dlq.get_failed_messages.await;
for msg in failed_messages
Circuit Breaker
If your SMTP server is down, stop hammering it:
use CircuitBreakerMiddleware;
let circuit_breaker = new;
let pool = new
.add_worker
.with_middleware
.with_middleware
.build
.unwrap;
How it works:
- Closed (normal): Messages flow through
- Open (after 5 failures): Reject immediately, fail fast
- Half-Open (after 30s): Allow one test message through
- Success → Close circuit
- Failure → Reopen circuit
Tracing
See what's happening in production:
use TracingMiddleware;
use tracing_subscriber;
// Initialize tracing
init;
let pool = new
.add_worker
.with_middleware
.with_middleware
.build
.unwrap;
Now you get structured logs:
INFO Message msg-123 received from email-queue
DEBUG Processing message msg-123 (attempt 1/3)
INFO Sending Welcome! to alice@example.com
DEBUG Message msg-123 processed successfully
3. Scaling Up
One worker isn't enough. Let's scale.
Multiple Workers
Process messages in parallel:
let pool = new
.with_strategy
.add_workers
.build
.unwrap;
Load balancing strategies:
RoundRobin- Distribute evenly (worker 1, 2, 3, 1, 2, 3...)Random- Pick randomly (good enough for most cases)LeastLoaded- Send to worker with fewest active tasks (best for variable workloads)
Concurrency Control
Limit how many messages process simultaneously:
let pool = new
.with_concurrency_limit // Max 50 concurrent messages
.add_worker
.build
.unwrap;
Why limit concurrency?
- Prevent overwhelming downstream services (SMTP servers have rate limits)
- Control memory usage
- Avoid connection pool exhaustion
How to choose the right number:
- CPU-bound tasks: Number of CPU cores
- I/O-bound (HTTP, DB): 50-200
- Mixed workloads: Start at 50, monitor and adjust
Connect to RabbitMQ
Time to use a real message broker:
use RabbitMqBackend;
let config = RabbitMqConsumerConfig ;
let backend = new;
let pool = new
.add_worker
.with_middleware
.build
.unwrap;
// Main loop
loop
Prefetch count matters:
- Low (1-10): Strict ordering, slower
- Medium (10-100): Good balance
- High (100+): Maximum throughput, may reorder
4. Production Ready
Final touches before deploying.
Graceful Shutdown
Handle SIGTERM properly so you don't lose in-flight messages:
use signal;
async
Health Checks
Expose health status for Kubernetes/orchestration:
use HealthEndpoint;
use ;
let health = new;
let app = new
.route;
bind
.serve
.await?;
Returns:
Metrics
Track performance in production:
= { = "0.1", = ["metrics"] }
= "0.12"
use PrometheusBuilder;
// Export metrics to Prometheus
new
.install
.unwrap;
// Now your pool automatically tracks:
// - foxtive_worker_messages_received_total
// - foxtive_worker_messages_processed_total
// - foxtive_worker_message_processing_duration_seconds
// - foxtive_worker_active_workers
Scrape at http://localhost:9090/metrics.
Rate Limiting
Don't overwhelm external services:
use RateLimitMiddleware;
// 100 messages per second, burst of 10
let rate_limiter = new;
let pool = new
.add_worker
.with_middleware
.build
.unwrap;
Powered by the governor crate—efficient, distributed-ready rate limiting.
Examples
Complete, copy-paste ready examples for common scenarios.
Payment Processing
Process payments with timeouts and circuit breakers:
;
let pool = new
.with_concurrency_limit // Conservative—payments are critical
.add_worker
.with_middleware
.with_middleware
.build?;
Batch Database Updates
Process updates in batches for efficiency:
use ;
use BatchMiddleware;
;
let config = default
.with_batch_size // Process 100 at a time
.with_flush_interval; // Or every 5 seconds
let batch_middleware = new;
let pool = new
.add_worker
.with_middleware
.build?;
Image Processing Pipeline
Chain multiple workers together:
// Worker 1: Download image
;
// Worker 2: Resize image
;
// Worker 3: Upload to CDN
;
// Separate pools for each stage
let download_pool = new
.with_concurrency_limit // Network-bound
.add_worker
.build?;
let resize_pool = new
.with_concurrency_limit // CPU-bound
.add_worker
.build?;
let upload_pool = new
.with_concurrency_limit // Network-bound
.add_worker
.build?;
Each worker publishes to the next queue in the pipeline.
Configuration Reference
RetryHandler
default
.with_max_retries // Total attempts
.with_initial_backoff // First retry delay
.with_max_backoff // Maximum delay
.with_backoff_multiplier // Exponential factor
.with_jitter // Add randomness
.with_dead_letter_queue // Where to send exhausted messages
.with_poison_pill_tracker // Detect always-failing messages
RabbitMQ Backend
RabbitMqConsumerConfig
Redis Streams Backend
RedisStreamConsumerConfig
Making Backends "Refuse to Die"
Network failures happen. Brokers restart. Kubernetes reschedules pods. Your workers should survive all of this.
The Problem
By default, if your RabbitMQ or Redis connection drops:
receive()returns an error- Your worker stops processing
- You need manual intervention to restart
The Solution: ResilientBackend
Wrap any backend in ResilientBackend and it will automatically retry forever with exponential backoff:
use ;
use Arc;
use Duration;
async
How It Works
- Detects failures - Catches any error from the wrapped backend
- Retries indefinitely - Never gives up (unless you explicitly shutdown)
- Exponential backoff - Starts fast (1s), slows down (max 60s)
- Jitter - Adds randomness to prevent thundering herd
- Observable - Track connection state and retry attempts
Custom Reconnection Strategies
Want more control? Configure your own strategy:
// Fast retries for critical systems
let aggressive = Exponential ;
let resilient = new
.with_strategy
.build;
Or use fixed delays (good for testing):
let fixed = Fixed;
let resilient = with_strategy;
Monitoring Connection Health
Track when things go wrong:
let resilient = new;
// Spawn a monitor task
spawn;
When to Use ResilientBackend
Use it when:
- Running in production (network failures are inevitable)
- Deployed on Kubernetes (pods get rescheduled)
- Using managed services (RDS, CloudAMQP, etc.)
- You want "set it and forget it" reliability
Skip it when:
- Testing locally (errors help you debug faster)
- You need immediate failure notification
- Building CLI tools (users expect fast feedback)
Real-World Example
Here's how to make your email worker truly bulletproof:
use ;
use Arc;
async
Now your worker will:
- Survive RabbitMQ restarts
- Handle temporary network partitions
- Retry failed messages with backoff
- Move poison pills to DLQ after 5 attempts
- Keep running for months without intervention
That's production-ready.
Load Balancing Strategies
| Strategy | Best For | Notes |
|---|---|---|
| RoundRobin | Uniform workloads | Even distribution |
| Random | Simple setups | Good enough usually |
| LeastLoaded | Variable workloads | Smartest routing |
Troubleshooting
Messages aren't being processed
Check:
-
Is your worker actually receiving messages?
init; // Enable logs -
Does the queue/stream name match what you're publishing to?
-
Are there permission issues on the queue?
-
Is your worker crashing silently? Wrap in try-catch:
async
Messages retrying forever
You have a poison pill (malformed message). Fix:
let retry_handler = default
.with_max_retries // Don't retry forever!
.with_dead_letter_queue;
Then inspect the DLQ to see what's failing.
High memory usage
Solutions:
-
Reduce concurrency:
.with_concurrency_limit // Instead of 500 -
Lower prefetch count (RabbitMQ):
prefetch_count: 10 // Instead of 100 -
Check for memory leaks in your worker code
Slow processing
Debug steps:
- Profile your worker—the bottleneck is usually your code, not the framework
- Increase concurrency if I/O-bound:
.with_concurrency_limit - Use batch processing for database operations
- Check network latency to your broker
Worker keeps crashing
Use Foxtive Supervisor for automatic restarts:
use Supervisor;
let supervisor = new
.add_child
.start
.await?;
Architecture
How it all fits together:
Message Broker (RabbitMQ/Redis)
│
▼
MessageBackend ← Abstracts broker-specific code
│
▼
WorkerPool ← Load balances across workers
│
├──► Middleware Chain ← Retry, circuit breaker, tracing
│ │
│ ▼
└──► Worker ← Your business logic
│
▼
Ack/Nack ← Manual acknowledgment
Design Decisions
Manual ack/nack
- You control when messages are acknowledged
- No accidental message loss
- Explicit error handling
Middleware pipeline
- Composable, reusable components
- Add/remove functionality without touching worker code
- Clean separation of concerns
Trait-based backends
- Swap RabbitMQ for Redis without changing worker logic
- Easy to add new brokers (Kafka, NATS, etc.)
Semaphore-based concurrency
- Precise control over parallelism
- Prevents resource exhaustion
- Works across all workers in the pool
Spawn-per-message
- Each message gets its own Tokio task
- Simple mental model
- Leverages Tokio's work-stealing scheduler
Contributing
Contributions welcome! Guidelines:
- Run tests:
cargo test - Format code:
cargo fmt - Clippy clean:
cargo clippy -- -D warnings - Update docs: If you change public API, update docs
Open an issue first for big changes. Let's discuss before you spend weeks on a PR.
License
MIT License - do whatever you want with this.
Credits
Built with:
- Tokio - Async runtime
- lapin - RabbitMQ client
- redis - Redis client
- governor - Rate limiting
- tracing - Structured logging
Inspired by Celery, Sidekiq, and Bull—but faster because Rust.
Need help? Open an issue on GitHub. Found a bug? Same thing. Want to chat? Also an issue.
Happy coding!
🦊