Python SDK vs Rust SDK - Worker Feature Comparison
May 7, 2026 · View on GitHub
This document provides a detailed comparison of worker features between the Python Conductor SDK and the Rust Conductor SDK.
Executive Summary
| Category | Python Features | Rust Features | Status |
|---|---|---|---|
| Worker Configuration | 10 properties | 10 properties | 100% EQUAL |
| Hierarchical Config | 3-tier (code/global/specific) | 3-tier (code/global/specific) | 100% EQUAL |
| Polling Loop | Dynamic batch + adaptive backoff | Dynamic batch + adaptive backoff | 100% EQUAL |
| Concurrency Control | Semaphore/ThreadPool | Semaphore | 100% EQUAL |
| Events/Metrics | 7 event types | 7 event types | 100% EQUAL |
| Task Registration | Auto-register with JSON Schema | Auto-register with JSON Schema | 100% EQUAL |
| Task Context | poll_count, task_id, etc. | TaskContext | 100% EQUAL |
| Long-Running Tasks | TaskInProgress | TaskInProgress | 100% EQUAL |
All worker features from the Python SDK are now implemented in Rust.
Worker Configuration Properties
| Property | Python | Rust | Notes |
|---|---|---|---|
poll_interval_millis | ✅ | ✅ | Both support env override |
domain | ✅ | ✅ | Both support env override |
worker_id | ✅ | ✅ | Both auto-generate hostname-pid |
thread_count | ✅ | ✅ | Max concurrent executions |
register_task_def | ✅ | ✅ | Auto-register task on startup |
overwrite_task_def | ✅ | ✅ | Overwrite existing definitions |
strict_schema | ✅ | ✅ | JSON Schema strictness |
poll_timeout | ✅ | ✅ | Poll request timeout |
paused | ✅ | ✅ | Env-only, pause worker |
lease_extend_enabled | ✅ (not impl) | ❌ | Reserved, not implemented in either |
Status: ✅ 10/10 properties implemented
Configuration Hierarchy
Both SDKs support the same 3-tier configuration hierarchy:
| Priority | Python Format | Rust Format |
|---|---|---|
| 1 (Highest) | conductor.worker.<name>.<property> | CONDUCTOR_WORKER_<NAME>_<PROPERTY> |
| 2 | conductor.worker.all.<property> | CONDUCTOR_WORKER_ALL_<PROPERTY> |
| 3 (Lowest) | Code defaults | Code defaults |
Rust also supports dot notation: conductor.worker.all.domain
Status: ✅ EQUAL
Polling Loop Features
| Feature | Python | Rust | Notes |
|---|---|---|---|
| Dynamic batch polling | ✅ | ✅ | Poll up to available_slots |
| Adaptive backoff | ✅ | ✅ | Exponential backoff on empty |
| Capacity tracking | ✅ | ✅ | Running task count |
| Immediate cleanup | ✅ | ✅ | Remove completed tasks |
Python Implementation:
available_slots = thread_count - len(running_tasks)
tasks = batch_poll(available_slots)
Rust Implementation:
let available_slots = config.thread_count.saturating_sub(running_count);
let tasks = task_client.batch_poll(..., available_slots, ...).await?;
Status: ✅ EQUAL
Concurrency Control
| Feature | Python | Rust |
|---|---|---|
| Sync workers | ThreadPoolExecutor | N/A (all async) |
| Async workers | AsyncTaskRunner + Semaphore | Tokio Semaphore |
| thread_count meaning | Threads (sync) / Concurrency limit (async) | Concurrency limit |
Note: Rust SDK is async-native, using Tokio. Python has dual sync/async runners.
Status: ✅ EQUAL (async semantics)
Event System & Metrics
Event Types
| Event | Python | Rust | Description |
|---|---|---|---|
PollStarted | ✅ | ✅ | When batch poll starts |
PollCompleted | ✅ | ✅ | When batch poll succeeds |
PollFailure | ✅ | ✅ | When batch poll fails |
TaskExecutionStarted | ✅ | ✅ | When task execution begins |
TaskExecutionCompleted | ✅ | ✅ | When task completes |
TaskExecutionFailure | ✅ | ✅ | When task fails |
TaskUpdateFailure | ✅ | ✅ | When update fails after retries |
Status: ✅ EQUAL (all 7 events)
Prometheus Metrics
Note: The table below is a historical snapshot from when the Python SDK was emitting its pre-harmonization (legacy) metrics surface. Worker metrics have since been harmonized across all Conductor SDKs under a single canonical catalog. For the complete and current Rust SDK metrics catalog, see METRICS.md.
| Metric | Python | Rust |
|---|---|---|
task_poll_total | ✅ | ✅ |
task_poll_time_seconds | ✅ | ✅ |
task_poll_error_total | ✅ | ✅ |
task_execute_time_seconds | ✅ | ✅ |
task_execute_error_total | ✅ | ✅ |
task_update_error_total | ✅ | ✅ |
task_result_size_bytes | ✅ | ✅ |
task_paused_total | ✅ | ✅ |
active_workers | ✅ | ✅ |
Status: ✅ EQUAL
HTTP Metrics Server
| Feature | Python | Rust |
|---|---|---|
/metrics endpoint | ✅ | ✅ |
/health endpoint | ✅ | ✅ |
| Configurable port | ✅ | ✅ |
Status: ✅ EQUAL
Task Registration & Schema
| Feature | Python | Rust | Notes |
|---|---|---|---|
| Auto-register task def | ✅ | ✅ | Both register on startup |
| JSON Schema from types | ✅ | ✅ | Using schemars crate |
| Input schema generation | ✅ | ✅ | From struct with JsonSchema derive |
| Output schema generation | ✅ | ✅ | From struct with JsonSchema derive |
| Schema registration | ✅ | ✅ | Auto-registers with SchemaClient |
| strict_schema flag | ✅ | ✅ | additionalProperties control |
| overwrite_task_def | ✅ | ✅ | Config supported |
Python Schema Generation:
@worker_task(task_definition_name='process_order', register_task_def=True)
def process_order(order: OrderInfo, priority: int = 1) -> dict:
...
# Generates JSON Schema from type hints automatically
Rust Schema Generation:
use schemars::JsonSchema;
use conductor::schema::generate_schema;
#[derive(JsonSchema)]
struct OrderInput {
order_id: String,
amount: f64,
customer_id: i64,
}
#[derive(JsonSchema)]
struct OrderOutput {
status: String,
processed_at: String,
}
// Option 1: Generate schema and attach to worker
let worker = FnWorker::new("process_order", handler)
.with_input_schema(generate_schema::<OrderInput>(true)) // strict mode
.with_output_schema(generate_schema::<OrderOutput>(true));
// Option 2: Use helper methods
let worker = FnWorker::new("process_order", handler)
.with_input_schema_from::<OrderInput>(true)
.with_output_schema_from::<OrderOutput>(true);
// Schemas are automatically registered when register_task_def=true
Status: ✅ 100% EQUAL
Task Context (poll_count)
| Feature | Python | Rust |
|---|---|---|
get_task_context() | ✅ | ✅ TaskContext::from_task() |
poll_count | ✅ | ✅ |
task_id | ✅ | ✅ |
workflow_instance_id | ✅ | ✅ |
retry_count | ✅ | ✅ |
correlation_id | ✅ | ✅ |
domain | ✅ | ✅ |
Python Usage:
@worker_task(task_definition_name='long_task')
def long_task(job_id: str) -> Union[dict, TaskInProgress]:
ctx = get_task_context()
poll_count = ctx.get_poll_count() # Track iterations
...
Rust Usage:
use conductor::TaskContext;
async fn long_task(task: &Task) -> WorkerOutput {
let ctx = TaskContext::from_task(task);
let poll_count = ctx.poll_count(); // Track iterations
if ctx.is_first_poll() {
// First execution
}
...
}
Status: ✅ EQUAL
Long-Running Tasks (TaskInProgress)
| Feature | Python | Rust |
|---|---|---|
TaskInProgress class | ✅ | ✅ |
callback_after_seconds | ✅ | ✅ |
| Intermediate output | ✅ | ✅ |
| Worker return type | Union[dict, TaskInProgress] | WorkerOutput::InProgress |
Python:
return TaskInProgress(callback_after_seconds=30, output={'progress': 50})
Rust:
return Ok(WorkerOutput::InProgress(
TaskInProgress::new(30).with_output_value("progress", 50)
));
Status: ✅ EQUAL
Worker Definition Patterns
Python: Decorator-based
@worker_task(
task_definition_name='process_order',
poll_interval_millis=1000,
domain='production',
thread_count=10,
register_task_def=True
)
def process_order(order_id: str, amount: float) -> dict:
return {'status': 'processed'}
Rust: Three Options
// Option 1: #[worker] macro (most similar to Python) - requires "macros" feature
use conductor_macros::worker;
#[worker(name = "process_order", poll_interval = 1000, domain = "production", thread_count = 10)]
async fn process_order(order_id: String, amount: f64) -> serde_json::Value {
serde_json::json!({"status": "processed"})
}
// Use the generated function
handler.add_worker(process_order_worker());
// Option 2: Function-based (FnWorker)
let worker = FnWorker::new("process_order", |task| async move {
let order_id: String = task.get_input("order_id").unwrap();
let amount: f64 = task.get_input("amount").unwrap();
Ok(WorkerOutput::completed_with_result(json!({"status": "processed"})))
})
.with_domain("production")
.with_thread_count(10);
// Option 3: Trait implementation (most flexible)
struct ProcessOrderWorker;
#[async_trait]
impl Worker for ProcessOrderWorker {
fn task_definition_name(&self) -> &str { "process_order" }
fn domain(&self) -> Option<&str> { Some("production") }
fn thread_count(&self) -> usize { 10 }
async fn execute(&self, task: &Task) -> Result<WorkerOutput> {
Ok(WorkerOutput::completed_with_result(json!({"status": "processed"})))
}
}
Status: ✅ 100% EQUAL - Rust now has #[worker] macro nearly identical to Python's @worker_task decorator!
Worker Discovery
| Feature | Python | Rust |
|---|---|---|
| Package scanning | ✅ scan_packages() | ❌ (not idiomatic) |
| Module scanning | ✅ scan_module() | ❌ (not idiomatic) |
| Decorator/Macro | ✅ @worker_task | ✅ #[worker] |
| Registration | Auto via decorator | Explicit add_worker() |
Python:
handler = TaskHandler(
configuration=config,
scan_for_annotated_workers=True,
import_modules=['my_app.workers']
)
Rust:
let mut handler = TaskHandler::new(config)?;
// With macro - similar feel to Python
handler.add_worker(greet_worker()); // Generated by #[worker] macro
handler.add_worker(process_worker());
Status: ✅ EQUAL for definition, DIFFERENT for discovery
Rust has #[worker] macro that provides the same declarative feel as Python's @worker_task.
The only difference is Rust requires explicit add_worker() calls (no runtime scanning),
which is more idiomatic for Rust and provides compile-time safety.
Update Retry Logic
| Feature | Python | Rust |
|---|---|---|
| Retry attempts | 4 | 4 |
| Backoff delays | 10s, 20s, 30s | 10s, 20s, 30s |
| TaskUpdateFailure event | ✅ | ✅ |
Status: ✅ EQUAL
Summary: Feature Status
Fully Implemented (100% Parity)
- Worker Configuration - All 10 properties including
strict_schema - Hierarchical Config - 3-tier environment variable resolution
- Polling Loop - Dynamic batch polling, adaptive backoff
- Concurrency Control - Semaphore-based limiting
- Events System - All 7 event types
- Prometheus Metrics - All metrics with HTTP endpoint
- Task Registration - Auto-register with overwrite control
- TaskContext - Full poll_count and metadata access
- Long-Running Tasks - TaskInProgress support
- JSON Schema Generation - Using
schemarscrate with strict mode support - Schema Registration - Automatic input/output schema registration
Idiomatic Differences (Not Missing)
- Worker Discovery/Scanning
- Python:
scan_packages(),scan_module() - Rust: Explicit
add_worker()registration - Reason: Not idiomatic in Rust (no runtime reflection)
- The explicit registration pattern is the Rust way and provides compile-time safety
- Python:
Conclusion
| Aspect | Status |
|---|---|
| Core Worker Features | 100% EQUAL |
| Configuration System | 100% EQUAL |
| Polling Loop | 100% EQUAL |
| Event System | 100% EQUAL |
| Metrics | 100% EQUAL |
| Task Registration | 100% EQUAL |
| TaskContext | 100% EQUAL |
| Schema Generation | 100% EQUAL |
| Schema Registration | 100% EQUAL |
Overall: 100% feature parity
The Rust SDK has complete feature parity with the Python SDK for all worker features:
- All 10 configuration options
- TaskContext with poll_count, retry_count, and metadata
- Automatic task definition registration
- JSON Schema generation from Rust types (using
schemars) - Automatic input/output schema registration
- Full event and metrics support
- Long-running task support with TaskInProgress