Cluster & Multi-Node
Scale Orch8 horizontally by running multiple engine nodes. Work is distributed automatically via database-level coordination — no external queue or service mesh required. Graceful drain ensures zero-downtime deployments.
Architecture
Orch8 uses a shared-nothing, database-coordinated architecture. Each node is an independent process that claims work from the same database. No leader election, no consensus protocol, no external coordination service.
Shared Database
PostgreSQL serves as the single source of truth. Nodes claim work using row-level locks (SELECT FOR UPDATE SKIP LOCKED).
No Single Point of Failure
Any node can go down. Unclaimed work is picked up by remaining nodes. No work is lost.
Heartbeat Registration
Each node registers itself and sends periodic heartbeats. Stale nodes (missed heartbeats) have their claimed work reclaimed.
Horizontal Scaling
Add nodes to handle more concurrent executions. Remove nodes when load decreases. No reconfiguration needed.
Node Lifecycle
Startup
Node generates a unique ID, registers in the cluster_nodes table, and starts its scheduler loop.
Heartbeat
Periodic heartbeat updates the node's last_seen_at timestamp. Other nodes can detect if this node becomes unresponsive.
Claim & Execute
Scheduler claims instances (SELECT FOR UPDATE SKIP LOCKED), executes steps, and writes outputs. Multiple nodes claim different instances.
Stale Detection
If a node misses heartbeats beyond the threshold, its claimed work is released back to the queue for other nodes to pick up.
Drain (optional)
Before shutdown, a drain signal tells the node to stop claiming new work and finish in-flight executions.
Shutdown
Node completes in-flight work, deregisters from cluster_nodes, and exits cleanly.
Work Distribution
Work distribution is pull-based. Each node's scheduler loop independently queries for claimable instances and processes them.
Claim Mechanics
-- Simplified claim query (actual implementation is more sophisticated)
SELECT id FROM instances
WHERE state = 'scheduled'
AND next_fire_at <= NOW()
ORDER BY priority DESC, next_fire_at ASC
FOR UPDATE SKIP LOCKED
LIMIT {batch_size}SKIP LOCKED ensures no two nodes ever claim the same instance. Higher priority instances are claimed first.
Concurrency Control
- ●Per-node batch size — each node claims up to N instances per tick (configurable)
- ●Concurrency keys — instances sharing a concurrency_key are limited to max_concurrency active at once, across all nodes
- ●Rate limits — per-handler rate limiting is enforced globally via database counters
Graceful Drain
Before taking a node offline (for deployment, maintenance, or scaling down), initiate a drain. The node stops claiming new work but finishes everything in progress.
Signal
Send drain signal via API or SIGTERM. Node enters draining state.
Stop Claiming
Scheduler stops pulling new instances from the queue.
Complete In-Flight
All currently executing steps run to completion. No work is abandoned.
Deregister
Once all in-flight work finishes, node removes itself from cluster_nodes and exits.
Zero-Downtime Deployment
# Rolling deployment example (3 nodes)
# 1. Drain node-1
curl -X POST http://node-1:8080/cluster/nodes/node-1/drain
# 2. Wait for node-1 to finish in-flight work (health endpoint returns 503)
while curl -s http://node-1:8080/health/ready | grep -q "ok"; do sleep 2; done
# 3. Deploy new version to node-1, start it
systemctl restart orch8-node-1
# 4. Repeat for node-2, node-3
# At all times, at least 2 nodes are serving trafficCluster API
List Nodes
GET /cluster/nodes
Response:
[
{
"id": "node-abc123",
"hostname": "orch8-prod-1",
"started_at": "2024-01-15T08:00:00Z",
"last_heartbeat": "2024-01-15T12:34:56Z",
"state": "active",
"instances_claimed": 42
},
{
"id": "node-def456",
"hostname": "orch8-prod-2",
"started_at": "2024-01-15T08:00:00Z",
"last_heartbeat": "2024-01-15T12:34:55Z",
"state": "draining",
"instances_claimed": 3
}
]Drain Node
POST /cluster/nodes/{node_id}/drain
Response: 200 OK (no body)
// Node transitions to "draining" state
// Stops claiming new work
// Completes in-flight executions
// Exits when idleHealth endpoints: UseGET /health/live (is the process running?) andGET /health/ready (is it accepting work?) for load balancer health checks. A draining node returns 503 on/health/ready but 200 on/health/live.
Deployment Patterns
Kubernetes
Deploy as a Deployment with multiple replicas. Use/health/ready as the readiness probe and/health/live as the liveness probe. SetterminationGracePeriodSeconds high enough for in-flight work to complete (e.g., 300s). PreStop hook sends the drain signal.
Docker Compose / VMs
Run multiple instances pointing to the same PostgreSQL. Each process self-registers. Use SIGTERM for graceful shutdown (triggers drain automatically).
Auto-Scaling
Scale based on queue depth (instances in Scheduled state withnext_fire_at <= now()). When queue grows, add nodes. When idle, drain and remove. No state migration needed — work rebalances automatically.
Single Node (Development)
Orch8 works perfectly with a single node — no cluster configuration required. The same binary scales from laptop to production fleet.
Environment Variables
# Required
DATABASE_URL=postgres://user:pass@host:5432/orch8
# Optional cluster tuning
ORCH8_NODE_ID=node-1 # Auto-generated if not set
ORCH8_HEARTBEAT_INTERVAL_SECS=10 # How often to heartbeat (default: 10)
ORCH8_STALE_THRESHOLD_SECS=60 # When to consider a node dead (default: 60)
ORCH8_BATCH_SIZE=50 # Instances claimed per scheduler tick
ORCH8_TICK_INTERVAL_MS=100 # Scheduler loop interval (default: 100ms)