Que: background job processing for Elixir without Redis
Simple Job Processing in Elixir with Mnesia :zap:
At a glance
- What is it?
- Que is a job processing library for Elixir that stores job state in Mnesia, the database that ships with Erlang, so there is no external service to install for the queue itself. Workers are plain modules with a perform callback, and the roadmap shows what is still missing, including delayed jobs, cancellation, retries and multi node operation.
- Who is it for?
- Adopt Que if you are running Elixir on the BEAM and want a job queue that needs no extra infrastructure, particularly for a service where adding a Redis instance for the queue is more operational weight than you want. Do not adopt it if you need delayed jobs, job cancellation, job priority, automatic retries or multi node operation, because every one of those is an unchecked box on the project roadmap.
- 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 134 days ago.
- What is it written in?
- Mainly Elixir, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The queue lives in the database that ships with Erlang
Que is a job processing library for Elixir backed by Mnesia, which the readme describes as a distributed real-time database that comes with Erlang and Elixir. The point of that choice is stated plainly: it does not depend on any external service like Redis for persisting job state, which makes it easy to use since you do not need to install anything other than Que itself.
For an Elixir team, that is a real operational difference. The common shape of a background job library in other stacks is a second data store to run, monitor, back up and secure. Que removes that surface, and the job state lands in a database the runtime already knows how to operate.
The trade is Mnesia's own characteristics, and the readme is upfront about the default. Que runs out of the box, but by default all jobs are stored in memory. To persist jobs across application restarts you specify the database path in your config.exs:
config :mnesia, dir: ~c"mnesia/#{Mix.env()}/#{node()}" # Use the ~c"" sigilThen you run the mix task that creates the schema and the job database:
$ mix que.setupTwo details in that snippet are easy to miss. The dir value is scoped by Mix.env and by node, so development and production do not share a directory, and it uses the ~c charlist sigil rather than a string, which the inline comment flags. The readme also notes that for compiled releases where Mix is not available there is a separate setup path, since a release has no mix task to run.
Installation itself is the ordinary Hex dependency:
def deps do
[{:que, "~> 0.12.0"}]
endThe package is MIT licensed, published on Hex as que, with a single release v0.12.0 dated 2026-06-11 and the last push to the repository on the same day.
A worker is a module with a perform callback
The API deliberately resembles other Elixir job libraries such as Ku and Toniq, and it starts with the worker. You define a module, use Que.Worker, and implement perform:
defmodule App.Workers.ImageConverter do
use Que.Worker
def perform(image) do
ImageTool.save_resized_copy!(image, :thumbnail)
ImageTool.save_resized_copy!(image, :medium)
end
endAdding a job is one call, and it returns the job struct:
Que.add(App.Workers.ImageConverter, some_image)
#=> {:ok, %Que.Job{...}}The argument to a job can be any term, from a tuple to a keyword list to a struct, and the point of that is that the worker can pattern match on it. The readme shows a worker with three perform clauses that dispatch on a type key, using ordinary pattern matching and guard clauses:
defmodule App.Workers.NotificationSender do
use Que.Worker
def perform(type: :like, to: user, count: count) do
User.notify(user, "You have #{count} new likes on your posts")
end
def perform(type: :message, to: user, from: sender) do
User.notify(user, "You received a new message from #{sender.name}")
end
def perform(to: user) do
User.notify(user, "New activity on your profile")
end
endThis is a design choice with a clear opinion behind it. The job payload is not a blob to be unpacked inside the worker, it is a term the language can match on, so different kinds of work can share one worker module and the compiler enforces the shapes. Whether that suits you depends on whether you like the queue and the domain model sharing a language, which in Elixir is usually the reason people pick the language at all.
Concurrency, success and failure callbacks
By default all workers process one job at a time, and the concurrency option changes that per worker module:
defmodule App.Workers.SignupMailer do
use Que.Worker, concurrency: 4
def perform(email) do
Mailer.send_email(to: email, message: "Thank you for signing up!")
end
endThe default of one job at a time is worth pausing on. It means a worker that takes thirty seconds to process a job blocks that worker for thirty seconds, and it means you choose per worker how many of that kind of job may run at once. The readme notes elsewhere on the roadmap that making jobs work in parallel and allowing the number of concurrent jobs to be customised are both done, so this is a per worker setting rather than a global one.
Two optional callbacks handle the outcome of a job. on_success/1 and on_failure/2 are exported by the worker and receive the job arguments, with the failure callback also receiving the error:
defmodule App.Workers.ReportBuilder do
use Que.Worker
def perform({user, report}) do
report.data
|> PDFGenerator.generate!
|> File.write!("reports/#{user.id}/report-#{report.id}.pdf")
end
def on_success({user, _}) do
Mailer.send_email(to: user.email, subject: "Your Report is ready!")
end
def on_failure({user, report}, error) do
Mailer.send_email(to: user.email, subject: "There was a problem generating your report")
Logger.error("Could not generate report #{report.id}. Reason: #{inspect(error)}")
end
endThe pattern is familiar from the rest of the Elixir ecosystem, and the failure callback is where you decide what a failed job means. Logging it and moving on is the minimum. Retrying is not, and that is the point at which the roadmap becomes relevant.
Setup and teardown receive the job struct, not the arguments
A second pair of callbacks, on_setup/1 and on_teardown/1, runs before and after the job is performed, whether it succeeds or not. The difference that matters is their argument. Where perform and the success and failure callbacks receive the job arguments, the setup and teardown callbacks receive the job struct, which holds more internal detail and is described as useful for custom features such as logging, metrics, requeuing and more.
defmodule MyApp.Workers.VideoProcessor do
use Que.Worker
def on_setup(%Que.Job{} = job) do
VideoMetrics.record(job.id, :start, process: job.pid, status: :starting)
end
def perform({user, video, options}) do
User.notify(user, "Your video is processing, check back later.")
FFMPEG.process(video.path, options)
end
def on_teardown(%Que.Job{} = job) do
{user, video, _options} = job.arguments
link = MyApp.Router.video_path(user.id, video.id)
VideoMetrics.record(job.id, :end, status: job.status)
User.notify(user, "We've finished processing your video. See the results.", link)
end
endThe fields this gives you are the interesting part. The job id identifies the run, the pid is the operating system process running it, and the status is the outcome, so you can record a duration by pairing the setup and teardown events on the job id. In the example the teardown callback also destructures the arguments back out of the struct, which shows that the struct wraps the arguments rather than replacing them.
Since on_teardown runs whether the job succeeded or failed, it is the right place for the cleanup that must happen either way, and for the metrics record that closes the interval opened in on_setup.
The roadmap is a list of what the library cannot do yet
The most useful section of the readme for an adoption decision is the roadmap, because the unchecked boxes are an honest list of gaps.
Delayed jobs are not implemented, so there is no way to say run this in ten minutes. Job cancellation is not implemented. Job priority is not implemented, so there is no ordering other than the order work arrived. Multi node operation is not supported, and neither is recovering from node failures, which follows from that. Better job failures is on the list with two sub items, an option to set a timeout on workers and strategies to automatically retry failed jobs. A web UI is also on the list.
The checked boxes tell you what is done. Documentation, tests, persisting job state to disk, an API to interact with jobs, concurrency support, making jobs work in parallel, customising the number of concurrent jobs, success and failure callbacks, and a mix task for creating the Mnesia database. One checked item is worth a second look: find a more reliable replacement for Amnesia, which reads as the project having outgrown an earlier persistence approach and settled on Mnesia.
The multi node and retry gaps are the two that would make Que the wrong tool for a specific job. If your workload is a set of background tasks that must eventually succeed, the absence of automatic retry means you have to build the retry loop yourself in the failure callback, and multi node operation is explicitly listed as unsupported on the roadmap.
The persistence adapter question is also open, with Redis and Postgres listed as future work. So today the choice is Mnesia or nothing, and a team that would rather run Postgres already has one database, the flexibility is not there yet.
Editorial conclusion
Adopt Que if you are running Elixir on the BEAM and want a job queue that needs no extra infrastructure, particularly for a service where adding a Redis instance for the queue is more operational weight than you want. Do not adopt it if you need delayed jobs, job cancellation, job priority, automatic retries or multi node operation, because every one of those is an unchecked box on the project roadmap. Before you commit, run mix que.setup with a persisted Mnesia directory so jobs survive a restart, and read the worker documentation to see whether the callback model fits the jobs you need to run.
Frequently asked questions
What is Que and how does it store jobs?
Que is an Elixir background job processing library backed by Mnesia, the distributed real-time database that comes with Erlang and Elixir, so it does not depend on an external service like Redis for persisting job state. By default jobs are stored in memory, and a database path in config.exs plus the mix que.setup task makes them persist across restarts.
How do I define a worker in Que?
Create a module that uses Que.Worker and implement a perform callback, then add a job with Que.add(YourWorker, argument), which returns an ok tuple with the job struct. The argument can be any term, including a keyword list, and the worker can pattern match on it with several perform clauses.
Can Que workers run jobs concurrently?
By default each worker processes one job at a time, and the concurrency option changes that per worker, for example use Que.Worker, concurrency: 4. The roadmap lists both making jobs work in parallel and allowing the number of concurrent jobs to be customised as completed.
Does Que support delayed jobs, retries or job cancellation?
Not yet. The roadmap lists delayed jobs, job cancellation and job priority as not done, and better job failures as a pending item that includes an option to set a timeout on workers and strategies to automatically retry failed jobs. Multi node operation and recovery from node failures are also listed as not supported.
What are the on_setup and on_teardown callbacks for?
They run before and after the job is performed, whether it succeeds or not, and they receive the job struct rather than the job arguments. That gives access to internal details such as the job id, the pid and the status, which makes them useful for logging, metrics and requeuing.
Official sources
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.
[](https://hysenlabs.com/projects/sheharyarn-que)Was this useful?