Library / SDK
maragudk/goqite avatar
maragudk/goqite

goqite puts a message queue in your SQLite table and lets you bring the driver

Go queue library built on SQLite and inspired by AWS SQS.

542 stars19 forksGoMIT

At a glance

What is it?
goqite is a Go library for a persistent message queue built on a single SQL table, inspired by AWS SQS but much simpler, with Postgres support, a job runner and an HTTP handler. It has no non-test dependencies, so you choose the SQL driver and own the schema.
Who is it for?
Adopt goqite if your Go service needs durable background work on infrastructure that already has a database, and you would rather own the schema and the SQL driver than run a broker. Do not adopt it if you need the throughput, fan-out or delayed delivery guarantees of a dedicated broker, because the whole design is one table and a poll loop.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 53 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One table, one queue name, one polling loop

goqite is described as a persistent message queue Go library built on SQLite and inspired by AWS SQS, but much simpler, and it also supports Postgres. The homepage is maragudk.github.io/goqite, the licence is MIT, and the primary language is Go.

The scope of what it does is worth stating before the details. The feature list is short. Messages are persisted in a single database table. Messages are sent to and received from the queue, and are guaranteed to not be redelivered before a timeout occurs. Multiple queues are supported in one table. Message timeouts can be extended for long-running tasks. There is a job runner abstraction on top of the queue, a simple HTTP handler, and no non-test dependencies, because you bring your own SQL driver.

That last point is the design decision everything else follows from. There is no client library for Redis or RabbitMQ underneath. The queue is a table in a database you already run, and the library issues SQL against it. That is what makes it appropriate for a small service or a single machine, and it is also what limits it.

The mechanics are familiar. You construct a queue with goqite.New and an options struct carrying the database handle and a queue name. You send a message with a body that is an arbitrary byte slice, so the payload format is yours to decide, and you can set a message delay. You receive a message, and during the receive the message is not available to other consumers until the timeout has passed. You then either delete it, or extend the timeout if processing needs longer.

The delete is not optional bookkeeping, it is the acknowledgement. A message that is not deleted is redelivered when its timeout expires, so at-least-once delivery falls out of the design rather than being an option you configure.

The schema is a file you copy, and you own it

The library does not create its table for you in a migration sense. The README ships a schema file per database flavour, and the example program reads schema_sqlite.sql from disk and executes it.

The SQLite schema is short, and it is where the whole model is visible.

sql
create table goqite (
  id text primary key default ('m_' || lower(hex(randomblob(16)))),
  created text not null default (strftime('%Y-%m-%dT%H:%M:%fZ')),
  updated text not null default (strftime('%Y-%m-%dT%H:%M:%fZ')),
  queue text not null,
  body blob not null,
  timeout text not null default (strftime('%Y-%m-%dT%H:%M:%fZ')),
  received integer not null default 0
) strict;

Six columns and a strict table. The id is generated in the database with a random blob, so there is no round trip to the application to get an identifier. The queue column is what separates multiple queues in one table, and the index on queue and created is what makes the poll query cheap. The timeout column is the redelivery deadline, and received is a counter of how many times the message has been handed out.

The trigger that keeps the updated column fresh is worth noticing, because it shows the library expects to modify rows.

sql
create index goqite_queue_created_idx on goqite (queue, created);

That index is what turns a scan of the table into a lookup of the oldest due message in a given queue. Everything else about the design follows from wanting that query to be cheap on a table you also use for other things.

For Postgres the README asks you to enable the pgcrypto extension.

sql
create extension if not exists pgcrypto;

The two schemas are not identical, and the difference is in how identifiers are generated, since the SQLite version uses hex(randomblob(16)) and the Postgres side needs a different function.

Postgres concurrency, and what read committed does not give you

The Postgres section of the README is the most detailed part of the documentation, and it is worth reading closely because it describes exactly what the concurrency guarantees are.

goqite runs its own PostgreSQL transactions at read committed isolation, and the receive query claims a message with for update skip locked. The effect is that concurrent receivers skip each other's rows instead of failing with serialization errors.

Skipping locked rows is the standard trick for letting several workers pull from one queue without blocking each other. The cost is stated immediately after, and it is a real limitation: skipping locked rows makes priority ordering best-effort under concurrent consumers. When another consumer holds the highest-priority row, the receiver takes the next message instead.

So if you are using the priority support added in v0.4.0, and you have many workers competing, the ordering you asked for is not the ordering you get. That is a deliberate trade of strict priority for throughput, and the README does not hide it.

The transaction methods are a separate case. The Tx methods such as SendTx and ReceiveTx run inside your own transaction, at whatever isolation level you set. At repeatable read or serializable, concurrent receives can still fail with serialization errors, and retrying them is up to you. ReceiveTx also keeps the message's row locked until your transaction ends, so the README advises keeping those transactions short.

That last sentence is the operational guidance: if you want the strong ordering guarantees of serializable isolation, you take on the responsibility of retrying serialization failures and you have to keep the transaction tight. The library will not do either for you.

The job runner turns the queue into background tasks

The jobs package sits on top of the queue and is what most people will actually use.

A job runner is created with jobs.NewRunner and an options struct carrying a Limit, a Log, a PollInterval and the Queue. Limit is the concurrency cap, so the runner will not process more than that many messages at once. The example sets it to 1, which means one job at a time, and a PollInterval of ten milliseconds, which is how often the runner checks the queue when it is idle.

Then you register handlers by name.

go
	r.Register("print", func(ctx context.Context, m []byte) error {
		fmt.Println(string(m))
		return nil
	})

And you create a job with that name.

go
	if err := jobs.Create(context.Background(), q, "print", []byte("Yo")); err != nil {
		log.Info("Error creating job", "error", err)
	}

The separation of job name and payload is the useful part. The runner routes by name, so the handler registry is a dispatch table and the body is whatever the job needs. The handler returns an error, and the natural consequence is that an error means the message is not deleted and gets redelivered after the timeout.

The runner also takes a context. In the example the context has a thirty millisecond timeout, so the example stops on its own, which is a tidy way to demonstrate a bounded run.

The HTTP handler is the other convenience the feature list mentions. It sits in an http/ directory at the top level of the repository, alongside the internal/ package and the jobs/ package, so the queue, the runner and the HTTP surface are three separate layers you can take or leave independently.

Installing it and running the queue for the first time

The module path is not the repository path, and that is the first thing to get right. The module was moved to maragu.dev/goqite in v0.3.0, and the README shows the install command.

shell
go get maragu.dev/goqite

The go.mod confirms it, with module maragu.dev/goqite and go 1.25.0 as the language version. The dependencies are listed in three groups: github.com/mattn/go-sqlite3 at v1.14.28, then github.com/jackc/pgx/v5 at v5.9.2 and maragu.dev/is at v0.3.0, then the indirect set including the pgx helper packages, golang.org/x/sync and golang.org/x/text.

Despite the feature list saying no non-test dependencies, the go.mod carries pgx and go-sqlite3, because the examples and the test suite import them. The library itself does not import a driver, which is the property that matters for you: you can use a driver that is not in this list at all.

A first run is the example program. It opens an in-memory SQLite database with the WAL journal, timeout and foreign key parameters set in the DSN, then limits the pool to a single open and single idle connection, which is the right shape for SQLite since it serialises writes anyway. It reads schema_sqlite.sql, executes it, creates a queue named jobs, sends a message with the body yo, receives it, prints the body, extends the timeout, and deletes the message.

The Makefile is where the test story lives, and it is worth knowing before you change anything.

make
test:
	go test -coverprofile cover.out -shuffle on ./...

The shuffle flag matters for a queue library, because tests that depend on ordering pass by luck otherwise. There are separate targets for coverage, lint with golangci-lint, and a benchmark target that runs go test with the cpu list 1,2,4,8,16, and test-up and test-down targets that bring the Postgres service from docker-compose up and down, since the compose file defines a postgres:17 service exposed on port 5433.

Where goqite is the wrong tool

Three cases, and the first is the one that decides most adoptions.

If you need the properties of a real broker, this is not one. There is no fan-out to multiple consumers, no dead letter queue, no visibility of queue depth as a first class concept, and no delivery guarantees stronger than at-least-once with a timeout. The queue is a poll loop against an index, which is exactly right for a service with a modest backlog and entirely wrong for a pipeline that needs to fan out to ten consumers or hold a million messages. AWS SQS is the stated inspiration, but the README says much simpler, and the differences are where the design was simplified.

The second is SQLite under real concurrency. The example sets the pool to a single connection, which is a deliberate concession to how SQLite works. If your workload has several processes writing to the queue at once, SQLite's single writer model becomes the bottleneck, and Postgres is the answer rather than a tuning exercise.

The third is priority correctness under parallel workers. The priority support is new, in v0.4.0, and the README is explicit that ordering is best-effort when concurrent consumers are involved. If your use case genuinely needs the highest priority message taken first, and you are running more than one worker, the guarantee you want is not the guarantee you get.

There is also the module path question, which is a small operational trap. The repository is maragudk/goqite on GitHub while the module is maragu.dev/goqite, and v0.3.0 was the release that moved it. An import of the old path will not resolve, and the README badges point at maragu.dev/goqite on pkg.go.dev, which is the current truth.

Editorial conclusion

Adopt goqite if your Go service needs durable background work on infrastructure that already has a database, and you would rather own the schema and the SQL driver than run a broker. Do not adopt it if you need the throughput, fan-out or delayed delivery guarantees of a dedicated broker, because the whole design is one table and a poll loop. Verify first that your Postgres setup is acceptable under the documented concurrency behaviour, because goqite runs at read committed isolation and only gives best-effort priority ordering, and check that the message timeout you choose is longer than the slowest job you will run against it.

Frequently asked questions

What is goqite?

goqite is a persistent message queue library for Go built on SQLite and inspired by AWS SQS, though much simpler. It also supports Postgres, persists messages in a single database table, and guarantees a message is not redelivered before its timeout expires.

How do I install goqite?

Run go get maragu.dev/goqite. The module path is maragu.dev/goqite rather than the GitHub repository path, since version v0.3.0 moved the module, and go.mod declares go 1.25.0.

Does goqite work with PostgreSQL?

Yes. You create the queue with SQLFlavor set to goqite.SQLFlavorPostgreSQL and use the provided Postgres schema. goqite runs its own transactions at read committed isolation and claims a message with for update skip locked, so concurrent receivers skip each other's rows rather than failing with serialization errors.

How does goqite guarantee a message is not processed twice?

A received message is not available to other consumers until its timeout has passed, and you can extend that timeout for long-running work with Extend. If you do not delete the message, it is redelivered after the timeout, so the delete call is the acknowledgement.

What does the jobs package add to goqite?

It provides a job runner on top of the queue for background tasks, with a concurrency Limit, a Log, a PollInterval and a Queue. You register handlers by name with Register and create jobs with jobs.Create, so routing is by job name and the body stays whatever payload the job needs.

Official sources

  1. License: MIT
  2. maragudk/goqite on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/maragudk-goqite.svg)](https://hysenlabs.com/projects/maragudk-goqite)

Was this useful?