Skip to content

Async flow

The relay is a sidecar container that runs alongside the inference pod. It pulls jobs from a Redis queue, calls the local inference model, and publishes results back to the gateway.

Gateway
├── Upload input → S3
├── Save job record → Redis (status: pending)
└── RPUSH job_id → relay:<model>:pending
│
┌─────────▼──────────┐
│ Relay │
│ (sidecar pod) │
└─────────┬──────────┘
│ BLMOVE LEFT RIGHT
relay:<model>:pending
→ relay:<model>:processing
│
1. Download + decrypt input from S3
2. POST to inference (127.0.0.1:9000/...)
3. Upload result → S3
4. PUBLISH → jobs:<model>:completed
5. Remove from relay:<model>:processing
│
Gateway subscriber
├── Update job status → Redis (completed)
├── Trigger webhook (if callback_url set)
└── Notify pub/sub (job:<id>:done)

The relay polls GET /health on the local inference service (port 9000) until it returns 200. This ensures the inference model is loaded before any jobs are dequeued.

BLMOVE relay:<model>:pending relay:<model>:processing LEFT RIGHT [timeout]

Blocks until a job arrives or the queue_pop_timeout expires (default: 5 min). On timeout, the relay exits 0 — KEDA will not restart it while the queue remains empty.

On a successful dequeue the relay acquires a processing lease — SET relay:<model>:lease:<id> with TTL lease_ttl (config, default 60s) — and refreshes it every lease_ttl/3 for the whole job. If this pod dies mid-job the lease expires, and the gateway reaper requeues the job (see Crash recovery). lease_ttl need not be sized to the job length — the heartbeat keeps it alive for arbitrarily long jobs; it only bounds how quickly a dead pod’s job is detected.

  1. Fetch job record from Redis (service type, model, S3 key, operation)
  2. Download input file from S3
  3. Decrypt if encryption is enabled (AES-256-GCM streaming, see Encryption)
  4. POST plaintext to http://127.0.0.1:9000/<inference_url> (multipart)
  5. Upload result.json to S3
PUBLISH jobs:<model>:completed {"job_id": "...", "status": "completed", ...}

The gateway subscriber receives this, updates the job record in Redis, and triggers the callback webhook if configured.

The relay removes the job from relay:<model>:processing and deletes its lease, then exits 0. KEDA creates a fresh pod for the next job.

When server.priority_header is set and the header is present on the submission request, the gateway uses LPUSH instead of RPUSH. The relay always dequeues from the left (BLMOVE LEFT RIGHT), so priority jobs skip ahead of the queue with no separate Deployment required.

When a client calls DELETE /jobs/{service_type}/{id}, the gateway publishes to the relay:<model>:cancel channel. The relay subscribes to this channel while processing; if it receives the signal, it interrupts inference and marks the job as cancelled. The relay then exits 0.

Jobs cancelled before processing begins are simply marked cancelled in Redis — no relay interaction needed.

ScenarioBehaviour
Inference returns non-200Job marked failed, error stored in Redis
S3 download failsJob marked failed
S3 upload failsJob marked failed
Relay pod crashes (no Done)Lease expires; the gateway reaper requeues the job automatically (see Crash recovery) — dead-lettered after gc.max_reap_attempts
Relay exits on BLMOVE timeoutPod exits 0, KEDA does not restart until queue is non-empty

A crashed pod’s job is recovered automatically (lease + reaper). Terminally-failed jobs (bad input, inference error) are not retried — resubmit via POST /jobs/{service_type} if needed.

KeyTypeDescription
relay:<model>:pendinglistJobs waiting to be picked up
relay:<model>:processinglistJob currently being processed (at most one per pod)
relay:<model>:lease:<id>string (TTL)Processing lease, refreshed while the pod works
relay:<model>:deadletterlistJobs the reaper could not recover
jobs:<model>:completedpub/subRelay → gateway completion signal
relay:<model>:cancelpub/subGateway → relay cancellation signal
job:<id>:donepub/subGateway → polling clients notification