Faster coverage with sys.monitoring (COVERAGE_CORE=sysmon)
pytest-cov makes tests slower because coverage.py traces every line. On Python 3.12+ set COVERAGE_CORE=sysmon to measure with sys.monitoring at much lower overhead.
Running tests with coverage is slower than running them without it, and the difference is often big enough that people only measure coverage in CI. Most of that overhead comes from how coverage.py has traditionally found out which lines ran: a trace function that Python calls for every line it executes.
Since Python 3.12 there's a cheaper way, sys.monitoring (PEP 669), and coverage.py can use it. On Python 3.14 and later it already does by default; on 3.12 and 3.13 you have to turn it on.
Why the default tracer is slow
coverage.py has three measurement cores:
| Core | How it works | Default |
|---|---|---|
ctrace |
a C trace function installed with sys.settrace |
up to Python 3.13 |
sysmon |
sys.monitoring events |
Python 3.14+ (coverage 7.9.1+), when your configuration supports it |
pytrace |
a pure-Python trace function | with --timid, or when the C extension isn't available |
A trace function runs on every line event of every frame it traces, for the whole run, even for a line that has already been recorded a million times. A hot loop in your code pays for coverage on every iteration.
sys.monitoring lets coverage.py ask for an event the first time a line runs and then switch that event off for that location. After the first execution, a line costs nothing extra. Coverage's own documentation describes it as much faster than trace functions for line and branch coverage.
The fix: COVERAGE_CORE=sysmon
With coverage 7.4 or later on Python 3.12+, set the environment variable wherever the tests run:
$ COVERAGE_CORE=sysmon pytest --cov=myapp
In GitHub Actions:
- name: Run tests
run: pytest --cov=myapp --cov-report=xml
env:
COVERAGE_CORE: sysmon
With coverage 7.9 or later you can put it in the configuration file instead, so it applies everywhere without an environment variable:
# pyproject.toml
[tool.coverage.run]
core = "sysmon"
pytest-cov runs coverage.py inside the pytest process, so both forms work with pytest --cov. To check which core is in use, run coverage run --debug=sys -m pytest and look for the core: line (coverage 7.11.1 and later also have --debug=core, which says why).
What sysmon doesn't support
The sysmon core doesn't support everything the trace-function cores do. From coverage.py's documentation:
- Branch coverage on Python 3.12 and 3.13. Branch measurement with
sysmonneeds Python 3.14. On 3.12 and 3.13 with--cov-branchorbranch = true, current coverage.py warns ("Can't use core=sysmon: sys.monitoring can't measure branches in this version") and uses the default core instead. - Plugins, such as coverage plugins for template languages.
- Dynamic contexts (the
dynamic_contextsetting). Recent versions warn when you combine them. - Some concurrency libraries.
When the default on 3.14+ would be sysmon but your configuration needs one of these, coverage.py falls back to ctrace on its own. If you ask for sysmon explicitly and it can't be used, it warns and uses another core.
So the decision is simple: on Python 3.12 or 3.13 with line coverage only, set COVERAGE_CORE=sysmon. If you need branch coverage, the way to get the faster core is Python 3.14. On 3.14+, make sure nothing in your configuration forces the fallback.
Keep the tests correct
- Coverage numbers are the same; only how they're measured changes.
- Watch for the "Can't use core=sysmon" warning in CI output: it means you're still on the slow core.
How flakytest finds it
flakytest records whether pytest-cov is active and whether the COVERAGE_CORE environment variable is set. When pytest-cov is running on Python 3.12 or 3.13 without it, flakytest suggests Measure coverage with sys.monitoring. It doesn't fire on Python 3.14+, where sysmon is already the default. Profile once with and once without the variable, and flakytest compare shows what it saved per test.
Further reading
- coverage.py: the
[run] coresetting - coverage.py: how it works, execution
- coverage.py changelog
- Python:
sys.monitoring - PEP 669: Low Impact Monitoring for CPython
Related guides
- Speed up pytest in CI: Speed up slow CI test jobs: cache dependencies with uv or pip, use pytest-xdist on every core, shard with pytest-split, migrate once, and see why CI is slower than local.
- Run tests in parallel with pytest-xdist: Run pytest in parallel on every CPU with pytest-xdist and -n auto. Per-worker databases with pytest-django, what breaks in parallel, pytest-randomly, and the --dist modes.
- How to profile a pytest suite: Profile slow pytest tests with cProfile, pytest-profiling, py-spy, pyinstrument, Python 3.15's tachyon or flakytest: the commands, what each one shows, and which to use when.
Updated October 18, 2026 · Suggest a change