A simple, reliable, distributed job scheduler — delayed tasks, cron, retries, and at-least-once delivery, in a single import.
Reliable primitives that survive crashes, redeploys, and load — without a heavyweight framework.
Every task runs to completion. Idempotent handlers are the only contract.
Run later with ProcessIn, or on a schedule with standard cron specs.
Exponential backoff with jitter; exhausted tasks land in an archive you can replay.
Visibility-timeout leases reclaim tasks from workers that die mid-flight.
Weighted, strict, or paused — route critical work ahead of the rest.
Suppress duplicate enqueues with Unique; cancel running tasks remotely.
Processed, failed, retried, in-flight, and latency — exported out of the box.
Inspect queues, list states, requeue, pause, and cancel from your terminal.
Just Redis. No broker to operate, no result backend to wire up.
A client enqueues, a server processes, a scheduler fires cron. Point them all at the same Redis.
client := relay.NewClient(relay.RedisClientOpt{Addr: "127.0.0.1:6379"})
defer client.Close()
// fire-and-forget
client.Enqueue(relay.NewTask("email:send", []byte(`{"to":"a@b.com"}`)))
// run in 10 minutes, retry up to 5 times
client.Enqueue(
relay.NewTask("report:build", []byte(`{"month":"june"}`)),
relay.ProcessIn(10*time.Minute),
relay.MaxRetry(5),
)
srv := relay.NewServer(relay.RedisClientOpt{Addr: "127.0.0.1:6379"}, relay.Config{
Concurrency: 10,
Queues: map[string]int{"critical": 6, "default": 3, "low": 1},
})
mux := relay.NewServeMux()
mux.HandleFunc("email:send", func(ctx context.Context, t *relay.Task) error {
return sendEmail(t.Payload())
})
srv.Run(mux) // blocks; drains gracefully on SIGTERM
scheduler := relay.NewScheduler(relay.RedisClientOpt{Addr: "127.0.0.1:6379"}, nil)
// run every hour, on the hour
scheduler.Register("0 * * * *", relay.NewTask("report:build", nil))
// safe to run several instances — duplicate fires collapse to one
scheduler.Run()
Every feature is an option or a one-liner. Mix and match per task.
// reject identical tasks within the window
client.Enqueue(task, relay.Unique(time.Hour))
client.Enqueue(task,
relay.Timeout(30*time.Second),
relay.Queue("critical"),
)
mux.HandleFunc("charge", func(ctx context.Context, t *relay.Task) error {
if invalid(t) {
return relay.SkipRetry // straight to the archive
}
return charge(t)
})
insp := relay.NewInspector(relay.RedisClientOpt{Addr: "127.0.0.1:6379"})
insp.CancelTask(taskID) // cancels the handler's ctx
http.Handle("/metrics", relay.MetricsHandler())
go http.ListenAndServe(":2112", nil)
relay stats
relay ls default retry
relay run default <task-id> # requeue
relay cancel <task-id>
relay pause low
relay is a library, not a daemon — run the worker inside your app, or as its own service. Same code, your call.
// main.go — your API also runs the workers
client := relay.NewClient(opt)
srv := relay.NewServer(opt, relay.Config{Concurrency: 10})
srv.Start(mux) // non-blocking
http.ListenAndServe(":8080", api(client))
Simplest deploy. One binary, one rollout. Jobs share CPU and memory with request handling — ideal for dev and small services.
// cmd/worker/main.go — this binary only runs jobs
srv := relay.NewServer(opt, relay.Config{Concurrency: 20})
srv.Run(mux) // blocks; drains on SIGTERM
// your API process only enqueues:
// client := relay.NewClient(opt)
Scale & isolate independently. Add worker replicas without touching the web tier; a job leak can't take down request handling. Recommended at scale.
Both point at the same Redis. Start embedded, then graduate to a dedicated worker when you need independent scaling or isolation — no rewrite, just a different main. Unlike Celery, the separate process is an option, not a requirement.
Redis lists and sorted sets, mutated by atomic Lua scripts. Nothing is lost when a worker dies.
{q}:pendinglist of ready task IDs{q}:scheduledzset by run-at — delayed jobs{q}:leasezset by expiry — crash recovery{q}:retryzset by next-attempt time{q}:archivedretry-exhausted dead letters{q}:unique:*NX locks for dedupThe same family as Celery and asynq — a lean, Go-native, Redis-only slice of it.
| Capability | relay | Celery |
|---|---|---|
| Language | Go (embedded library) | Python (worker processes) |
| Broker | Redis | Redis / RabbitMQ |
| Delayed + cron | ✓ | ✓ |
| Retries + dead-letter | ✓ | ✓ |
| At-least-once delivery | ✓ | ✓ |
| Multi-instance scheduler, no leader | ✓ | beat is single |
| Result backend | — | ✓ |
| Workflow chains / chords | — (roadmap) | ✓ |