janus is one queue with a synchronous and an asynchronous face
Thread-safe asyncio-aware queue for Python
At a glance
- What is it?
- janus is a thread-safe queue for Python built for the case where classic threaded code has to talk to asyncio code. The queue object exposes two interfaces over the same data, one fully compatible with the standard library queue and one that follows the asyncio queue design, which lets a producer thread hand work to an event loop consumer without either side knowing about the other.
- Who is it for?
- janus fits exactly one job, passing work from a blocking thread into an event loop without converting either side, and it does that job with a typed API, three queue flavours and a clear shutdown contract.
- Can I use it commercially?
- Yes. Apache-2.0 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 81 days ago.
- What is it written in?
- Mainly Python, 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
The two faced queue
The library describes itself as a mixed sync-async queue, supposed to be used for communicating between classic synchronous, threaded, code and asynchronous code in terms of asyncio. The naming is the explanation, because like the Janus god the queue object has two faces, a synchronous and an asynchronous interface. The synchronous side is fully compatible with the standard library queue, and the asynchronous side follows the asyncio queue design, including the task_done convention and the join semantics that the standard queue has. That compatibility is the point of the library rather than an incidental detail. If the synchronous face were a different API, you would have to rewrite the threaded producer, and the entire value of the library would be lost. The development was sponsored by DataRobot.
Three queue types, two properties
Three queues are available, Queue, LifoQueue and PriorityQueue, and each has two properties, sync_q and async_q. You use the first for the synchronous interface and the second for the asynchronous one, and the two properties point at the same underlying queue rather than at two queues that have to be kept in step. The type annotations in the documentation show how the two sides are typed separately, with a janus.SyncQueue parameter for the threaded function and a janus.AsyncQueue parameter for the coroutine, so a static type checker can tell you that a side is being used correctly. The three flavours mirror the standard library, so a last-in-first-out queue and a priority queue are available in the same mixed sync-async form, which is what makes the library usable for more than a single work queue.
A thread produces, a coroutine consumes
The worked example is the shape you will write in practice. The threaded producer puts a hundred integers into the synchronous face and then waits for the queue to drain:
import asyncio
import janus
def threaded(sync_q: janus.SyncQueue[int]) -> None:
for i in range(100):
sync_q.put(i)
sync_q.join()
async def async_coro(async_q: janus.AsyncQueue[int]) -> None:
for i in range(100):
val = await async_q.get()
assert val == i
async_q.task_done()The main coroutine creates the queue, runs the threaded function in an executor so it does not block the loop, and consumes from the asynchronous face:
async def main() -> None:
queue: janus.Queue[int] = janus.Queue()
loop = asyncio.get_running_loop()
fut = loop.run_in_executor(None, threaded, queue.sync_q)
await async_coro(queue.async_q)
await fut
await queue.aclose()
asyncio.run(main())Two details in that second half are the ones people forget. The producer is handed to run_in_executor with the default executor, because a blocking put has to happen off the event loop thread. And the last line is aclose, which is the shutdown contract the library requires.
You have to close the queue
The first limitation is about lifetime, and it is the one that will bite you. Once you are done working with a queue, you must properly close it using aclose(). The reason given is mechanical: the library creates new tasks to notify other threads, so if you do not properly close the queue, asyncio may generate error messages, with a link to the issue where that behaviour is discussed. That is a real design consequence of the classic thread-safe design the library is built on. The notification mechanism between a thread that has put an item and an event loop that is waiting for one is implemented with tasks, and a task that is never resolved is a task the loop complains about at shutdown. The fix is easy to apply and easy to forget, so the rule for a codebase is to close every janus queue, which is a different discipline from the standard queue where forgetting close() costs nothing.
Using it for the wrong thing is slow
The second limitation is about performance, and the documentation is unusually candid about it. The library has quite good performance only when used as intended, that is, for communication between synchronous code and asynchronous one. For sync-only and async-only cases, use the queues from the standard queue and asyncio queue modules, otherwise the slowdown can be significant. The reason is structural. Every put from one side has to wake up a waiter on the other side, and that involves creating and scheduling a task, which is exactly the machinery that a plain queue does not need. If you are not crossing the sync and async boundary, you are paying for the boundary and getting nothing back. The third limitation is about event loops: you cannot use queues for communicating between two different event loops, because, like all asyncio primitives, they bind to the current one. A janus queue belongs to the loop that created it, which is a property shared with every asyncio primitive but one that bites when a queue is passed to a thread that starts its own loop.
Stable, maintained, and explicitly finished
The development status is production and stable, and the next sentence sets the expectations. The library is maintained to support the latest versions of Python and fixes, but no major changes will be made. That is a clear signal to a user, and it is paired with a recommendation. If your application is performance-sensitive, or if you need any new features such as anyio support, try the experimental culsans library as an alternative. So the project is not abandoned and not growing, it is in maintenance with a named successor for the cases it does not want to serve. The version history matches that, with 2.0.0 published on 2024-12-13, 1.2.0 the day before on 2024-12-12 and 1.1.0 on 2024-10-30, and the last push to the repository on 2026-08-03. The project is not archived, so the fixes do keep coming.
The tooling around a small package
The repository is a small Python package with a thorough quality setup. The Makefile shows the whole routine. The develop target installs the package in editable mode with setup.py develop, the lint target runs flake8 over the package and the tests, and the test target depends on both flake and develop before running pytest. There is a verbose test variant, a format target that runs isort and then black, and a coverage target that runs pytest with coverage over the package and the tests and prints the path of the HTML report. The remaining targets are the ones worth noting for a library: checkrst validates the reStructuredText, pyroma checks the package metadata, bandit runs a security scan over the package, and mypy runs in strict mode over the package. The strict mypy setting is why the example is annotated as heavily as it is, and the repository also carries a.coveragerc, a.style.yapf, a setup.cfg, a requirements-dev.txt and a CHANGES.rst. The licence is Apache 2, questions go to GitHub Discussions, and there is a Gitter lobby for the aio-libs group.
Editorial conclusion
janus fits exactly one job, passing work from a blocking thread into an event loop without converting either side, and it does that job with a typed API, three queue flavours and a clear shutdown contract. It does not fit a project that only needs one style of queue, because the documentation is explicit that the slowdown in that case can be significant and that the standard queue or the asyncio queue is the better choice, and it does not fit a codebase that needs to share a queue between two different event loops. Before you adopt it, read the limitations section, because the requirement to call aclose() on every queue is a real obligation rather than a style preference, and check the version, since 2.0.0 was published on 2024-12-13 and the last push was on 2026-08-03 with the project explicitly not planning major changes.
Frequently asked questions
What is the janus library?
It is a mixed sync-async queue for Python, intended for communicating between classic threaded code and asyncio code, where the queue object has a synchronous interface fully compatible with the standard library queue and an asynchronous interface following the asyncio queue design.
What queue types does janus provide?
Queue, LifoQueue and PriorityQueue. Each exposes two properties, sync_q for the synchronous interface and async_q for the asynchronous one, and both point at the same underlying queue.
Do I have to close a janus queue?
Yes. Once you are done with a queue you must close it with aclose(), because the library creates new tasks to notify other threads, and leaving the queue open can make asyncio generate error messages.
When should I not use janus?
For sync-only or async-only code, where the standard library queue or the asyncio queue is faster and the slowdown with janus can be significant. You also cannot use a janus queue between two different event loops, since it binds to the current one.
Is janus still maintained?
Yes, but in maintenance mode. The development status is production and stable, the library is kept working with the latest Python versions and receives fixes, but no major changes are planned, and the documentation points performance-sensitive users and anyone needing anyio support at the experimental culsans library. The last push was on 2026-08-03.
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/aio-libs-janus)