Skip to main content

hydroid

Finds blocking calls that run on the event loop in async FastAPI (and any asyncio) code — at any call depth, across modules, through callbacks, decorators, context managers, properties and subclass overrides — and shows the whole chain.

error[blocking-http]: `requests.api.get` blocks the event loop
  --> app/main.py:12:12 in `app.main.get_user`
   = reached from route GET /users/{user_id}: app.main.get_user (app/main.py:11)
   = blocking chain (3 calls):
       app/main.py:12:12  services.load -> app.services.load
       app/services.py:7:22  fetch -> app.repo.fetch
       app/repo.py:5:16  http_get -> requests.api.get
   = help: use an async client (`httpx.AsyncClient`, `aiohttp`) or `await asyncio.to_thread(...)`

New here? Read the usage guide. Adding it to CI, or letting a coding agent set it up: CI/CD and agent setup.

Written in Rust on top of ty's semantic model (types, imports, go-to-definition). Polar's server (1,854 files, 108k call sites) takes about as long as ty needs to type-check it the first time (~10 s on 4 vCPUs), then ~0.35 s while nothing changed: extracted facts are cached in .hydroid_cache/. HYDROID_TIMINGS=1 prints where the time goes.

Install and run

With uv, using the prebuilt binaries of the latest release (macOS, Linux, Windows; no Rust needed):

uvx --from hydroid-cli --find-links https://github.com/agusmdev/hydroid/releases/expanded_assets/v0.1.0 hydroid .

Or straight from the source on main (builds once with a Rust toolchain, then cached):

uvx --from git+https://github.com/agusmdev/hydroid hydroid .

To install the hydroid command permanently, replace uvx --from with uv tool install (e.g. uv tool install git+https://github.com/agusmdev/hydroid), then run hydroid . anywhere. From a clone: cargo install --path crates/hydroid. More in the guide.

hydroid path/to/project                 # finds .venv / VIRTUAL_ENV like ty
hydroid . --python .venv --format sarif > hydroid.sarif
Flag
--python PATH virtualenv or interpreter to resolve imports (default: discovered)
--follow-libs also analyze third-party function bodies
--cpu also report CPU-bound calls (bcrypt, pbkdf2, scrypt)
--strict report calls on the loop whose target is unknown, and fail on them
--exclude GLOB skip files (repeatable)
--no-cache do not read or write .hydroid_cache/
--format human|json|sarif output

Exit status: 0 clean, 1 findings (or unresolved calls with --strict), 2 error. Suppress a line with # hydroid: ignore.

Configuration in pyproject.toml (flags win):

[tool.hydroid]
python = ".venv"
exclude = ["migrations/**"]
strict = false

[[tool.hydroid.sink]]            # your own blocking functions (defining qualified names)
category = "sdk"
advice = "use the async client"
functions = ["acme.client.Client.fetch"]

CI/CD and agent setup

hydroid exits 1 when it finds blocking calls, so it can gate a build as is. On GitHub, --format sarif puts each blocking chain on the pull request through code scanning:

# .github/workflows/hydroid.yml
name: hydroid
on: [push, pull_request]
jobs:
  hydroid:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v6
      - run: uv sync                      # hydroid needs the project's dependencies installed
      - run: >-
          uvx --from hydroid-cli --find-links https://github.com/agusmdev/hydroid/releases/expanded_assets/v0.1.0
          hydroid . --format sarif > hydroid.sarif || [ $? -eq 1 ]
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: hydroid.sarif

|| [ $? -eq 1 ] reports findings without failing the job (exit 2, an error, still fails it); drop it and the upload step to fail the build instead.

docs/AGENT_SETUP.md has:

  • a prompt to paste into a coding agent (Claude Code, Cursor, Codex…) that installs hydroid in your repository, configures [tool.hydroid], triages the existing findings (fix, # hydroid: ignore with a reason, or report to you) and adds the CI job;
  • configs for GitLab CI, pre-commit, monorepos, projects not using uv, and other CI systems;
  • a path for adopting it on a code base with existing findings: report-only first, then gate.

How it works

  1. Facts (crates/hydroid_ty, the only crate that touches ty): every function body is walked in parallel; each call is resolved to its definitions through ty (types, then go-to-definition), including implicit calls (with, for and comprehensions, properties and their setters, descriptors, operators like x[k] and k in x on user classes, __getattr__, iterators' __next__, constructors with __post_init__ and pydantic validators, builtins and contextlib helpers that iterate, enter or call their arguments like sorted(xs, key=f) or stack.enter_context(cm), singledispatch implementations). Callables passed as arguments are recorded as flows (to_thread(f), Depends(dep), on_startup=[f], decorators), and so are callables stored on self and returned by functions. When ty does not know a value's type because it comes from an unannotated function, the class that function returns is used. The facts of an unchanged project are reused from .hydroid_cache/ (keyed by the binary, the options, the installed packages and every source file's contents).
  2. Analysis (crates/hydroid_core, plain graph code): where code runs is a property of the call, so "does calling f block?" is computed once per function by a backwards BFS from blocking functions — shortest witness chains, no recursion, any depth. Diagnostics are the call sites inside loop code (async def bodies, sync callbacks scheduled on the loop) whose callee blocks. Offload APIs cut the graph; callbacks are attributed to the call site passing them; overrides are found by class-hierarchy analysis (test doubles only stand in for real classes in test code); calling a generator function runs nothing until the generator is iterated; a decorated function is reached through its decorator's wrappers only when they call it. Starting an event loop (asyncio.run) is only reported when loop code does it directly: sync bridges reaching it almost always check for a running loop first.
  3. Catalog (crates/hydroid_core/catalog.toml): blocking functions, offload APIs, loop callback APIs and FastAPI entry points, by defining qualified name. A test checks every name exists in a real environment.

Sync def endpoints and dependencies are not reported: FastAPI runs them in a threadpool.

Limits

Python is dynamic: calls through getattr, untyped parameters, or containers of callables cannot always be resolved. They are never silently dropped — --strict lists every one reachable on the loop. Not modeled yet: getattr(obj, "name"), containers of callables (HANDLERS[kind]()), metaclasses, and .pyi-only libraries under --follow-libs. A change to any source file re-extracts the whole project (no per-file incremental cache yet).

Development

cargo test --workspace        # needs `uv`; syncs tests/fixtures/.venv from uv.lock
scripts/corpus.sh [names]     # Polar, mealie, lnbits, dispatch, litellm, open-webui, template

Fixture cases live in tests/fixtures/cases/*; expectations are inline comments: # expect: time.sleep via route GET /x, # expect: * (any blocking function), # expect-unresolved.

tests/adversarial/ is a suite of cases written to break hydroid, split into a training half (required to pass, by cargo test) and a held-out half that is only scored:

scripts/hillclimb/score.py --split train            # misses and false positives, per case
scripts/hillclimb/score.py --split holdout --quiet  # aggregate only: don't tune on it
scripts/hillclimb/polar_time.sh target/release/hydroid 5
uv run --with anthropic scripts/hillclimb/adversary.py --count 5   # Claude proposes new cases
scripts/hillclimb/oracle.py PROJECT report.json   # naive syntactic scan: candidate misses

ty's crates are pinned to exact versions (=0.0.15); they have no API stability promise. The bundled typeshed stubs are Apache-2.0; hydroid is MIT.

Releases: docs/RELEASING.md.

Metadata

Release files for hydroid-cli 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hydroid-cli 0.2.0
File Size Uploaded
hydroid_cli-0.2.0.tar.gz 74.9 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for hydroid-cli 0.2.0
File
hydroid_cli-0.2.0-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
hydroid_cli-0.2.0-py3-none-musllinux_1_2_x86_64.whl Python 3 none Linux musl 1.2+ x86-64 Details
hydroid_cli-0.2.0-py3-none-musllinux_1_2_aarch64.whl Python 3 none Linux musl 1.2+ ARM64 Details
hydroid_cli-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl Python 3 none Linux glibc 2.17+ x86-64 Details
hydroid_cli-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl Python 3 none Linux glibc 2.17+ ARM64 Details
hydroid_cli-0.2.0-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details
hydroid_cli-0.2.0-py3-none-macosx_10_12_x86_64.whl Python 3 none macOS 10.12+ x86-64 Details

Total release size: 71.3 MB

Release files / hydroid_cli-0.2.0.tar.gz

Download URL hydroid_cli-0.2.0.tar.gz
Size 74.9 kB
Tags Source
SHA-256 checksum
How to use checksums
a119324af3978b07f031150e5f24b5c316b18f3e3a7d4aad5e078745e28459af
BLAKE2b-256 checksum
How to use checksums
48c68c21ac231f91b2f85eed19a49780a0863a9b6d961d874177c1791f2f0dcc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release files / hydroid_cli-0.2.0-py3-none-win_amd64.whl

Download URL hydroid_cli-0.2.0-py3-none-win_amd64.whl
Size 10.6 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
81798c8726dac20ef978db7aab91fe5be68e12c479d00fc4a6ebee436b5a9d07
BLAKE2b-256 checksum
How to use checksums
29410aa2c9272fd1544a974ea4ea010854fb288242658bfeb09a1bdd493bfceb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release files / hydroid_cli-0.2.0-py3-none-musllinux_1_2_x86_64.whl

Download URL hydroid_cli-0.2.0-py3-none-musllinux_1_2_x86_64.whl
Size 10.6 MB
Tags Linux musl 1.2+ x86-64 Python 3
SHA-256 checksum
How to use checksums
7806ecb845ad7268d8eb19a2f7660740f1c30e8473c8c115e3aa836e9937b479
BLAKE2b-256 checksum
How to use checksums
a599d706e180a6ccc14475348273e11451a7ada72617cfa902a7ce739e95337e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release files / hydroid_cli-0.2.0-py3-none-musllinux_1_2_aarch64.whl

Download URL hydroid_cli-0.2.0-py3-none-musllinux_1_2_aarch64.whl
Size 9.8 MB
Tags Linux musl 1.2+ ARM64 Python 3
SHA-256 checksum
How to use checksums
be9fa756dac2738250a5e62333a64d396559cc33c1a2770b2db2367f424f9f6d
BLAKE2b-256 checksum
How to use checksums
98d0ea0f5c535eca0f2680d7ed4fc6a83347da56334693d21152716be5f7938c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release files / hydroid_cli-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL hydroid_cli-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 10.4 MB
Tags Linux glibc 2.17+ x86-64 Python 3
SHA-256 checksum
How to use checksums
a1ccdc4a4a4a14f80408c2aa355ecaa4d5dd40ea338b07d183bec24d383a2566
BLAKE2b-256 checksum
How to use checksums
5ab31f2f9af8f852e7bdc9dd769b9f3053c0997e6c9e08f2c1518223275ba76f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release files / hydroid_cli-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL hydroid_cli-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 9.9 MB
Tags Linux glibc 2.17+ ARM64 Python 3
SHA-256 checksum
How to use checksums
185b5c2c8ee9fbcb76f8240719282501e9649788c51a9401b850e804d443efa8
BLAKE2b-256 checksum
How to use checksums
660d61c552f1f4890f80b1555cf1543a40ff02eec744557540e9fc0c5b0f5569
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release files / hydroid_cli-0.2.0-py3-none-macosx_11_0_arm64.whl

Download URL hydroid_cli-0.2.0-py3-none-macosx_11_0_arm64.whl
Size 9.8 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
e2bd2e3d16010c2dc6c387f2ec39c3adb5e4fb0c1abc3fc8def2c9611dd837d2
BLAKE2b-256 checksum
How to use checksums
64fca65835c8a771a3d55004a8977ce28be9462aa3d99feea08c1a294ee2ba30
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release files / hydroid_cli-0.2.0-py3-none-macosx_10_12_x86_64.whl

Download URL hydroid_cli-0.2.0-py3-none-macosx_10_12_x86_64.whl
Size 10.3 MB
Tags Python 3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
dabbac41b2029cc45cbd9d5c0c98809a197f6f2c0413c988a0c195789049673c
BLAKE2b-256 checksum
How to use checksums
1c7d135b87a92aace614d1382b73b30b9d7768af322a198caadfdc2282b3441e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

8 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page