Library / SDK
quora/asynq avatar
quora/asynq

asynq: batching calls to external services without rewriting your code

Python library for asynchronous programming

695 stars54 forksPythonApache-2.0

At a glance

What is it?
asynq is Quora's Python library for asynchronous programming, built around generator functions and a scheduler that groups individual calls into batched requests against storage services. It also solves the awkward case of synchronous-looking code that must still run in an ordered, context-aware way, and it is a Cython project whose last release was 1.6.0 in 2024.
Who is it for?
Adopt asynq if you are calling memcache or redis in a loop and the per-request round trip is your bottleneck, and if you like a scheduler that decides when to flush a batch instead of managing that yourself.
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 82 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 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem is one-request-versus-many, not concurrency for its own sake

The framing in the project description is worth repeating because it explains why asynq exists at all: it is a library for asynchronous programming in Python with a focus on batching requests to external services.

The specific claim is that for many storage services, memcache and redis among them, it is far faster to make a single request that fetches many keys at once than to make many requests that each fetch a single key. That is a latency and connection-overhead argument, not a CPU-parallelism argument. asynq is not trying to make your Python code run on many cores; it is trying to turn N round trips into one.

The stated use case at Quora is retrieving the names of the authors of a list of answers. The synchronous version of that code calls author_of_answer once per answer and name_of_user once per user, and each of those calls becomes its own memcache request. The asynchronous version produces one memcache request for all the author lookups and one for all the user lookups.

The claim that makes this interesting is that the code structure barely changes. The framework is described as making it easy to write code that takes advantage of batching without radical changes in code structure from code that does not use batching. That is the whole design goal, and it is why the library is worth looking at even if you would build the same thing differently today.

Generator functions and the @asynq decorator

asynq asynchronous functions are Python generator functions. When such a function yields one or more futures, it cedes control to the asynq scheduler, which resolves the futures that were yielded and then continues running the function once they have been computed.

The synchronous original and its asynq counterpart look like this:

python
    def all_author_names(aids):
        uids = [author_of_answer(aid) for aid in aids]
        names = [name_of_user(uid) for uid in uids]
        return names
python
    @asynq()
    def all_author_names(aids):
        uids = yield [author_of_answer.asynq(aid) for aid in aids]
        names = yield [name_of_user.asynq(uid) for uid in uids]
        return names

The decorator is mandatory on all asynchronous functions, and it does two things. It wraps the generator function so it can be called like a normal synchronous function, and it creates a .asynq attribute on that function, which returns an AsyncTask object when called.

That gives you both call styles. Calling the function directly returns the result:

python
    result = async_fn(a, b)

and yielding the .asynq version hands the scheduler a task to resolve later:

python
    result = yield async_fn.asynq(a, b)

Calling async_fn.asynq(a, b).value() has the same result as calling async_fn(a, b). There is also a pure=True option on the decorator, which disables the .asynq attribute and makes the function itself asynchronous, so calling it returns an AsyncTask. The documentation recommends using pure only in special cases such as writing decorators for asynchronous functions.

Futures and the scheduler that decides when a batch flushes

Futures are the building blocks of the programming model. The scheduler keeps track of futures and attempts to schedule them in an efficient way, and asynq uses its own hierarchy of future classes rooted in FutureBase. Every future has a .value() method that computes its value if necessary and then returns it.

Three classes matter in practice. AsyncTask is a future representing the execution of an asynchronous function, normally created by calling .asynq() on one. ConstFuture is a future whose value is already known at creation time, which is useful when you need to pass a future somewhere but no computation is actually needed. And BatchBase together with BatchItemBase are the building blocks for batching.

The scheduler is the part that decides when work happens, and it is invoked every time an async function yields. The documentation notes that because the scheduler is invoked on every yield, the debugging story is part of the library rather than an afterthought, and the package ships a profiler module, a scoped_value module and a _debug module among the compiled ones.

The interesting design point is what the scheduler is allowed to do. It is not a promise about the order in which your functions run, and the contexts section of the documentation is explicit that the scheduler can schedule tasks in arbitrary order. That is a deliberate trade: the freedom to reorder is exactly what makes batching possible, since the whole point is to collect many calls before issuing one request, and it is also what forces the AsyncContext mechanism.

Batching requires two subclass hooks, and the example shows both

To implement batching you subclass asynq.BatchItemBase and asynq.BatchBase. The first represents a single entry in a batch, such as a single memcache key to fetch. The second is responsible for executing the batch when the scheduler requests it.

The work is split in a way that keeps the common case cheap. Batch items usually do not require much logic beyond registering themselves with the currently active batch in __init__. The batch itself has to override two methods: _try_switch_active_batch, which changes the batch that is currently active, and _flush, which executes it. The flush method should call .set_value() on all the items in the batch, and that is what releases the waiting callers.

A worked memcache implementation lives in the asynq/examples/batching.py file, and the framework also provides a DebugBatchItem for testing. The repository tree confirms there is exactly one example module, examples/batching.py, alongside examples/__init__.py.

The scaling claim in the documentation is the number to hold in mind: most users of asynq should not need to implement batches frequently, and at Quora the team uses thousands of asynchronous functions but only five BatchBase subclasses. That is a favourable ratio, and it tells you that the batching work is concentrated in a small number of places rather than spread across the codebase.

AsyncContext exists because the scheduler reorders your code

This is the part of asynq that has no equivalent in the async and await world, and it is the strongest argument for understanding the library's model.

The problem: the scheduler can schedule tasks in arbitrary order. Consider a function that shows a warning and a function that suppresses warnings with the standard warnings.catch_warnings() context manager. If the caller yields both, only the second call to show_warning is inside the catch_warnings context, but because the scheduler may run things in a different order, the code that shows the warning can also execute while catch_warnings is active. The result is that a suppression you asked for leaks to a call you did not want suppressed.

The fix is an AsyncContext, which is automatically paused when the task that created it is no longer active and resumed when it becomes active again. The catch_warnings equivalent becomes a small class:

python
    class catch_warnings(asynq.AsyncContext):
        def pause(self):
            stop_catching_warnings()

        def resume(self):
            start_catching_warnings()

Two methods, pause and resume, and the scheduler handles the rest. Thread-local state and scoped values are the same problem in a different costume, which is why there is a scoped_value module in the compiled set.

If your use case for asynq is simply concurrent IO with a handful of independent calls, this machinery is overhead you do not need. It becomes essential the moment your code has ambient state, ordering assumptions or context that must not leak between tasks.

A Cython package with type stubs, and a release history that stopped

The build is where this project shows its age and its craft at the same time. The setup.py defines eleven Cython modules: async_task, asynq_to_async, batching, contexts, _debug, decorators, futures, profiler, scheduler, scoped_value and utils. Each one is compiled as an Extension, and the build sets language_level to 3 for the Cython directives. The package also ships a py.typed marker and .pxd and .pyi files, so the compiled code is typed for consumers.

The toolchain is pinned in requirements.txt: pytest for tests, Cython at least 0.27.1, qcore, pygments, black pinned at 24.10.0, mypy pinned at 1.11.2, pyright pinned at 1.1.384 and typing_extensions at 4.12.2. The pyproject configures black with target versions from py39 through py313, and mypy with warn_unused_ignores enabled. A pyright configuration alongside a mypy configuration is a sign of a project that took typing seriously.

The version history is where the caution goes. The releases are 1.6.0 from 2024-10-11, 1.5.1 from 2023-07-20 and 1.5.0 from 2023-05-26. The last push to the repository was on 2026-08-04, and the project is not archived. So the source is still touched, but no new release has shipped since October 2024, and the package version in setup.py is 1.6.0 to match.

The licence is Apache-2.0, with the LICENSE file at the root and setup.py carrying the standard Apache header. The README is an .rst file, and the changelog is a CHANGELOG file rather than GitHub release notes. A team that wants to adopt asynq today is adopting a 1.6.0 that will not change shape, which for a stable internal library is arguably a feature.

Editorial conclusion

Adopt asynq if you are calling memcache or redis in a loop and the per-request round trip is your bottleneck, and if you like a scheduler that decides when to flush a batch instead of managing that yourself. Do not adopt it for new code that could use asyncio or a modern async framework, because asynq predates the async and await syntax and its generator-based model will look unfamiliar, and its last release was 1.6.0 on 2024-10-11 with the last push on 2026-08-04, so the API is effectively frozen. Before you commit, read examples/batching.py, which contains the worked memcache batch implementation, and plan for the Cython build, since the package compiles eleven modules from source rather than shipping a pure Python wheel.

Frequently asked questions

What is asynq and what problem does it solve?

It is a Python library for asynchronous programming developed at Quora, with a focus on batching requests to external services. For storage services such as memcache and redis, one request fetching many keys is much faster than many requests fetching one key each.

How do I write an asynchronous function with asynq?

Write it as a generator function and apply the @asynq() decorator, yielding futures instead of computing values directly. The decorator also creates a .asynq attribute, so the function can be called synchronously or its AsyncTask yielded to the scheduler.

How do I implement batching in asynq?

Subclass asynq.BatchItemBase for a single batch entry and asynq.BatchBase for the batch itself. Items register themselves with the active batch in __init__, and the batch overrides _try_switch_active_batch and _flush, with _flush calling .set_value() on every item in the batch.

What is AsyncContext used for in asynq?

It keeps context tied to the task that created it. Because the scheduler can run tasks in arbitrary order, a plain context manager such as warnings.catch_warnings() can leak into tasks you did not intend to suppress, so an AsyncContext is paused and resumed automatically as the owning task becomes active and inactive.

Is the asynq library still being released?

The most recent releases are 1.6.0 from 2024-10-11, 1.5.1 from 2023-07-20 and 1.5.0 from 2023-05-26, and the last push to the repository was on 2026-08-04. The package is licensed under Apache-2.0 and is compiled from Cython sources.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. quora/asynq on GitHub
  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/quora-asynq.svg)](https://hysenlabs.com/projects/quora-asynq)

Was this useful?