# uvloop swaps the event loop under asyncio without touching your code

> A Cython implementation of the asyncio loop on top of libuv, vendored and compiled at install time. Fast by design, but the licence metadata and Windows support are worth reading before you depend on it.

**MagicStack/uvloop** — Ultra fast asyncio event loop.

- Repository: https://github.com/MagicStack/uvloop
- Stars: 11,911 · Forks: 616
- Language: Cython
- License: Apache-2.0
- Published: 2026-10-07 · Updated: 2026-10-07 · Language: en
- Canonical page: https://hysenlabs.com/projects/magicstack-uvloop

## A loop replacement, not a faster asyncio

The README describes uvloop as a fast, drop-in replacement of the built-in asyncio event loop, implemented in Cython and using libuv under the hood. GitHub reports the repository language as Cython, and the topics are async, asyncio, event-loop, libuv and python.

The distinction in that first sentence is the one that matters for your codebase. uvloop is not a library you call instead of asyncio. It is an implementation of the loop object that asyncio's own machinery runs on, so your application still awaits the same futures, uses the same Task and the same protocol APIs, and keeps its existing imports. What changes is which C loop is underneath.

That framing also explains the design. A pure-Python loop pays per-callback overhead in the interpreter, and libuv is an event loop written in C with its own callback machinery. Moving the loop into Cython buys back some of that. The README states the headline plainly, saying uvloop makes asyncio 2-4x faster, and points to a chart of an echo server at different message sizes.

Whether that range applies to you depends entirely on what your workload does. The chart behind the claim covers three shapes of the same echo server, and they are worth reading individually, because the number you care about depends on which one resembles your code.

## Reading the echo benchmark properly

The performance chart is an echo server measured three ways, and the README explains what differs between them. The `sockets` benchmark uses `loop.sock_recv()` and `loop.sock_sendall()`. The `streams` benchmark uses the high-level asyncio streams created by `asyncio.start_server()`. The `protocol` benchmark uses `loop.create_server()` with a simple echo protocol.

Those are three different amounts of Python work per message. The sockets case spends its time in two direct loop methods, so it is the closest to a measurement of the loop itself. The streams case adds the higher-level wrapper on top. The protocol case moves the callback into a transport and protocol object, which is the shape most real servers use and the shape where Python overhead is largest.

This is the detail that decides whether the 2-4x figure is worth chasing. If your service spends its time in database calls and upstream HTTP, the loop is not where your time goes and swapping it will not move your latency. If your service accepts many small messages and does little per message, the loop is the whole story, and this is exactly the shape the chart measures.

The benchmark is also old in the way benchmarks are: an echo server on a chart image, with a blog post as the detailed write-up. Nothing in the repository reports current hardware or a current Python version. Treat it as a reason to try uvloop on your own traffic, not as a number to quote.

## Installing it and choosing an entry point

Installation is a single package from PyPI. The README asks you to upgrade pip first, which matters because uvloop ships compiled extensions and an older pip is more likely to fail on a source build:

```bash
$ pip install uvloop
$ pip install -U pip
```

Since version 0.18 the README calls `uvloop.run()` the preferred way to use the loop, and the example is this short:

```python
import uvloop

async def main():
    # Main entry-point.
    ...

uvloop.run(main())
```

The README says `uvloop.run()` works by configuring `asyncio.run()` to use uvloop and passing through its arguments, so `uvloop.run(main(), debug=True)` behaves as you would expect. That single function is the whole integration surface for most applications.

There is a second snippet in the README, headed as the alternative for Python 3.11 and earlier, which is worth reading carefully because the heading and the code disagree. The snippet itself checks the version at runtime: on 3.11 and above it constructs an `asyncio.Runner` with `loop_factory=uvloop.new_event_loop`, and on anything older it calls `uvloop.install()` before `asyncio.run(main())`. So the block handles both cases rather than only the older one. If you are on 3.11 or newer and want the runner form, take it from the snippet; the heading overstates which versions it applies to.

## The licence label and the licence text disagree

Here is a place where the repository's own files say different things, and it is worth resolving yourself rather than trusting the badge.

The README states that uvloop is dual-licensed under MIT and Apache 2.0. The repository root carries both `LICENSE-APACHE` and `LICENSE-MIT` as separate files, which is consistent with that. But GitHub reports the repository license as Apache-2.0 alone, and `pyproject.toml` records the licence as MIT License. So three sources give you two different single answers and one dual answer.

The practical resolution is easy: read both files in the root. When a project ships two licence files side by side, the usual arrangement is that you may pick either, and which one you pick depends on your own obligations rather than the project's preference. Nothing in the README tells you to prefer one, and nothing suggests the Apache file is a fallback for a subset of users.

If your compliance process records a single licence identifier, do not copy it from the repository header. Record the dual licence and attach both texts. That is the defensible position, and it costs you one line of documentation.

## Building from source means building libuv too

Installing from PyPI gets you a wheel, but the source build is where the project's structure shows. libuv is vendored as a git submodule under `vendor/libuv`, and `setup.py` runs that dependency's own configure or autogen step during the build, adding `-fPIC` to the compiler flags and defaulting the optimisation level to `-O2` if the environment does not already set one.

The build is driven from a Makefile whose default target compiles, with `make test` running the suite twice against the in-tree build and the installed package:

```bash
$ git clone --recursive git@github.com:MagicStack/uvloop.git
$ cd uvloop
$ python3 -m venv uvloop-dev
$ source uvloop-dev/bin/activate
$ pip install -e .[dev]
$ make
$ make test
```

Two details in that flow are worth noting. The clone uses `--recursive` because a non-recursive clone leaves the submodule empty, and `setup.py` raises a RuntimeError telling you to run `git submodule init; git submodule update` when it finds no `configure` and no `autogen.sh`. The test target also sets `PYTHONASYNCIODEBUG=1`, so the suite runs with asyncio's debug mode enabled rather than in production timing.

If you need a debug build, the Makefile has one. The `debug` target adds `--cython-annotate`, a linetrace directive and the `UVLOOP_DEBUG`, `CYTHON_TRACE` and `CYTHON_TRACE_NOGIL` defines, which is the configuration to reach for when you are tracing into the Cython layer itself.

## Platform limits and version drift

Two constraints bound where uvloop can run, and both are enforced in code rather than mentioned as caveats.

Windows is out. `setup.py` raises a RuntimeError on the `win32`, `cygwin` and `cli` platforms, before any compilation starts, and the classifiers list only POSIX and MacOS X. The Python floor is 3.8: `setup.py` raises on anything older than `(3, 8)`, `pyproject.toml` records `requires-python = '>=3.8.1'`, and the README says Python 3.8 or greater. Those three agree with each other.

The version metadata drifts in one direction, though. The classifiers enumerate Python 3.8, 3.9, 3.10, 3.11, 3.12 and 3.13, with an implementation marker for CPython. But the 0.22.0 release notes list fixes for Python 3.14 and a test fix for task names on Python 3.13.3 and 3.14. The packaging metadata therefore describes support one release behind what the code has been fixed for.

That gap is small and easy to handle: if you run 3.14, install 0.22.0 or newer and expect it to work, because the release notes say it was fixed, while accepting that a tool reading only the classifiers will disagree. The last release on the repository is v0.22.1 from 2025-10-16, and its note says it is identical to 0.22.0, re-run with CI fixes. The repository was pushed to on 2026-07-14, so work has continued since that tag without a new release attached to it.

## Conclusion

uvloop is a good fit when you have an asyncio service whose profile matches its benchmarks: socket-heavy echo work, streams, or a hand-written protocol callback, on a POSIX machine, where you have measured Python-level loop overhead rather than guessed at it. Three things decide it in practice. Check the classifiers in `pyproject.toml` before you pin, because the packaging metadata lists Python 3.8 through 3.13 while the 0.22.0 release notes describe fixes for Python 3.14, so the metadata trails the releases. Read both licence files rather than the single label GitHub shows, since the README calls the project dual-licensed while the repository metadata names Apache-2.0. And accept that Windows is out: `setup.py` raises on `win32`, `cygwin` and `cli` platforms before anything else happens. If none of that rules it out, `uvloop.run(main())` is a one-line change and the bundled echo benchmark is the fastest way to find out whether it helps your workload.

## FAQ

### Does uvloop work on Windows?

No. `setup.py` raises a RuntimeError on the `win32`, `cygwin` and `cli` platforms, and the classifiers in `pyproject.toml` list only POSIX and MacOS X. The README does not discuss Windows support at all.

### How do I make asyncio use uvloop?

Since version 0.18 the README calls `uvloop.run()` the preferred way, and it passes its arguments through to `asyncio.run()`, so `uvloop.run(main())` and `uvloop.run(main(), debug=True)` both work. On Python 3.11 and newer the README also shows an `asyncio.Runner` constructed with `loop_factory=uvloop.new_event_loop`, and `uvloop.install()` covers older versions.

### What licence is uvloop under?

The README says MIT and Apache 2.0, and the repository root carries both `LICENSE-APACHE` and `LICENSE-MIT`. GitHub reports the repository licence as Apache-2.0 while `pyproject.toml` records MIT License, so the two files in the root are the authoritative source.

### Which Python versions does uvloop support?

Python 3.8 or greater. `setup.py` raises on anything older, `pyproject.toml` sets `requires-python = '>=3.8.1'`, and the classifiers list 3.8 through 3.13. The 0.22.0 release notes also describe fixes for Python 3.14, which the classifiers do not yet reflect.

### How much faster is uvloop than the built-in asyncio loop?

The README states that uvloop makes asyncio 2-4x faster, measured on an echo server at several message sizes. The chart separates a sockets case using `loop.sock_recv()` and `loop.sock_sendall()`, a streams case built on `asyncio.start_server()`, and a protocol case using `loop.create_server()`, so the range depends on how much Python work your own workload does per message.

## Sources

- [Issues](https://github.com/MagicStack/uvloop/issues)
- [License: Apache-2.0](https://github.com/MagicStack/uvloop/blob/master/LICENSE)
- [MagicStack/uvloop on GitHub](https://github.com/MagicStack/uvloop)
- [README](https://github.com/MagicStack/uvloop/blob/master/README.md)
- [Releases](https://github.com/MagicStack/uvloop/releases)

---

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