A durable, idempotent job queue for Go with rate limiting, priority, backpressure, and fair scheduling built in — a small library plus an optional HTTP service. Standard library only; no third-party dependencies.
The problem it solves: a naive in-memory channel loses work on crash, redelivers nothing, has no retry policy, and lets one noisy producer starve everyone else. safequeue gives you at-least-once durability and the admission-control machinery you normally have to bolt on afterwards — in one dependency-free package.
- At-least-once, durable. Every state change is appended to a log and
fsync'd; reopening the data directory replays the log to rebuild state. A torn final record (partial write before a crash) is tolerated. - Idempotent enqueue. Enqueuing the same idempotency key twice returns the original message instead of creating a duplicate.
- Priority delivery. Enqueue with a priority; higher priority is dequeued first, FIFO within a band. Default priority 0 reduces to plain FIFO.
- Backpressure. Bounded capacity with pluggable policy: fail fast
(
ErrBackpressure/ HTTP 429), shed the oldest, or block with a context. - Rate limiting. Thread-safe token-bucket and leaky-bucket limiters with
Allow/Wait(ctx)/Reservesemantics and an injectable clock. - Fair scheduling. Deficit Round Robin across per-tenant keys so a flood from one producer cannot starve others; weights control relative throughput.
- Observability. A tiny metrics interface with an
expvaradapter, plusDepth()and backpressure counters inStats().
Maintainer: Cognis Digital · License: COCL 1.0
go get github.com/cognis-digital/safequeue/queueModule: github.com/cognis-digital/safequeue (Go 1.22+). Cross-platform
(Windows, macOS, Linux). Build the service binary with the Makefile,
install.sh (macOS/Linux), or install.ps1 (Windows); a Dockerfile is
provided for containers.
| Import path | What it provides |
|---|---|
.../safequeue/queue |
Durable, idempotent at-least-once queue + priority + backpressure |
.../safequeue/ratelimit |
Token-bucket and leaky-bucket limiters |
.../safequeue/scheduler |
Deficit Round Robin fair scheduler |
.../safequeue/metrics |
Minimal metrics interface + expvar adapter |
.../safequeue/config |
JSON config loader + validation (schema in config/) |
.../safequeue/httpapi |
JSON/HTTP service |
package main
import (
"fmt"
"log"
"github.com/cognis-digital/safequeue/queue"
)
func main() {
q, err := queue.Open("./data", queue.Options{
Capacity: 10000, // backpressure at 10k live messages
BackpressurePolicy: queue.PolicyReject, // fail fast when full
})
if err != nil {
log.Fatal(err)
}
defer q.Close()
// Producer: enqueue with an idempotency key and a priority.
msg, created, err := q.EnqueueWith("send-welcome:user-42", "user-42",
queue.EnqueueOptions{Priority: 5, FairnessKey: "tenant-a"})
if err == queue.ErrBackpressure {
return // queue full: slow down / retry
}
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.ID, "created:", created)
// Consumer: lease a message, process it, then ack or nack.
m, err := q.Dequeue()
if err == queue.ErrEmpty {
return
}
if processOK(m) {
q.Ack(m.ID)
} else {
q.Nack(m.ID) // retry with backoff, or dead-letter if exhausted
}
}
func processOK(*queue.Message) bool { return true }import "github.com/cognis-digital/safequeue/ratelimit"
// 500 events/sec sustained, bursts up to 100.
tb := ratelimit.NewTokenBucket(500, 100, ratelimit.RealClock{})
if tb.Allow() { /* proceed now */ }
_ = tb.Wait(ctx) // block until a token is available or ctx is done
r := tb.Reserve() // commit a slot; r.Delay() says how long to waitLeakyBucket has the same shape but enforces a strictly even output rate
instead of permitting bursts.
import "github.com/cognis-digital/safequeue/scheduler"
d := scheduler.New(1) // quantum 1 => per-item interleaving
d.SetWeight("premium", 3) // 3x throughput of a weight-1 tenant when both busy
d.Add("premium", job)
d.Add("free", job)
item, _ := d.Next() // returns the next item under DRR fairness| Field | Default | Meaning |
|---|---|---|
VisibilityTimeout |
30s |
How long a leased message stays invisible before redelivery. |
MaxAttempts |
5 |
Delivery attempts before a message is dead-lettered. |
BaseBackoff |
1s |
First retry delay; doubles each attempt up to MaxBackoff. |
MaxBackoff |
5m |
Upper bound on the backoff delay. |
Capacity |
0 |
Bounded live (ready+leased) messages; 0 = unbounded. |
BackpressurePolicy |
Reject |
At capacity: Reject (429), DropOldest (shed), Block. |
Metrics |
no-op | A metrics.Recorder sink; injectable for expvar/OTel. |
Clock |
real | Time source; injectable for deterministic tests. |
Real numbers from go test -bench on this machine — AMD Ryzen 9 5950X,
Go 1.26.4, windows/amd64. Reproduce with make bench. Your hardware and disk
will differ; the durable enqueue path is dominated by fsync, so it reflects
your storage, not just CPU.
| Benchmark | ns/op | Notes |
|---|---|---|
TokenBucket.Allow |
~13 ns/op | 0 allocs; ~76M ops/sec single-threaded |
TokenBucket.Allow (parallel) |
~45 ns/op | under contention across 32 goroutines |
LeakyBucket.Allow |
~13 ns/op | 0 allocs |
scheduler.DRR Add+Next |
~190 ns/op | 1 alloc; 8 keys |
queue.Enqueue (durable, fsync) |
~1.45 ms/op | fsync-bound (~690 durable enqueues/sec on this SSD) |
The limiter and scheduler are CPU-bound and fast. The durable enqueue is
deliberately fsync-per-op for crash safety; batch/group-commit is a documented
roadmap item (see issues) for callers who want higher durable throughput.
go run ./cmd/safequeue -addr :8080 -data ./data -capacity 10000 -backpressure reject
# or from a config file:
go run ./cmd/safequeue -config ./config/config.example.json -metricsFlags: -addr, -data, -visibility, -max-attempts, -base-backoff,
-max-backoff, -capacity, -backpressure (reject|drop_oldest|block),
-config, -metrics.
| Method & path | Request body | Response |
|---|---|---|
POST /enqueue |
{"payload":"...","idempotency_key":"...","priority":N,"fairness_key":"..."} |
{"message":{...},"created":bool} (429 if at capacity) |
POST /dequeue |
(none) | 200 leased message, or 404 if empty |
POST /ack |
{"id":"..."} |
{"ok":true} (404/409 on misuse) |
POST /nack |
{"id":"..."} |
{"dead":bool} (404/409 on misuse) |
GET /stats |
(none) | counts by state + backpressure counters |
GET /depth |
(none) | {"current":N,"high_water":N} |
GET /healthz |
(none) | {"ok":true} |
GET /debug/vars |
(none, only with -metrics) |
expvar dump incl. safequeue metrics |
Status codes: 400 invalid/missing fields, 404 unknown id or empty queue,
405 wrong method, 409 message not leased, 429 backpressure (at capacity).
config/config.schema.json is a JSON Schema (Draft 2020-12) for the config
file; config/config.example.json is a working example. The loader rejects
unknown fields and validates value ranges and cross-field invariants (e.g.
base_backoff must not exceed max_backoff).
- Enqueue adds a
readymessage. A non-empty idempotency key that already maps to a live message returns that message (no duplicate). IfCapacityis set and reached,BackpressurePolicydecides: reject, drop-oldest, or block. - Dequeue picks the highest-priority
readymessage whose visibility time has passed (FIFO within a priority band), leases it forVisibilityTimeout, incrementsAttempts, and returns a copy. - Ack deletes the leased message; its idempotency key is freed.
- Nack schedules a retry after exponential backoff, or dead-letters the
message once
AttemptsreachesMaxAttempts. - Visibility timeout. A leased message not acked/nacked before its lease
expires is reclaimed to
ready(at-least-once — make consumers idempotent).
See docs/ARCHITECTURE.md for the layered design
(durable core + rate/priority/backpressure/fairness), the fairness algorithm,
and the storage/wire format.
Runnable demos under examples/ (each exits 0):
go run ./examples/priority # priority-ordered delivery
go run ./examples/ratelimit # token-bucket smoothing a burst
go run ./examples/fairness # DRR fair scheduling / no starvationmake build # go build ./...
make test # go test ./... -race -count=1 (requires cgo for -race)
make bench # go test -bench . -benchmem
make lint # gofmt -l . (must be empty) + go vet ./...Tests inject a fake clock and use millisecond backoffs, so the suite never sleeps on the real wall clock (the limiter/scheduler tests are fully deterministic).
License: COCL 1.0. See the canonical COCL text distributed with Cognis Digital projects.