# 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](https://peps.python.org/pep-0669/)), 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:

```console
$ COVERAGE_CORE=sysmon pytest --cov=myapp
```

In GitHub Actions:

```yaml
- 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:

```toml
# 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 `sysmon` needs Python 3.14. On 3.12 and 3.13 with `--cov-branch` or `branch = 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_context` setting). 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] core` setting](https://coverage.readthedocs.io/en/latest/config.html#config-run-core)
- [coverage.py: how it works, execution](https://coverage.readthedocs.io/en/latest/howitworks.html#execution)
- [coverage.py changelog](https://coverage.readthedocs.io/en/latest/changes.html)
- [Python: `sys.monitoring`](https://docs.python.org/3/library/sys.monitoring.html)
- [PEP 669: Low Impact Monitoring for CPython](https://peps.python.org/pep-0669/)
