Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

REST API and Admin UI

arbiter-servant and arbiter-servant-ui provide the REST API and admin dashboard, as standalone WAI applications or as routes in an existing Servant API.

import Arbiter.Servant qualified as Servant
import Arbiter.Simple (createSimpleEnv, runSimpleDb)

env <- createSimpleEnv (Proxy @AppRegistry) connStr "arbiter"
config <- Servant.createArbiterServer (runSimpleDb env)
Servant.runArbiterAPI 8080 config

The server runs over any backend. Pass runHasqlDb env for a hasql application. The SSE stream uses the env's listener.

Embed as a sub-route in an existing Servant application:

import Arbiter.Servant qualified as Servant
import Arbiter.Servant.UI qualified as ServantUI

type MyAPI =
  "api" :> MyBusinessRoutes
    :<|> "arbiter" :> (Servant.ArbiterAPI AppRegistry :<|> ServantUI.AdminUI)

Server and route types: arbiter-servant Haddocks. UI route type: arbiter-servant-ui Haddocks.

POST jobs and POST jobs/batch enqueue from any language.

OpenAPI

arbiter-servant-openapi serves an OpenAPI 3 description of the API at /openapi.json:

import Arbiter.Servant (ArbiterAPI, arbiterServer, createArbiterServer)
import Arbiter.Servant.OpenApi (OpenApiAPI, openApiServer)
import Arbiter.Servant.UI (AdminUI, adminUIServer)

type MyApp = ArbiterAPI AppRegistry :<|> OpenApiAPI :<|> AdminUI

config <- createArbiterServer (runSimpleDb env)
run 8080 $
  serve (Proxy @MyApp) (arbiterServer config :<|> openApiServer @AppRegistry "1.0.0" :<|> adminUIServer)
RequirementDetail
payload and result typesa ToSchema instance. deriving anyclass (ToSchema) works for generic JSON
Value payloadsthe module supplies the instance
registrygiven to openApiServer by type application
document versiongiven to openApiServer as info.version. The document describes your queues and payloads, so you own its version

[!NOTE] Mount OpenApiAPI before AdminUI. AdminUI is a Raw route that matches every path.

openApiSpec @AppRegistry "1.0.0" returns the same document as a Value. Module: arbiter-servant-openapi Haddocks.

Endpoints

Per-queue endpoints under /api/queues/:queue/:

MethodPathDescription
GETjobsList jobs. payload searches the payload text
POSTjobsInsert a job
POSTjobs/batchInsert multiple jobs
GETjobs/:idGet a job by ID
DELETEjobs/:idCancel a job and delete its children
POSTjobs/:id/force-cancelCancel a job, delete its children, and interrupt the running handler
POSTjobs/:id/promoteMake a delayed job immediately visible
POSTjobs/:id/rescheduleSet when a job becomes visible
POSTjobs/:id/move-to-dlqMove a job to the dead-letter queue
POSTclaimLease visible jobs and return each job with its lease
POSTjobs/:id/ackComplete a job the lease still holds and store its result
POSTjobs/:id/nackHand a job back without spending its attempt
POSTjobs/:id/extendExtend a held lease
POSTjobs/:id/suspendSuspend a job
POSTjobs/:id/resumeResume a suspended job
POSTjobs/:id/pause-childrenPause all visible children of a job
POSTjobs/:id/resume-childrenResume all suspended children
GETdlqList DLQ entries. payload and error search the payload and the last error
POSTdlq/:id/retryRetry a job from the DLQ, optionally with a new payload
DELETEdlq/:idDelete one DLQ entry
POSTdlq/batch-deleteDelete multiple DLQ entries
POSTdlq/batch-retryRetry multiple DLQ entries
POSTdlq/retry-matchingRetry every DLQ entry the dlq list filters match. No filters retries the whole DLQ
GETarchiveList archived jobs. payload searches the payload text
POSTarchive/:id/reenqueueRe-enqueue an archived job as a new job, optionally with a new payload
DELETEarchive/:idDelete one archive entry
POSTarchive/batch-deleteDelete multiple archive entries
GETstatsQueue statistics
GETkindsList the kind labels the queue declares
GETgroupsList the queue's open groups, largest first
GETdetailsGet the queue's pause state and DLQ retention
POSTpausePause the queue (all workers stop claiming)
POSTresumeResume a paused queue
PUTdlq-retentionSet the queue's DLQ retention. {"retentionSeconds": null} keeps entries

Global endpoints under /api/:

MethodPathDescription
GETqueuesList all registered queues
GETqueues/statsStatistics for every registered queue
GETevents/streamSSE stream for real-time notifications
GETcron/schedulesList cron schedules
PATCHcron/schedules/:nameOverride a schedule's expression, overlap policy, time zone, or enabled state
POSTcron/schedules/:name/runRun an enabled schedule once, out of band
GETworkersList registered workers
POSTworkers/:id/pausePause a single worker
POSTworkers/:id/resumeResume a single worker
GETrate-limitsList policies with bucket and throttle stats
GETrate-limits/:prefix/bucketsList a prefix's per-key buckets
PATCHrate-limits/:prefixSet or clear a policy's override params
POSTrate-limits/:prefix/resetReset a prefix's buckets
POSTrate-limits/:prefix/buckets/:key/tokensAdd tokens to one bucket and wake its throttled jobs
POSTrate-limits/pruneDelete full buckets idle for idle seconds
GETconcurrencyList concurrency policies with limit and in-flight stats
GETconcurrency/:prefix/keysList a policy's per-key in-flight counts
PATCHconcurrency/:prefixSet or clear a policy's override limit
POSTconcurrency/reconcileRepair the in-flight counts of every policy
POSTconcurrency/pruneDelete drained keys that have no live job
POSTmaintenanceRun one gated maintenance pass
GEThealthReadiness check. Returns 503 when the database is unavailable
GEThealth/liveLiveness check. Does not query the database

Operating jobs

dlq/:id/retry and archive/:id/reenqueue take an optional replacement payload:

POST /api/queues/email_queue/dlq/12/retry
Content-Type: application/json

{"payload": {"tag": "SendWelcome", "contents": "alice@example.com"}}
BodyEffect
emptythe stored payload runs again
payloadthe new payload runs. Its kind, rate-limit and concurrency keys come from it
a payload the queue's type rejects400. Nothing changes
a body with no Content-Type400. Nothing changes
a body with a content type that is not application/json415. Nothing changes

jobs/:id/reschedule sets when a job becomes visible:

POST /api/queues/email_queue/jobs/41/reschedule
Content-Type: application/json

{"runAt": "2026-10-01T09:00:00Z"}

An in-flight, suspended, cancelled or exhausted job returns 409.

Consuming over HTTP

POST claim is the worker-pool claim: it spends admission tokens, increments the attempt count, records a claimant, and hides each job for the lease. A paused queue returns no leases.

POST /api/queues/email_queue/claim
{"maxJobs": 5, "leaseSeconds": 60}

maxJobs defaults to 1 and clamps to 1000. leaseSeconds defaults to 60 and clamps to 3600.

claimSeq and claimedBy in each returned job identify the lease. Every finalization request carries them:

POST /api/queues/email_queue/jobs/41/ack
{"claimSeq": 7, "claimedBy": "0f5e...c31"}

On a QueueWithResult queue, ack takes the result:

POST /api/queues/email_queue/jobs/41/ack
{"claimSeq": 7, "claimedBy": "0f5e...c31", "result": ["delivered"]}

The result is stored as ackWith stores it. A body that does not match the result type returns 400. Omit result to store none. The route needs FromJSON and ToJSON on the result type.

RouteEffect
ackcompletes the job
nackrefunds the attempt. The job stays invisible for the rest of the lease.
extendmoves the lease expiry to leaseSeconds from now, at most 3600

A mismatched lease, a lease that a registered worker holds, a suspended job, or a force-cancelled job returns 409.

The server does not renew an HTTP lease. After it expires another consumer can claim the job.

[!IMPORTANT] These routes have no authentication. Protect them with WAI middleware, a Servant authentication combinator, or an authenticating proxy.

Maintenance

POST maintenance runs one pass over stale workers, exhausted and cancelled jobs, rate-limit buckets, concurrency counts, archive and DLQ retention, and group summaries. Each operation runs in one caller at a time.

Server settingMeaning
maintenanceIntervalminimum gap between runs of one operation (default: none)
maintenanceSparseIntervalminimum gap between runs of the bucket prune and the concurrency reconcile-and-prune
maintenanceTimeoutstatement timeout
maintenanceBucketIdleidle time before a rate-limit bucket is removed

The response lists the affected row count per completed operation and the names of failed operations. Skipped operations appear in neither:

{"ops": {"sweep-stale-workers": 2, "purge-archives": 140}, "failed": []}

Worker pools run the same pass in their reaper. A pass covers every queue the migrations created in the schema, including queues outside the caller's registry. The endpoint supports deployments without worker pools.