Priority queues
Give a job a number. The lowest number runs first, and equal numbers keep the order you added them.
Add jobs to a queue. A pool of workers runs them. Keep the jobs in memory, or in Redis or SQLite so they survive a restart.
go get github.com/goptics/varmqgo get github.com/goptics/varmq github.com/goptics/redisqgo get github.com/goptics/varmq github.com/goptics/sqliteqEach scene shows the order varmq runs the jobs in, with the call that makes it. Typed results and errors, job IDs and deduplication, context cancellation, metrics, a REST API and Prometheus are in the guides.
Give a job a number. The lowest number runs first, and equal numbers keep the order you added them.
Bind many queues to one worker. A strategy decides which queue gives the next job: Priority, RoundRobin, MaxLen or MinLen.
Publish once and every subscribed worker gets its own job. A queue gives each job to one worker.
Press Restart process. A queue in memory ends with the process. A queue in Redis or SQLite keeps the waiting jobs.
Built in. Plain queues, priority queues and topics live in your process. They have nothing to set up, and they end when the process ends.
queue := worker.BindQueue()
// or, ordered by a number:
pq := worker.BindPriorityQueue()Use it when a lost job is fine, or when you can add it again.
sqliteq keeps jobs in local SQLite files, so a single process keeps them across a restart with no server to run.
sdb, err := sqliteq.New("data")
if err != nil {
panic(err)
}
defer sdb.Close()
pq, err := sdb.NewQueue("jobs")
if err != nil {
panic(err)
}
queue := worker.WithPersistentQueue(pq)A job that was running when the process died can run again. Write jobs so a second run is safe.
redisq keeps jobs in Redis. A persistent queue lets one process keep its jobs across a restart. A distributed queue lets several processes consume one queue, and each job goes to one of them.
rdb := redisq.New("redis://localhost:6375")
defer rdb.Close()
pq := rdb.NewQueue("persistent_queue")
queue := worker.WithPersistentQueue(pq)
// several processes: rdb.NewDistributedQueue(name)
// with worker.WithDistributedQueueA job that was running when the process died can run again. Write jobs so a second run is safe.
Each lane is one library on the same harness, same machine and same run. The longest bar is the highest value. The last column is the lane's value divided by VarMQ's.
Mean tasks per second over 15 rows: 3 task types × 5 producer shapes (1 to 1,000,000 producers), 50K workers, 1M tasks per run. Averaged over GOMAXPROCS 24, 8 and 4. Memory is bytes allocated per run of 1M tasks, from the same rows.
† Baselines, not libraries: N goroutines reading one channel. The buffered one has a channel that holds all 1M tasks.
100,000 no-op jobs through one library instance with 100 workers, Redis 7.2 on one pinned core. VarMQ runs on redisq. Median time from enqueue to handler, one job at a time: VarMQ 0.058 ms, the lowest of the run. taskq 0.062 ms, gocraft/work 0.082 ms, rmq 1.25 ms, Asynq 998 ms.
Drain 20,000 jobs added by 4 producers, WAL checkpoint every 1,000 pages. VarMQ runs on sqliteq. In this run VarMQ was the fastest subject in all 8 scenario and checkpoint groups.
Add never blocks, so waiting tasks sit in an unbounded queue.Loop10 rows, a plain buffered channel is fastest in all 15.Run of 2026-10-25 · varmq v1.10.0 · Intel Core i7-13700 (24 threads), linux/amd64, Go 1.26 · Absolute numbers vary between runs, so compare ratios within one run. Figures come from the raw output in github.com/goptics/benchmarks; charts and the harness are at benchmarks.goptics.org.
Press Run to run it in the Go Playground, or edit it first.
A worker with the default concurrency of 1 runs two jobs, one after the other. Read it in the docs
package main
import (
"fmt"
"time"
"github.com/goptics/varmq"
)
func main() {
worker := varmq.NewWorker(func(j varmq.Job[int]) {
fmt.Printf("Processing %d\n", j.Data())
time.Sleep(1 * time.Second)
}) // default concurrency is 1
defer worker.WaitUntilIdle()
queue := worker.BindQueue()
queue.Add(1)
queue.Add(2)
}
Tab inserts a tab. Esc leaves the editor. Ctrl+Enter runs.
Output
The worker starts after the three jobs are added, so the lowest number runs first. Read it in the docs
package main
import (
"fmt"
"github.com/goptics/varmq"
)
func main() {
// Not started, so all three jobs wait in the queue together.
worker := varmq.NewWorker(
func(j varmq.Job[string]) {
fmt.Println("running", j.Data())
},
varmq.WithAutoRun(false),
)
queue := worker.BindPriorityQueue()
queue.Add("report", 10)
queue.Add("alert", 1)
queue.Add("digest", 10)
worker.Start()
worker.WaitUntilIdle()
}
Tab inserts a tab. Esc leaves the editor. Ctrl+Enter runs.
Output
One worker takes jobs from three queues in turn. Read it in the docs
package main
import (
"fmt"
"github.com/goptics/varmq"
)
func main() {
worker := varmq.NewWorker(
func(j varmq.Job[string]) {
fmt.Println(j.Data())
},
varmq.WithStrategy(varmq.RoundRobin),
varmq.WithAutoRun(false),
)
alerts := worker.BindQueue()
reports := worker.BindQueue()
emails := worker.BindQueue()
alerts.Add("alert 1")
alerts.Add("alert 2")
reports.Add("report 1")
emails.Add("email 1")
emails.Add("email 2")
emails.Add("email 3")
worker.Start()
worker.WaitUntilIdle()
}
Tab inserts a tab. Esc leaves the editor. Ctrl+Enter runs.
Output
Two workers subscribe to one topic, and each gets every order. Read it in the docs
package main
import (
"fmt"
"time"
"github.com/goptics/varmq"
)
func main() {
topic := varmq.NewTopic[int]()
email := varmq.NewWorker(func(j varmq.Job[int]) {
time.Sleep(500 * time.Millisecond)
fmt.Println("email for order", j.Data())
})
stock := varmq.NewWorker(func(j varmq.Job[int]) {
time.Sleep(10 * time.Millisecond)
fmt.Println("stock for order", j.Data())
}, 2)
email.WithTopic(topic)
stock.WithTopic(topic)
for i := range 4 {
topic.Publish(i) // both workers get their own job for this order
}
email.WaitUntilIdle()
stock.WaitUntilIdle()
}
Tab inserts a tab. Esc leaves the editor. Ctrl+Enter runs.
Output
A result worker returns a value and an error, which you read from the job. Read it in the docs
package main
import (
"fmt"
"strconv"
"github.com/goptics/varmq"
)
func main() {
worker := varmq.NewResultWorker(func(j varmq.Job[string]) (int, error) {
return strconv.Atoi(j.Data())
})
queue := worker.BindQueue()
for _, s := range []string{"42", "forty-two"} {
job, _ := queue.Add(s)
n, err := job.Result()
fmt.Printf("%q: %d, %v\n", s, n, err)
}
}
Tab inserts a tab. Esc leaves the editor. Ctrl+Enter runs.
Output
go get github.com/goptics/varmqgo get github.com/goptics/varmq github.com/goptics/redisqgo get github.com/goptics/varmq github.com/goptics/sqliteqVarMQ is MIT licensed. Benchmark figures come from github.com/goptics/benchmarks.