Open source corePro workflowsSelf-hosted console
Type-safe job queues & durable workflows.
Start with open-source Redis queues in Crystal, and run workers in Python, TypeScript, Ruby, Go, Rust, Java, PHP or .NET through the SDK gateway of a self-hosted JoobQUI. Scale to event-sourced sagas, deterministic replay and a real-time operator console.
- Self-hostedJobs, workers and data stay in your network; only a licence check-in leaves it.
- MIT coreThe JoobQ queue is open source.
- RBAC · SSO · TOTPThree roles, OpenID Connect, two-factor.
- Audit logA row for every state change.
- crystal
- >= 1.22.0
- joobq
- 0.5.6
- joobq_pro
- 0.4.1
# worker.cr (excerpt): a job with a sealed field
struct SealedJob
include JoobQ::Job
@queue = SEALED
@retries = 3
@max_retries = 3
@expires = 1.day.from_now.to_unix_ms
property probe : String = ""
property enqueued_before_ms : Int64 = 0_i64
encrypted_property card_number : String = "", job_name: "Conf::SealedJob"
def initialize
end
def initialize(@probe : String, @card_number : String, @enqueued_before_ms : Int64 = 0_i64)
end
def perform
Conf.report("sealed", probe, {"card_sha256" => Conf.any(Digest::SHA256.hexdigest(card_number)), "enqueued_before_ms" => Conf.any(enqueued_before_ms)})
end
end
# ...and, in another job's perform, a delayed enqueue of it
when "conf.produce"
before = Conf.now_ms # before the call: the delay counts from when the job is stored
jid = SealedJob.delay(for: 1.5.seconds, probe: probe, card_number: card_number, enqueued_before_ms: before)
Conf.report("produce", probe, {"jid" => Conf.any(jid.to_s)})A typed Crystal job with an AES-256-GCM sealed field, and a delayed enqueue of it. MIT licensed; tested against a real server.
# worker.py (excerpt): jobs through the SDK gateway
@job("conf.retry", queue=QUEUE, retries=3)
def retry_job(payload, ctx):
if ctx.attempt < 3:
raise RuntimeError(f"transient failure on attempt {ctx.attempt}")
report(ctx, "retry", payload, attempt=ctx.attempt)
@job("conf.dead", queue=QUEUE)
def dead_job(payload, ctx):
report(ctx, "dead_attempt", payload)
raise NonRetryableError(f"permanent failure {payload['probe']}")
@job("conf.produce", queue=QUEUE)
def produce_job(payload, ctx):
before = now_ms() # before the call: the gateway counts the delay from when it receives it
res = ctx.enqueue(sealed_job, {"probe": payload["probe"], "card_number": payload["card_number"], "enqueued_before_ms": before},
delay_ms=1500)
report(ctx, "produce", payload, jid=res["jid"])The same kind of jobs in Python, through the SDK gateway of a self-hosted JoobQUI: retries, a non-retryable failure and a delayed enqueue.
# worker.cr (excerpt): two workflows
workflow "conf-saga-#{LANG}" do
step "reserve", Reserve, input: {probe: input(:probe)}, compensate: compensation(Release, input: ref("steps.reserve.output.reservation"))
step "charge", Charge
end,
workflow "conf-signal-#{LANG}" do
step "before", Ok
wait_for "approval", signal: "approve", timeout: 60.seconds
step "after", Approved, input: {by: ref("steps.approval.output.by")}
end,# worker.py (excerpt): the same workflows as blueprints
wf("saga").step("reserve", reserve, input={"probe": input("probe")},
compensate=compensation(release, input=output("reserve", "reservation")))
.step("charge", charge),
wf("signal").step("before", before).wait_for("approval", signal="approve", timeout_ms=60000)
.step("after", approved, input={"by": output("approval", "by")}),A saga whose failed charge releases the reservation, and a run that waits for an approve signal: in Crystal on the engine, and in Python as a blueprint the engine conducts.
JOOBQUI_SELF_HOSTED_IMAGE=ghcr.io/azutoolkit/joobqui-selfhosted:<your release tag>
JOOBQUI_SELF_HOSTED_DOMAIN=queues.example.com# From the Quickstart: run it from the folder that holds deploy/, after writing secrets/ and deploy/.env
docker compose -f deploy/selfhosted.compose.yml --env-file deploy/.env --profile redis run --rm migrate
docker compose -f deploy/selfhosted.compose.yml --env-file deploy/.env --profile redis --profile https up -d
# Until it has a licence the server serves its setup page: open it, enter this code, Sign in with JoobQ
docker compose -f deploy/selfhosted.compose.yml --env-file deploy/.env logs app | grep -A4 "not set up yet"
docker compose -f deploy/selfhosted.compose.yml --env-file deploy/.env exec app ./joobqui doctor
# Sign-up is closed by default: the first account made on the command line is an admin
docker compose -f deploy/selfhosted.compose.yml --env-file deploy/.env exec app ./joobqui accounts:create admin@example.com admin
# Just trying it? One container, nothing configured: the setup page asks for everything
docker run -d -p 4000:4000 -v joobqui-data:/data ghcr.io/azutoolkit/joobqui-selfhosted:latestThe Quickstart: Docker Compose runs the licensed server beside PostgreSQL, Redis and Caddy. There is no licence key: you open the server, enter the setup code from its log and Sign in with JoobQ, then create the first admin.
Engineering guarantees
Architecture specs, not adjectives
- Encryption01
AES-256-GCM field-level sealing
Chosen job fields are sealed before they are stored: at rest in Redis, backups, dead letters and the console. Key rotation built in.
- encrypted_property
- encrypted_fields
- JOOBQUI_PAYLOAD_KEYS
- Worker ecosystem02
Crystal native + 8 SDK languages
Python, TypeScript, Go, Rust, Ruby, Java, PHP and .NET workers long-poll the SDK gateway; they never hold Redis credentials.
- joobq
- joobq_pro
- /api/v1/sdk
- Auth & governance03
3-role RBAC · OIDC SSO · TOTP
viewer, operator and admin enforced on every route; OpenID Connect single sign-on, TOTP two-factor and an audit row for every state change.
- viewer
- operator
- admin
- audit log
- Deployment04
One docker compose stack
The server and PostgreSQL, with Redis (bundled or your own) and Caddy for HTTPS as optional profiles. Settings and licence persist in a /data volume.
- selfhosted.compose.yml
- --profile redis
- --profile https
One ecosystem, three steps
Start with the open-source queue. Add what you need when you need it.
- Step 01: JoobQ · OSSOpen source · MIT
JoobQ
Redis-backed job queues, native to Crystal, with typed jobs and nothing else to run.
- Typed
JoobQ::Jobstructs, per-queue workers and throttles - Exponential-backoff retries and a dead-letter set
- Delayed, recurring and cron jobs
- AES-256-GCM
encrypted_propertyjob fields - A REST management API, off by default, behind bearer tokens
- Typed
- Step 02: JoobQ ProCommercial shard · Pro key
JoobQ Pro
A durable workflow engine and production extensions on top of the same queues. Every Pro feature checks a Pro key.
- Workflows whose state is a fold of an append-only Redis history
- Sagas: compensations run newest first on failure or cancel
- Signals, timers, queries, child workflows, continue-as-new; updates and scopes in Crystal
- Deterministic replay, versioning gates and worker build ids
- Batches with callbacks, unique jobs, rate limits, metrics and alerting
- Step 03: JoobQUISelf-hosted console · licensed
JoobQUI
An operator console and SDK gateway you run next to your workers, with your data in your database. Licensed through the portal: Sign in with JoobQ once, no key to paste.
- Live KPIs, queues, jobs and dead letters over a WebSocket stream
- Workflow graph, event history with time travel, bulk actions
- Viewer / operator / admin roles, API tokens and an audit log
- TOTP two-factor and OpenID Connect single sign-on
- The SDK gateway for workers in 8 more languages,
/metricsand an OpenAPI 3.1/api/v1
Queues
Background jobs, native in Crystal.
In Crystal, jobs are structs checked by the compiler. JoobQ keeps them in Redis, runs them on fibers, and stops cleanly on SIGTERM. Workers in the other 8 languages reach the same queues through the SDK gateway of a self-hosted JoobQUI: they long-poll it over HTTPS and never connect to Redis.
- JoobQ · OSS
Typed jobs and queues
Include
JoobQ::Job, implementperform, register a queue with a worker count. Arguments are JSON-serialized, and each job class publishes a JSON schema for the REST API. - JoobQ · OSS
Retries and dead letters
Three retries by default with exponential backoff; exhausted jobs move to the dead-letter set with their error context, ready to redrive.
- JoobQ · OSS
Delayed, recurring and cron
delay(for:),enqueue_at,schedule(every:)and aschedulerblock withcronandevery, evaluated in a time zone. - JoobQ · OSS
Throttling and graceful drain
Per-queue
throttle: {limit:, period:}, applied when a worker claims a job.JoobQ.forgetraps SIGTERM, finishes running jobs and puts back what outlives the deadline. - JoobQ · OSS
Sealed job arguments
encrypted_propertystores chosen fields as AES-256-GCM sealed strings bound to the job class and field, with key rotation. - JoobQ Pro
Metrics, batches and limits
Pro adds latency percentiles, alert rules, batches with completion callbacks (Crystal), the two unique-job release modes, per-key rate limiting and job timeouts.
Nine languages
Workers in the language your service is written in.
Crystal jobs and workflows run natively on the JoobQ shards. Python, TypeScript, Ruby, Go, Rust, Java, PHP and .NET workers use thin SDKs that talk to the SDK gateway of a self-hosted JoobQUI (/api/v1/sdk) with the server’s SDK token: they long-poll for tasks, renew their leases and complete or fail them with a stack trace, and never connect to Redis. Jobs are Core; SDK workflows need a JoobQ Pro key on the server.
Registry packages: on the roadmap. The 8 SDKs are not published to package registries yet. Copy sdks/<language> from the joobqui repository into your project and reference it by path.
dependencies:
# Queues and jobs (MIT)
joobq:
github: azutoolkit/joobq
version: "= 0.5.6"
# Or durable workflows, with JoobQ Pro (needs read access to the repository)
# joobq_pro:
# github: azutoolkit/joobq-pro
# version: "= 0.4.1"shards install
# src/worker.cr
# require "joobq" # or: require "joobq_pro"JoobQ is a shard; JoobQ Pro (workflows) is a commercial shard in a private repository and brings the joobq revision it was tested with, so list one or the other.
mkdir -p vendor/joobq-sdk/python
cp -r joobqui/sdks/python/joobq vendor/joobq-sdk/python/
export PYTHONPATH=vendor/joobq-sdk/python
export JOOBQ_GATEWAY_URL=http://127.0.0.1:8080 # the JoobQUI server
export JOOBQUI_SDK_TOKEN=... # JOOBQUI_SDK_TOKEN set on the serverimport os
from joobq import Job, JoobQClient, JoobQWorker
client = JoobQClient(os.environ["JOOBQ_GATEWAY_URL"], os.environ["JOOBQUI_SDK_TOKEN"])One package, joobq, standard library only. Not on PyPI: copy it and put its folder on the import path.
mkdir -p vendor/joobq-sdk/typescript
cp joobqui/sdks/typescript/src/index.ts vendor/joobq-sdk/typescript/
export JOOBQ_GATEWAY_URL=http://127.0.0.1:8080 # the JoobQUI server
export JOOBQUI_SDK_TOKEN=... # JOOBQUI_SDK_TOKEN set on the serverimport { JoobQClient, JoobQWorker, defineJob } from './vendor/joobq-sdk/typescript/index.ts'
const client = new JoobQClient(process.env.JOOBQ_GATEWAY_URL!, process.env.JOOBQUI_SDK_TOKEN!)One file, index.ts, on Node built-ins (fetch, node:crypto). Not on npm: import it by path.
mkdir -p vendor/joobq-sdk/ruby
cp -r joobqui/sdks/ruby/lib vendor/joobq-sdk/ruby/
export JOOBQ_GATEWAY_URL=http://127.0.0.1:8080 # the JoobQUI server
export JOOBQUI_SDK_TOKEN=... # JOOBQUI_SDK_TOKEN set on the serverrequire_relative "vendor/joobq-sdk/ruby/lib/joobq"
client = JoobQ::Client.new(gateway_url: ENV.fetch("JOOBQ_GATEWAY_URL"), token: ENV.fetch("JOOBQUI_SDK_TOKEN"))One file, lib/joobq.rb, on json and net/http. Not a published gem: require it by path.
module example.com/billing
go 1.22
require github.com/azutoolkit/joobq-sdk-go v0.1.0
replace github.com/azutoolkit/joobq-sdk-go => ./vendor/joobq-sdk/go// cp -r joobqui/sdks/go vendor/joobq-sdk/go
import joobq "github.com/azutoolkit/joobq-sdk-go"
client := joobq.NewClient(os.Getenv("JOOBQ_GATEWAY_URL"), os.Getenv("JOOBQUI_SDK_TOKEN"))Module github.com/azutoolkit/joobq-sdk-go (Go 1.22, no dependencies). It is not fetchable with go get: point the module path at the copy with replace.
# cp -r joobqui/sdks/rust vendor/joobq-sdk/rust
[dependencies]
joobq = { path = "vendor/joobq-sdk/rust" }use joobq::{JobDef, JoobQClient, Workflow};
let client = JoobQClient::new(env::var("JOOBQ_GATEWAY_URL")?, env::var("JOOBQUI_SDK_TOKEN")?);Crate joobq 0.1.0 (edition 2021; pinned dependencies: serde, serde_json, ureq with rustls, signal-hook). Not on crates.io: add it as a path dependency.
mkdir -p vendor/joobq-sdk/java
cp -r joobqui/sdks/java/src vendor/joobq-sdk/java/
javac -d out $(find vendor/joobq-sdk/java/src/main/java -name '*.java') App.java
export JOOBQ_GATEWAY_URL=http://127.0.0.1:8080 # the JoobQUI server
export JOOBQUI_SDK_TOKEN=... # JOOBQUI_SDK_TOKEN set on the server
java -cp out Appimport io.joobq.JoobQClient;
import io.joobq.JoobQClient.JobDefinition;
JoobQClient client = new JoobQClient(System.getenv("JOOBQ_GATEWAY_URL"), System.getenv("JOOBQUI_SDK_TOKEN"));One source file, io/joobq/JoobQClient.java, on the JDK only (java.net.http, records). Not on Maven Central: compile it with your sources.
mkdir -p vendor/joobq-sdk/php
cp -r joobqui/sdks/php/src vendor/joobq-sdk/php/
export JOOBQ_GATEWAY_URL=http://127.0.0.1:8080 # the JoobQUI server
export JOOBQUI_SDK_TOKEN=... # JOOBQUI_SDK_TOKEN set on the server<?php
require_once __DIR__ . '/vendor/joobq-sdk/php/src/JoobQClient.php';
use JoobQ\JoobQClient;
use JoobQ\JoobQWorker;
$client = new JoobQClient(getenv('JOOBQ_GATEWAY_URL'), getenv('JOOBQUI_SDK_TOKEN'));One file, src/JoobQClient.php, namespace JoobQ (readonly properties). Not on Packagist: require it by path.
# SDK-style projects compile every .cs file under the project folder
mkdir -p vendor/joobq-sdk/dotnet
cp joobqui/sdks/dotnet/JoobQClient.cs vendor/joobq-sdk/dotnet/
export JOOBQ_GATEWAY_URL=http://127.0.0.1:8080 # the JoobQUI server
export JOOBQUI_SDK_TOKEN=... # JOOBQUI_SDK_TOKEN set on the server
dotnet runusing JoobQ;
var client = new JoobQClient(
Environment.GetEnvironmentVariable("JOOBQ_GATEWAY_URL")!,
Environment.GetEnvironmentVariable("JOOBQUI_SDK_TOKEN")!);One file, JoobQClient.cs, namespace JoobQ, on System.Net.Http and System.Text.Json. Not on NuGet: copy it into your project folder.
Data flow
SDK workers talk to the gateway, never to Redis
Your SDK workers
Python · TypeScript · Ruby · Go · Rust · Java · PHP · .NET
Operators
Browser · console at /ui/
joobqui-selfhosted · one or more instances
JoobQUI server
- SDK gateway/api/v1/sdk
- Console API/api/v1
- Workflow engineconducts SDK runs · Pro
Only outbound call: licence check-in every 15 min → app.joobq.io
Redis 6.2+
queues · leases · workflow histories
PostgreSQL
accounts · token hashes · audit
Your Crystal workers
Crjoobq + joobq_pro shards
Workflows · JoobQ Pro
Durable workflows that replay, not just retry.
A workflow is a declared graph of steps. Every state change is an event appended to its history in Redis by one Lua script, so a crashed worker loses nothing Redis kept. Crystal workflows run on the engine directly; workflows written with the 8 SDKs are conducted by the same engine on the server, which sends each step to a worker as its own leased task.
Fire-and-forget job
JoobQ · OSS- enqueue
- perform
- retry · backoff
- dead letter
A job is one unit of work: it is retried with exponential backoff, then kept in the dead-letter set with its error. Undoing what earlier jobs already did is left to your code.
Durable workflow
JoobQ Pro- reserve
- charge
- release
A workflow is a declared graph whose state is a fold of an append-only history in Redis. The engine decides every step, so a crashed worker loses nothing Redis kept.
- Saga compensation on failureA step that fails for good runs the compensations of the completed steps, newest first; then the run is
failed. - Signals with timeouts
wait_forparks the run without holding a worker; with no signal in time the engine recordsStepTimedOut. - Deterministic replay
replay(history, definition)re-decides a recorded history against new code and reports the first divergence.
Simulate execution
- step beforestep · activitypending
- wait_for approvalwait_for signal: "approve" · 60 spending60s
- step afterinput: steps.approval.output.bypending
- step reservecompensate: releasepending
- step chargeNonRetryableError("card declined")pending
- compensate releasecompensation of reservepending
The run parks on wait_for "approval" without holding a worker, until an operator sends the approve signal.
Event history
- No events yet. Press Simulate execution.
Audit log preview
append-only| Rows appear when an operator calls the API: the signal, a re-drive. |
Compensation is the engine's own work: it is in the run's event history, not the audit log, because nobody called the API. A re-drive is an operator call, and is audited.
A deterministic replay of the conformance workflows, in your browser. Nothing is sent anywhere.
- JoobQ Pro
Sagas with compensation
Give a step a
compensate:activity (in the SDKs also a wait, a timer or a child). On failure, cancel or timeout the completed steps are undone one at a time, newest first. - JoobQ Pro
Signals, queries, children, conditions
wait_fora signal, answer aqueryfrom state, run achildworkflow, skip a node withonly_if,timer,parallelandsequence, in Crystal and the 8 SDKs. Crystal workflows also accept validatedupdates. - JoobQ Pro
Deterministic replay
replay(history, definition)re-decides a recorded history against new code and reports the first divergence.migratemoves a run only if it agrees. Change gates for SDK blueprints are coming. - JoobQ Pro
Worker build ids
Pin each run of a Crystal workflow worker to the build it started on (
JOOBQ_PRO_BUILD_ID), promote a new build for new runs, and retire the old one once nothing is pinned to it. - JoobQ Pro
Search attributes and schedules
Typed attributes (keyword, int, float, bool, time) to find runs, and cron or interval workflow schedules with overlap policy, catch-up and backfill.
- JoobQ Pro
Scopes, continue-as-new, archive
Cancel a group of steps while the rest goes on (Crystal; coming to the SDKs), roll long runs into new ones (Crystal and the SDKs), and archive closed runs, redacted by default, before retention deletes them.
JoobQUI console
The operator console your on-call team will actually open.
JoobQUI runs next to your workers and reads the same Redis. The frame below copies the self-hosted console, page for page, with sample numbers: the re-drive, the signal and the audit log work.
Overview
Live health of workflows, queues and workers.
- Active workflows8up1
- Jobs/s103.9up15.6
- Failed / DLQ13up2
- Worker nodes5
- p99280msdown31ms
Throughput
Completed jobs per second
Latency
Time from start to finish
Queues
93 waiting · 22 running
- default payments checkout-api22 waiting · 5/8 busy
- emails growth notifier33 waiting · 6/10 busy
- billing payments billing-worker2 waiting · 4/8 busy
- conf-python conformance-python0 waiting · 1/4 busy
- reports growth notifierPaused31 waiting · 0/8 busy
- webhooks payments checkout-api5 waiting · 6/12 busy
Quick actions
Also available from ⌘K
The run parks on wait_for "approval" without holding a worker, until an operator sends the approve signal.
conf-signal-python
Pending Running Waiting Completed Failed- Execution ID
- conf-signal-python-7d2e
- Definition
- 4f9c2a71d0be
- Started
- 2026-11-08 13:42:47
- Duration
- 6.2s
- beforePending Running Waiting Completed Failed Timed Out Compensated
before
- approvalPending Running Waiting Completed Failed Timed Out Compensated60s
Signal
- afterPending Running Waiting Completed Failed Timed Out Compensated
approved
- reservePending Running Waiting Completed Failed Timed Out Compensated
reserve
- chargePending Running Waiting Completed Failed Timed Out Compensated
charge
- compensate:reservePending Running Waiting Completed Failed Timed Out Compensated
release
| # | Event | Step | Time |
|---|---|---|---|
| No events yet. Press Simulate execution. | |||
Dead letters
Jobs that ran out of retries.
dlq.redrive row.| Job | Queue | Error | Failed | |
|---|---|---|---|---|
| conf-python | NonRetryableErrorpermanent failure p-51c2 | 2m ago | ||
| emails | Mailer::SmtpError421 4.7.0 try again later | 7h 10m ago | ||
| emails | Mailer::SmtpError421 4.7.0 try again later | 7h 49m ago | ||
| billing | PG::ConnectionLostconnection reset by peer | 9h 41m ago | ||
| unreadable | billing | Job could not be parsed | 13h 33m ago | |
| webhooks | Redis::CommandTimeoutErrorRedis command timed out after 2.0s | 20h 39m ago |
Nothing here. No dead jobs: failures that exhaust their retries land here.
4e1c9a07-5b2d-4f8e-9c11-2a7d3e0f6b19
conf-python · failed 2m ago
NonRetryableError
pythonpermanent failure p-51c2
- 1invokejoobq/__init__.py:1501
- 2dead_jobworker.py:70
Stored job (secret-looking values masked)
{
"jid": "4e1c9a07-5b2d-4f8e-9c11-2a7d3e0f6b19",
"queue": "conf-python",
"name": "conf.dead",
"status": "dead",
"retries": 0,
"args": { "probe": "p-51c2" },
"error": {
"error_class": "NonRetryableError",
"message": "permanent failure p-51c2"
}
}9f2ca41e-77d0-4c3b-8a52-c1e09b6d2f34
emails · failed 7h 10m ago
Mailer::SmtpError
crystal421 4.7.0 try again later
- 1deliverlib/mailer/src/smtp.cr:88:7
- 2performsrc/jobs/welcome_email_job.cr:19:5
Stored job (secret-looking values masked)
{
"jid": "9f2ca41e-77d0-4c3b-8a52-c1e09b6d2f34",
"queue": "emails",
"name": "WelcomeEmailJob",
"status": "dead",
"retries": 0,
"args": { "user_id": 48213, "token": "[masked]" },
"error": {
"error_class": "Mailer::SmtpError",
"message": "421 4.7.0 try again later"
}
}c81a77f2-0e4b-4d61-b9a3-5f2e8c107d4a
emails · failed 7h 49m ago
Mailer::SmtpError
crystal421 4.7.0 try again later
- 1deliverlib/mailer/src/smtp.cr:88:7
- 2performsrc/jobs/welcome_email_job.cr:19:5
Stored job (secret-looking values masked)
{
"jid": "c81a77f2-0e4b-4d61-b9a3-5f2e8c107d4a",
"queue": "emails",
"name": "WelcomeEmailJob",
"status": "dead",
"retries": 0,
"args": { "user_id": 48177, "token": "[masked]" },
"error": {
"error_class": "Mailer::SmtpError",
"message": "421 4.7.0 try again later"
}
}6c3cdad9-b628-4be2-9ab5-40635eb87e73
billing · failed 9h 41m ago
PG::ConnectionLost
crystalconnection reset by peer
- 1post_entrysrc/repositories/ledger_repository.cr:31:5
- 2performsrc/jobs/charge_job.cr:27:9
Stored job (secret-looking values masked)
{
"jid": "6c3cdad9-b628-4be2-9ab5-40635eb87e73",
"queue": "billing",
"name": "ChargeJob",
"status": "dead",
"retries": 0,
"args": { "invoice": "inv_20931", "card_number": "[encrypted]" },
"error": {
"error_class": "PG::ConnectionLost",
"message": "connection reset by peer"
}
}61f0c919-e3d2-47a8-8c05-93b4e2a1f6d0
billing · failed 13h 33m ago
Stored job (secret-looking values masked)
null
ec308a7c-1d64-4f2b-a9e8-7b5c0d3e91f2
webhooks · failed 20h 39m ago
Redis::CommandTimeoutError
crystalRedis command timed out after 2.0s
- 1commandlib/redis/src/redis/connection.cr:142:11
- 2performsrc/jobs/deliver_webhook_job.cr:33:7
Stored job (secret-looking values masked)
{
"jid": "ec308a7c-1d64-4f2b-a9e8-7b5c0d3e91f2",
"queue": "webhooks",
"name": "DeliverWebhookJob",
"status": "dead",
"retries": 0,
"args": { "endpoint": "[masked]", "event": "invoice.paid" },
"error": {
"error_class": "Redis::CommandTimeoutError",
"message": "Redis command timed out after 2.0s"
}
}Audit
Every change made through the console and the API, and every sign-in.
| Time | Actor | Action | Target | Outcome | From |
|---|---|---|---|---|---|
| 2026-11-08 12:00:00 | ada@example.com admin | dlq.redrive | dlq · 1 jid | success | 10.0.0.4 |
| 2026-11-08 12:00:00 | lin@example.com operator | POST /workflows/conf-signal-go-55aa/signal | /workflows/conf-signal-go-55aa/signal | success | 10.0.0.7 |
| 2026-11-08 12:00:00 | ada@example.com admin | token.create | prometheus-scrape · viewer, no expiry | success | 10.0.0.4 |
| 2026-11-08 12:00:00 | sam@example.com viewer | POST /dlq/purge | /dlq/purge | denied | 10.0.0.9 |
| 2026-11-08 12:00:00 | lin@example.com operator | mfa.enable | lin@example.com | success | 10.0.0.7 |
| 2026-11-08 12:00:00 | mallory | session.sign_in | mallory | failure | 203.0.113.5 |
/ui/
Throughput and latency, as it happens
The overview listens to a WebSocket stream of lifecycle events and metric ticks: throughput, p50 / p95 / p99 latency, every queue’s backlog and busy workers, from 1 minute to 7 days.
- Live stream
The header says Live while the stream is open, and when it drops.
- Latency percentiles
p50, p95 and p99 from the run-time histogram JoobQ keeps in Redis; per queue too, with a JoobQ Pro key.
- Quick actions
Drain or resume every queue at once, operators and admins only; viewers see them disabled.
/ui/workflows/conf-signal-python-7d2e
Each run as a graph, with its full history
With a JoobQ Pro key on the server: every step of a run, its compensations and its event history. Operators signal, cancel, terminate or re-drive a run, and every one of those calls is audited.
- Saga compensation
A step that fails for good undoes the completed ones, newest first.
- Signals with timeouts
wait_forparks the run without holding a worker until the signal or the timeout. - Try it
Run the simulation: send the approve signal, or watch
releasecompensate a failed charge.
/ui/dlq
Dead letters, triaged in bulk
Every dead letter with its error class and message; open one for its stack trace and the stored job, secret-looking values masked. Re-drive one, or select many and re-drive or purge them at once.
- NonRetryableError
A job that raises it skips its remaining retries and goes straight to the dead letters.
- Outcome per job
The answer counts what was re-driven and what was skipped: already gone, or unreadable.
- Try it
Re-drive the selected rows, or open a job and re-drive it: the Audit view gets the row.
/ui/audit
Access control an auditor can read
viewer, operator and admin enforced by the server on every route; OpenID Connect single sign-on, TOTP two-factor, admin-issued API tokens, and an audit row for every change and every sign-in.
- Critical changes first
Token, role and second-factor changes write the audit row before they act.
- Denied is recorded too
A viewer’s refused purge is a row, with the address it came from.
- Export
CSV or JSON, filtered by actor, action, outcome and date.
Comparison
What each part of the ecosystem gives you.
JoobQ Pro builds on the open-source core; JoobQUI operates both and runs the SDK gateway. The JoobQUI column says what the self-hosted server and its console add; a feature marked Pro there needs a JoobQ Pro key on the server.
| Capability | JoobQ Open source (MIT) | JoobQ Pro Commercial shard, Pro key | JoobQUI Self-hosted, licensed |
|---|---|---|---|
| At a glance | |||
| Self-hosting | In your process, on your Redis | In your process, on your Redis | Your servers, Docker Compose3 |
| Polyglot workers | Crystal | Crystal | + 8 SDK languages via /api/v1/sdk |
| Native sagas and signals | Not included | Included | Inspect, signal, cancel, re-drive1 |
| Encrypted payloads (AES-256-GCM) | encrypted_property | Included | SDK encrypted_fields; shown as [encrypted] |
| RBAC, SSO and audit log | Not included | Not included | 3 roles · OIDC · TOTP · audit2 |
| Licensing | MIT, free | Included with a paid licence1 | Flat monthly tiers, cancel any time |
| Queues and jobs | |||
| Typed jobs, Redis queues, per-queue workers | Included | Included | Pause, resume, clear, resize |
| Retries with exponential backoff | Included | Included | Retry and delete jobs |
| Dead-letter set and redrive | Included | Included | Bulk redrive and purge |
| Delayed, recurring and cron jobs | Included | Included | Pause, resume, run now |
| Queue throttling (per queue, at claim) | Included | Included | SDK queues the server’s JoobQ config defines |
| HTTP management API | Bearer tokens, no OpenAPI file | Same as JoobQ | OpenAPI 3.1, /api/v1 |
| Job expiry | Included | Included | Not included |
| Batch enqueue | Not included | Not included | SDK enqueue_many, 1 to 500 jobs |
| Batches with completion callbacks | Not included | partialCrystal only | Not included |
| Unique jobs | Not included | Included | SDKs: while_locked; the release modes need Pro |
| Rate limiting (per key, across processes) | Not included | Included | SDK rate_limit1 |
| Run timeouts | Not included | Included | Not included |
| Durable workflows | |||
| Event-sourced history in Redis | Not included | Included | Event history, time travel |
| Workflows written with the 8 SDKs | Not included | The engine conducts them | Through the gateway1 |
| Sagas: compensation newest first | Not included | Included | Cancel with compensation |
| Signals, queries, timers, child workflows | Not included | Included | Signal, query |
| Validated updates | Not included | partialCrystal; SDKs coming | Send updates |
| Deterministic replay and migration | Not included | Included | Replay, migrate (dry run) |
| Continue-as-new | Not included | Included | Continue a run now |
| Cancellation scopes | Not included | partialCrystal; SDKs coming | Cancel one scope |
| Worker build ids | Not included | partialCrystal workers | Promote, retire, re-pin |
| Search attributes and workflow schedules | Not included | Included | Filter runs, manage schedules |
| Bulk operations with dry run | Not included | Not included | Up to 200 ids or a filter1 |
| Operations and security | |||
| Latency percentiles (p50 / p95 / p99) | partialHistogram in Redis | Included | Live charts, 1 min to 7 days1 |
| Alert rules | Not included | Included | Rules, acknowledgement1 |
| Email, Slack, PagerDuty and signed webhook channels | Not included | Not included | They deliver Pro alerts |
| Prometheus metrics endpoint | Not included | Not included | /metrics |
| Roles, API tokens and audit log | Not included | Not included | viewer · operator · admin |
| TOTP two-factor and OIDC single sign-on | Not included | Not included | TOTP; SSO2 |
| Secret-looking payload keys masked | Not included | History archive redaction | Every view and export |
- 1Needs a JoobQ Pro key on the server. Every paid self-hosted licence includes one; a 14-day trial key is available once per organization.
- 2Single sign-on is in the Team and Enterprise tiers.
- 3The server checks in with the licence portal every 15 minutes (version, active users, worker count, the Pro features in use, an instance id). Jobs, payloads and accounts stay in your Redis and PostgreSQL.
Pricing
Free to start, licensed when you run the console.
JoobQ is open source and free. A paid tier licenses the self-hosted JoobQUI server for your deployments, and every one of them includes JoobQ Pro: there is no separate Pro price.
Free Forever
JoobQ Open Source · MIT
$0
JoobQ, the open-source core, in Crystal. MIT licensed, no key, no limits we enforce.
- Licence
- MIT
- Key
- None
- Limits
- None
- Typed jobs, queues, retries and dead letters
- Delayed, recurring and cron jobs
- Encrypted job properties
- A REST management API (off by default)
- Community support on GitHub
Starter
JoobQUI Self-Hosted + JoobQ Pro
$49/ month
One self-hosted console for a small team.
- Seats
- 5
- Deployments
- 1
- SSO
- No
- Everything in Free
- Self-hosted JoobQUI: 5 seats, 1 deployment
- The SDK gateway: workers in 8 more languages
- A JoobQ Pro key, renewed with each licence check-in
- Live KPIs, workflows, alerts, dead-letter triage
- Roles, API tokens, audit log, TOTP
Team
RecommendedJoobQUI Self-Hosted + JoobQ Pro
$199/ month
Up to 3 deployments and single sign-on.
- Seats
- 25
- Deployments
- 3
- SSO
- OIDC
- Everything in Starter
- 25 seats, 3 deployments
- OpenID Connect single sign-on (PKCE)
- SSO group-to-role mapping
Enterprise
JoobQUI Self-Hosted + JoobQ Pro
Contact us
Unlimited seats for organisations with many operators.
- Seats
- Unlimited
- Deployments
- 10
- SSO
- OIDC
- Everything in Team
- Unlimited seats, 10 deployments
- Terms agreed with you
Transparent licence terms
- Monthly, billed through Stripe; taxes added at checkout where they apply.
- No key to paste: each server connects itself with Sign in with JoobQ and fetches its licence.
- Seats, deployments and SSO are the limits the licence enforces, nothing hidden.
- Cancel any time: the licence runs to the end of the paid month.
- An unreachable portal does not end a paid period for up to 7 days after the last check-in.
- A 14-day JoobQ Pro trial key, once per organization, from the portal.
Enterprise security & sovereignty
Runs in your network, answers to your auditors.
Your network, your data
Jobs, workflow histories and payloads stay in your Redis; accounts and the audit trail in your PostgreSQL. Only the licence check-in leaves.
Workers keep running
If a licence lapses, the console’s operations stop until you renew, but the SDK gateway keeps serving: workers go on running jobs and workflows.
Keys you hold
Fields sealed with AES-256-GCM under
JOOBQUI_PAYLOAD_KEYS; the console shows them as[encrypted].Least privilege
viewer, operator and admin on every route, OIDC single sign-on, TOTP two-factor, an audit row for every change.
FAQ
Questions operators ask first.
What Redis do I need?
One Redis server, version 6.2 or newer (the SDK gateway claims tasks with LMOVE), reachable by your Crystal workers and the JoobQUI server (SDK workers never connect to it: they talk to the server over HTTPS). Connect with REDIS_URL as redis://, rediss:// or a unix socket; ACL users are supported. Over TLS, JoobQUI checks the certificate chain (REDIS_CA_FILE names your own authority) but not the host name.
Redis Cluster is not supported: keys are not hash-tagged. If losing the last second of workflow history on a crash is not acceptable, run Redis with AOF appendfsync always. Use a noeviction (or volatile-*) policy so the console never forgets a session revocation.
Where does my data live with the self-hosted console?
In your infrastructure. Jobs, workflow histories and live events stay in your Redis; accounts, API token hashes, the audit log and notification channels go to your PostgreSQL.
The only traffic to us is the licence check-in, every 15 minutes: the server’s address, its version, the number of active accounts, the number of workers, which Pro features are in use and an instance id. Payload values whose keys look secret are masked in every view and export, and fields sealed with encrypted_property are shown as [encrypted]: the console holds no key to open them.
Can I write workers in languages other than Crystal?
Yes, in 8 more: Python, TypeScript, Ruby, Go, Rust, Java, PHP and .NET. Each SDK is a small client of the SDK gateway (/api/v1/sdk) of a self-hosted JoobQUI server: it registers the worker, long-polls for tasks, renews their leases and completes or fails them with a structured stack trace. Workers never connect to Redis. Jobs are Core; workflows need a JoobQ Pro key on the server, whose engine conducts them step by step.
Crystal stays first-class: the JoobQ and JoobQ Pro shards run jobs and workflows natively, without the gateway. The SDKs are not on package registries yet; copy sdks/<language> from the joobqui repository and reference it by path. Every language is tested against a real server.
How does JoobQ fit with Crystal and Azu?
JoobQ is a plain shard: require "joobq", define jobs as structs and start workers with JoobQ.forge. It works in any Crystal program; Crystal 1.22 or newer is required.
For Azu apps, the Azu CLI generates the JoobQ setup (azu generate joobq) and runs workers (azu jobs:worker); the JoobQUI console is itself an Azu application.
What happens if the console cannot reach the license service?
The server checks in every 15 minutes. If the license service cannot be reached, the last good license stays in force for up to 7 days after the server’s last successful check-in, as long as its paid-until date has not passed. A server that has not checked in once since it started gets no such grace.
After that, or when the license is expired, unpaid or disabled, the console’s operations routes of /api/v1 and /metrics answer 402 paid_license_required; sign-in and account routes keep working, and so does the SDK gateway (/api/v1/sdk/*), so workers keep running jobs. ./joobqui license:refresh checks in on demand.
How fast is it?
Throughput depends on payload size, TLS, Redis persistence and what your jobs do, so we publish no headline number. JoobQ Pro ships its benchmark harness (benchmarks/ in joobq-pro) and joobqui one for the SDK gateway (bench/sdk-gateway/). Measure completed jobs with your own workload.
Are jobs and activities exactly-once?
No. JoobQ delivers at least once: after a crash or redelivery a job, or a workflow activity, can run again. Each workflow attempt starts at most once, and ctx.dedupe_key gives activities a stable key to pass to services that support idempotency.