# WakaQ: a deliberately small Redis-backed task queue for Python and Node

> WakaQ is a background task queue for Python backed by Redis, with queue priorities, ETA tasks, cron schedules, broadcast, soft and hard timeouts and retries, and a sibling TypeScript implementation that speaks the same protocol. The project deliberately refuses new features.

**wakatime/wakaq** — Background task queue for Python backed by Redis, a super minimal Celery

- Repository: https://github.com/wakatime/wakaq
- Website: https://pypi.org/project/WakaQ/
- Stars: 596 · Forks: 20
- Language: Python
- License: BSD-3-Clause
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/wakatime-wakaq

## A task queue that refuses to grow

WakaQ is described as a background task queue for Python backed by Redis, a super minimal Celery. The feature list is short: queue priority, delayed tasks that run after a timedelta eta, scheduled periodic tasks, tasks that can be async or normal synchronous functions, broadcast of a task to all workers, task soft and hard timeout limits, optionally retrying tasks on soft timeout, and ways to combat memory leaks with max_mem_percent or max_tasks_per_worker. The list ends with the words Super minimal.

And then there is the sentence that defines the project better than any feature list could: if you want more features like rate limiting or task deduplication, too bad, because feature pull requests are not accepted. Maximal features belong in your app's worker tasks.

That is a real design stance, and it is the reason the project also ships a comparison table of nine Python task queues and seven TypeScript ones, with a column for licence, broker and notes. Celery is listed as the industry standard with the most features and the biggest ecosystem, described as heavy to operate. WakaQ's own row describes it as a lightweight Celery alternative with cron and scheduled tasks, ETA, broadcast tasks, soft and hard timeouts and retries, and notes the polyglot pairing with wakaq-ts.

The primary language is Python, the licence is BSD-3-Clause, and the package is published on PyPI as wakaq.

## Why the queue compares itself to eight others

The comparison table is unusual and it is the most useful part of the documentation, because it tells you the trade-off you are making.

Looking at the Python side, arq is described as the best pick for asyncio-native code, small and well-built. TaskTiger is reliability-focused, with unique tasks, task locks, flexible retries, periodic tasks, subqueues and per-task forking, and it is production-proven at Close. RQ has the simplest sync mental model, with a big community and a thinner feature set. Dramatiq has a clean API and middleware, but its LGPL licence gets flagged by many corporate compliance reviews. TaskIQ is async-first with pluggable broker and result backends, but newer with a smaller community. huey is minimal and its SQLite mode avoids running Redis, and Procrastinate is there for teams that specifically want a Postgres broker.

On the TypeScript side, BullMQ is the de facto standard with flows, rate limiting and repeatable jobs, pg-boss is the best option for transactional enqueue with no Redis, Graphile Worker is high-throughput and LISTEN/NOTIFY-driven, and Bull is marked as legacy and superseded by BullMQ, with the note not to start new projects on it.

Reading these side by side makes the decision visible. Celery is what you pick when you need everything and can afford the operations. WakaQ is what you pick when the operational weight of Celery is the problem, and you are willing to write the missing pieces yourself.

## Polyglot by design, and why that is the differentiator

The first of the Why WakaQ reasons is polyglot by design. WakaQ works with Python and with TypeScript, speaking the same Redis queue protocol. The README makes the scenario concrete: enqueue a task from a Python service and run it on a Node worker, or the reverse. One Redis instance, one set of queues, two languages. And it states that no other task queue in the comparison can do this.

That claim is worth checking against the table, because it is the one differentiator that is not about feature count. The TypeScript rows are otherwise dominated by Postgres-based options and by BullMQ, and the one Redis entry in that column besides BullMQ and Bee-Queue is WakaQ-TS, described as lightweight with the same cron, ETA, broadcast and timeout feature set as WakaQ, and polyglot with wakaq.

Practically, this means a task payload has to survive a round trip between two languages and two runtime models. A Python worker enqueuing a job that a Node worker consumes has to agree on the serialization, the argument passing convention and the failure signalling, and both implementations of that protocol have to be kept in step. That is a real cost, and it is the cost you pay for not having to standardise on one language for the whole system.

## How the configuration model works

The example in the README is the documentation, and it is worth reading as a description of the configuration surface rather than as boilerplate.

Everything is set on one WakaQ object at import time. Queues are listed with their priorities, and the comment says queues can be defined as Queue instances, tuples, or just a str. The example uses tuples of priority and name for most, and a full Queue instance where the extra settings matter.

```python
    queues=[
        (0, Q_HIGH),
        (1, Q_MED),
        (2, Q_LOW),
        Queue(Q_OTHER, priority=3, max_retries=5, soft_timeout=300, hard_timeout=360),
        Q_DEFAULT,
    ],
```

The Queue instance carries its own retry and timeout settings, which is the per-queue override mechanism. A queue can therefore be slower and more tolerant than another, without every task having to repeat the values.

The timeouts are a two-level system and the difference matters. The soft timeout raises SoftTimeout or asyncio.CancelledError in the task if it runs longer than the limit, and the hard timeout sends SIGKILL. The example sets a soft timeout of 30 seconds and a hard timeout of one minute.

```python
    soft_timeout=30,  # seconds

    # SIGKILL a task if it runs longer than 1 minute. Can be set per task or queue.
    hard_timeout=timedelta(minutes=1),
```

Retries are resolved in a documented order. If the task soft timeouts, it retries up to the max retries value, and the max retries comes first from the task decorator if set, next from the Queue's max_retries, and lastly from the option below. If no max_retries is found, the task is not retried on a soft timeout.

## Two knobs for surviving a memory leak

The two memory-leak options are unusual enough to be worth their own look, because they encode an opinion about how worker processes should fail.

max_mem_percent is described as a way to combat memory leaks by reloading the worker, specifically the one using the most RAM, when the total machine RAM usage is at or equal to 98%. max_tasks_per_worker does the same thing after the worker has processed a fixed number of tasks, 5000 in the example.

The reasoning is straightforward and it is worth understanding because it changes what your tasks can do. A long-running worker process accumulates whatever your task code accumulates, and the standard alternatives are either to run short-lived workers or to have the framework restart them on a schedule. WakaQ offers both triggers and lets you pick, and the second one, a task count, is the useful one for a leak that scales with work done rather than with time.

The cost of the RAM-based trigger is that it measures the whole machine, not one worker, so on a shared host with other processes the threshold means something different than on a dedicated box.

There is also a concurrency model worth understanding. The concurrency option sets the number of worker processes and accepts an int or a str which evaluates to an int, and the variable cores is replaced with the number of processors on the current machine, so the example uses four times the core count. The async_concurrency option sets the number of concurrent asyncio tasks per worker process, with a default of zero meaning no limit.

## Cron schedules, the scheduler daemon, and the task decorators

Scheduled tasks are declared in the same WakaQ constructor, and the documentation is clear that running them requires a separate long-lived component: to run scheduled tasks you must keep a wakaq scheduler running as a daemon.

The schedules list accepts CronTask instances or tuples.

```python
    schedules=[

        # Runs mytask on the queue with priority 1.
        CronTask('* * * * *', 'mytask', queue=Q_MED, args=[2, 2], kwargs={}),

        # Runs mytask once every 5 minutes.
        ('*/5 * * * *', 'mytask', [1, 1], {}),
    ],
```

Tasks themselves are declared with a decorator, and per-task overrides exist for queue, retries and both timeouts.

```python
# timeouts can be customized per task with a timedelta or integer seconds
@wakaq.task(queue=Q_MED, max_retries=7, soft_timeout=420, hard_timeout=480)
def mytask(x, y):
    print(x + y)
```

The same decorator handles synchronous functions and coroutine functions, so a CPU-bound task and an IO-bound task are declared the same way, and async_concurrency is what separates them at runtime.

The setup.py lists the console entry points, which tells you what processes you actually run: wakaq-worker, wakaq-scheduler, wakaq-info and wakaq-purge. The worker consumes, the scheduler handles cron, info is presumably an inspection command and purge cleans up, and those four commands are the whole operational surface of the project.

## What you give up, and the small surface that is the point

The limitations are stated rather than hidden, which makes them easy to evaluate.

The first is the feature ceiling. Rate limiting, task deduplication, and similar things are simply not in scope, and the project says feature pull requests for them are not accepted. The reasoning is that maximal features belong in your app's worker tasks, so you implement rate limiting inside the task body or in front of the queue. That is a coherent position and a real constraint: if you need a burst limiter across a fleet of workers with shared state, you are writing it yourself, probably against Redis, and probably with a race condition in it.

The second is the Python version. setup.py declares python_requires of 3.14 or newer, and the classifiers list Python 3.14 only. That is a current, narrow floor. It is a deliberate choice for a modern codebase, but it means the package does not drop into a project pinned to 3.12 or 3.13.

The third is what the project calls super minimal as a feature, and it is the reason to adopt it. The stated reason is that you can read the entire source in an afternoon, so when something misbehaves you can understand exactly why instead of spelunking through a sprawling framework. Cron schedules, ETA tasks, broadcast tasks, timeouts and retries are all in a surface area small enough to keep in your head. When something goes wrong at three in the morning, that is worth a great deal.

The dependencies are correspondingly thin: click, croniter, psutil and redis, with psutil serving the memory measurement behind max_mem_percent.

## Conclusion

Adopt WakaQ when you want a task queue small enough to read in an afternoon, need both Python and TypeScript workers on the same Redis queues, or want to avoid a Celery deployment. Do not adopt it if you need rate limiting, task deduplication or a large plugin ecosystem, because the project states those feature pull requests are not accepted. Before you build on it, confirm your Python version meets the requirement of 3.14 or newer, and note the last push was on 2026-05-21 with release 4.0.6 on 2026-05-17.

## FAQ

### How do I install and use WakaQ?

Install it with pip install wakaq, then create a WakaQ object with your queues, concurrency and timeouts, and decorate task functions with @wakaq.task. Run the worker with the wakaq-worker command, and keep a wakaq scheduler running as a daemon for cron schedules.

### What is the difference between the soft and hard timeout in WakaQ?

The soft timeout raises SoftTimeout or asyncio.CancelledError in a task that runs too long, and the task can then be retried. The hard timeout sends SIGKILL to the task. Both can be set globally, per queue, or per task.

### Can WakaQ run both Python and TypeScript workers?

Yes. The project is polyglot by design, with a TypeScript implementation in wakaq-ts speaking the same Redis queue protocol, so a task enqueued from a Python service can be run by a Node worker and the reverse, over one Redis instance and one set of queues.

### Does WakaQ support rate limiting or task deduplication?

No. The project states that feature pull requests for rate limiting, task deduplication and similar capabilities are not accepted, and that maximal features belong in your app's worker tasks instead.

### How does WakaQ handle a worker that leaks memory?

Two options. max_mem_percent reloads the worker using the most RAM once total machine RAM usage reaches the threshold, 98% in the example, and max_tasks_per_worker reloads a worker after it has processed a fixed number of tasks, 5000 in the example.

## Sources

- [License: BSD-3-Clause](https://github.com/wakatime/wakaq/blob/main/LICENSE)
- [Project website](https://pypi.org/project/WakaQ/)
- [README](https://github.com/wakatime/wakaq/blob/main/README.md)
- [Releases](https://github.com/wakatime/wakaq/releases)
- [wakatime/wakaq on GitHub](https://github.com/wakatime/wakaq)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/wakatime-wakaq
