Repository: aristanetworks/anta
Pull request #680: feat(anta): New AntaRunner class with limit concurrency
Author: login: carl-baillargeon
Labels: name: rn: feat(anta)
Created: 2024-05-16T05:04:18Z | Merged: 2025-05-13T14:32:03Z

# Description

- Add AntaRunner class
- Add settings.py module to manage ANTA settings
- Add Semaphore option to limit the number of tests running concurrently

Fixes: #832
Fixes: #1187 

**Small benchmarks on i7-12800H, 32GB RAM. 41044 tests on 31 devices**:
PR: `anta nrfu json --output anta_results.json  78.45s user 4.17s system 82% cpu 1:40.15 total`

main: `anta nrfu json --output anta_results.json  74.72s user 3.85s system 75% cpu 1:43.48 total`

**With max concurrency of 25000 (instead of default 50000):**
`anta nrfu json --output anta_results.json  76.49s user 4.14s system 66% cpu 2:01.39 total`

As expected, with a limit, it takes more time but less CPU usage.

# Checklist:

- [x] Update FAQ
- [x] Update benchmark + unit tests
- [x] #826 
- [x] Confirm that the number of open file descriptors never exceed device * max_connections (100 by default)



Diff:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 220176e49..8236c9980 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -75,6 +75,7 @@ repos:
           - pytest
           - pytest-codspeed
           - respx
+          - pydantic-settings
 
   - repo: https://github.com/codespell-project/codespell
     rev: v2.4.1
@@ -124,6 +125,7 @@ repos:
         pass_filenames: false
         additional_dependencies:
           - anta[cli]
+          - pydantic-settings
       - id: doc-snippets
         name: Generate doc snippets
         entry: >-
@@ -135,3 +137,4 @@ repos:
         pass_filenames: false
         additional_dependencies:
           - anta[cli]
+          - pydantic-settings
diff --git a/anta/_runner.py b/anta/_runner.py
new file mode 100644
index 000000000..109108bee
--- /dev/null
+++ b/anta/_runner.py
@@ -0,0 +1,491 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass, field
+from datetime import datetime, timedelta, timezone
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict
+
+from anta import GITHUB_SUGGESTION
+from anta.cli.console import console
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.catalog import AntaCatalog, AntaTestDefinition
+    from anta.device import AntaDevice
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+class AntaRunFilters(BaseModel):
+    """Define filters for an ANTA run.
+
+    Filters determine which devices and tests to include in a run, and how to
+    filter them with tags. This class is used by the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+@dataclass
+class AntaRunContext:
+    """Store the complete context and results of an ANTA run.
+
+    A unique context is created and returned per ANTA run.
+
+    Attributes
+    ----------
+    inventory: AntaInventory
+        Initial inventory of devices provided to the run.
+    catalog: AntaCatalog
+        Initial catalog of tests provided to the run.
+    manager: ResultManager
+        Manager with the final test results.
+    filters: AntaRunFilters
+        Provided filters to the run.
+    selected_inventory: AntaInventory
+        The final inventory of devices selected for testing.
+    selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]]
+        A mapping containing the final tests to be run per device.
+    devices_filtered_at_setup: list[str]
+        List of device names that were filtered during the inventory setup phase.
+    devices_unreachable_at_setup: list[str]
+        List of device names that were found unreachable during the inventory setup phase.
+    warnings_at_setup: list[str]
+        List of warnings caught during the setup phase.
+    start_time: datetime | None
+        Start time of the run. None if not set yet.
+    end_time: datetime | None
+        End time of the run. None if not set yet.
+    """
+
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager
+    filters: AntaRunFilters
+    dry_run: bool = False
+
+    # State populated during the run
+    selected_inventory: AntaInventory = field(default_factory=AntaInventory)
+    selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = field(default_factory=lambda: defaultdict(set))
+    devices_filtered_at_setup: list[str] = field(default_factory=list)
+    devices_unreachable_at_setup: list[str] = field(default_factory=list)
+    warnings_at_setup: list[str] = field(default_factory=list)
+    start_time: datetime | None = None
+    end_time: datetime | None = None
+
+    @property
+    def total_devices_in_inventory(self) -> int:
+        """Total devices in the initial inventory provided to the run."""
+        return len(self.inventory)
+
+    @property
+    def total_devices_filtered_by_tags(self) -> int:
+        """Total devices filtered by tags at inventory setup."""
+        return len(self.devices_filtered_at_setup)
+
+    @property
+    def total_devices_unreachable(self) -> int:
+        """Total devices unreachable at inventory setup."""
+        return len(self.devices_unreachable_at_setup)
+
+    @property
+    def total_devices_selected_for_testing(self) -> int:
+        """Total devices selected for testing."""
+        return len(self.selected_inventory)
+
+    @property
+    def total_tests_scheduled(self) -> int:
+        """Total tests scheduled to run across all selected devices."""
+        return sum(len(tests) for tests in self.selected_tests.values())
+
+    @property
+    def duration(self) -> timedelta | None:
+        """Calculate the duration of the run. Returns None if start or end time is not set."""
+        if self.start_time and self.end_time:
+            return self.end_time - self.start_time
+        return None
+
+
+# pylint: disable=too-few-public-methods
+class AntaRunner:
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+    An `AntaRunner` instance is stateless between runs. All necessary inputs like inventory
+    and catalog are provided to the `run()` method.
+
+    Attributes
+    ----------
+    _settings : AntaRunnerSettings
+        Settings container for the runner. This can be provided during initialization;
+        otherwise, it is loaded from environment variables by default. See the
+        `AntaRunnerSettings` class definition in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunFilters
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner()
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run(inventory, catalog))
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(inventory, catalog, filters=AntaRunFilters(tags={"leaf"})))
+    ```
+    """
+
+    def __init__(self, settings: AntaRunnerSettings | None = None) -> None:
+        """Initialize AntaRunner."""
+        self._settings = settings if settings is not None else AntaRunnerSettings()
+        logger.debug("AntaRunner initialized with settings: %s", self._settings.model_dump())
+
+    async def run(
+        self,
+        inventory: AntaInventory,
+        catalog: AntaCatalog,
+        result_manager: ResultManager | None = None,
+        filters: AntaRunFilters | None = None,
+        *,
+        dry_run: bool = False,
+    ) -> AntaRunContext:
+        """Run ANTA.
+
+        Run workflow:
+
+        1. Build the context object for the run.
+        2. Set up the selected inventory, removing filtered/unreachable devices.
+        3. Set up the selected tests, removing filtered tests.
+        4. Prepare the `AntaTest` coroutines from the selected inventory and tests.
+        5. Run the test coroutines if it is not a dry run.
+
+        Parameters
+        ----------
+        inventory
+            Inventory of network devices to test.
+        catalog
+            Catalog of tests to run.
+        result_manager
+            Manager for collecting and storing test results. If `None`, a new manager
+            is returned for each run, otherwise the provided manager is used
+            and results from subsequent runs are appended to it.
+        filters
+            Filters for the ANTA run. If `None`, run all tests on all devices.
+        dry_run
+            Dry-run mode flag. If `True`, run all setup steps but do not execute tests.
+
+        Returns
+        -------
+        AntaRunContext
+            The complete context and results of this ANTA run.
+        """
+        start_time = datetime.now(tz=timezone.utc)
+        logger.info("ANTA run starting ...")
+
+        ctx = AntaRunContext(
+            inventory=inventory,
+            catalog=catalog,
+            manager=result_manager if result_manager is not None else ResultManager(),
+            filters=filters if filters is not None else AntaRunFilters(),
+            dry_run=dry_run,
+            start_time=start_time,
+        )
+
+        if len(ctx.manager) > 0:
+            msg = (
+                f"Appending new results to the provided ResultManager which already holds {len(ctx.manager)} results. "
+                "Statistics in this run context are for the current execution only."
+            )
+            self._log_warning_msg(msg=msg, ctx=ctx)
+
+        if not ctx.catalog.tests:
+            self._log_warning_msg(msg="The list of tests is empty. Exiting ...", ctx=ctx)
+            ctx.end_time = datetime.now(tz=timezone.utc)
+            return ctx
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            setup_inventory_ok = await self._setup_inventory(ctx)
+            if not setup_inventory_ok:
+                ctx.end_time = datetime.now(tz=timezone.utc)
+                return ctx
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                setup_tests_ok = self._setup_tests(ctx)
+                if not setup_tests_ok:
+                    ctx.end_time = datetime.now(tz=timezone.utc)
+                    return ctx
+
+            # Get test coroutines
+            test_coroutines = self._get_test_coroutines(ctx)
+
+        self._log_run_information(ctx)
+
+        if ctx.dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            self._close_test_coroutines(test_coroutines, ctx)
+            ctx.end_time = datetime.now(tz=timezone.utc)
+            return ctx
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=ctx.total_tests_scheduled)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            sem = Semaphore(self._settings.max_concurrency)
+
+            async def run_with_sem(test_coro: Coroutine[Any, Any, TestResult]) -> TestResult:
+                """Wrap the test coroutine with semaphore control."""
+                async with sem:
+                    return await test_coro
+
+            results = await gather(*[run_with_sem(coro) for coro in test_coroutines])
+            for res in results:
+                ctx.manager.add(res)
+
+        self._log_cache_statistics(ctx)
+
+        ctx.end_time = datetime.now(tz=timezone.utc)
+        return ctx
+
+    async def _setup_inventory(self, ctx: AntaRunContext) -> bool:
+        """Set up the inventory for the ANTA run.
+
+        Returns True if the inventory setup was successful, otherwise False.
+        """
+        initial_device_names = set(ctx.inventory.keys())
+
+        if not initial_device_names:
+            self._log_warning_msg(msg="The initial inventory is empty. Exiting ...", ctx=ctx)
+            return False
+
+        # Filter the inventory based on the provided filters if any
+        filtered_inventory = (
+            ctx.inventory.get_inventory(tags=ctx.filters.tags, devices=ctx.filters.devices) if ctx.filters.tags or ctx.filters.devices else ctx.inventory
+        )
+        filtered_device_names = set(filtered_inventory.keys())
+        ctx.devices_filtered_at_setup = sorted(initial_device_names - filtered_device_names)
+
+        if not filtered_device_names:
+            msg_parts = ["The inventory is empty after filtering by tags/devices."]
+            if ctx.filters.devices:
+                msg_parts.append(f"Devices filter: {', '.join(sorted(ctx.filters.devices))}.")
+            if ctx.filters.tags:
+                msg_parts.append(f"Tags filter: {', '.join(sorted(ctx.filters.tags))}.")
+            msg_parts.append("Exiting ...")
+            self._log_warning_msg(msg=" ".join(msg_parts), ctx=ctx)
+            return False
+
+        # In dry-run mode, set the selected inventory to the filtered inventory
+        if ctx.dry_run:
+            ctx.selected_inventory = filtered_inventory
+            return True
+
+        # Attempt to connect to devices that passed filters
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable if required
+        ctx.selected_inventory = filtered_inventory.get_inventory(established_only=True) if ctx.filters.established_only else filtered_inventory
+        selected_device_names = set(ctx.selected_inventory.keys())
+        ctx.devices_unreachable_at_setup = sorted(filtered_device_names - selected_device_names)
+
+        if not selected_device_names:
+            msg = "No reachable devices found for testing after connectivity checks. Exiting ..."
+            self._log_warning_msg(msg=msg, ctx=ctx)
+            return False
+
+        return True
+
+    def _setup_tests(self, ctx: AntaRunContext) -> bool:
+        """Set up tests for the ANTA run.
+
+        Returns True if the test setup was successful, otherwise False.
+        """
+        # Build indexes for the catalog. If `ctx.filters.tests` is set, filter the indexes based on these tests
+        ctx.catalog.build_indexes(filtered_tests=ctx.filters.tests)
+
+        # Create the device to tests mapping from the tags
+        for device in ctx.selected_inventory.devices:
+            if ctx.filters.tags:
+                # If there are CLI tags, execute tests with matching tags for this device
+                if not (matching_tags := ctx.filters.tags.intersection(device.tags)):
+                    # The device does not have any selected tag, skipping
+                    # This should not never happen because the device will already be filtered by `_setup_inventory`
+                    continue
+                ctx.selected_tests[device].update(ctx.catalog.get_tests_by_tags(matching_tags))
+            else:
+                # If there is no CLI tags, execute all tests that do not have any tags
+                ctx.selected_tests[device].update(ctx.catalog.tag_to_tests[None])
+
+                # Then add the tests with matching tags from device tags
+                ctx.selected_tests[device].update(ctx.catalog.get_tests_by_tags(device.tags))
+
+        if ctx.total_tests_scheduled == 0:
+            msg_parts = ["No tests scheduled to run after filtering by tags/tests."]
+            if ctx.filters.tests:
+                msg_parts.append(f"Tests filter: {', '.join(sorted(ctx.filters.tests))}.")
+            if ctx.filters.tags:
+                msg_parts.append(f"Tags filter: {', '.join(sorted(ctx.filters.tags))}.")
+            msg_parts.append("Exiting ...")
+            self._log_warning_msg(msg=" ".join(msg_parts), ctx=ctx)
+            return False
+
+        return True
+
+    def _get_test_coroutines(self, ctx: AntaRunContext) -> list[Coroutine[Any, Any, TestResult]]:
+        """Get the test coroutines for the ANTA run."""
+        coros = []
+        for device, test_definitions in ctx.selected_tests.items():
+            for test_def in test_definitions:
+                try:
+                    coros.append(test_def.test(device=device, inputs=test_def.inputs).test())
+                except Exception as exc:  # noqa: BLE001, PERF203
+                    # An AntaTest instance is potentially user-defined code.
+                    # We need to catch everything and exit gracefully with an error message.
+                    msg = "\n".join(
+                        [
+                            f"There is an error when creating test {test_def.test.__module__}.{test_def.test.__name__}.",
+                            f"If this is not a custom test implementation: {GITHUB_SUGGESTION}",
+                        ],
+                    )
+                    anta_log_exception(exc, msg, logger)
+        return coros
+
+    def _close_test_coroutines(self, coros: list[Coroutine[Any, Any, TestResult]], ctx: AntaRunContext) -> None:
+        """Close the test coroutines. Used in dry-run."""
+        for coro in coros:
+            # Get the AntaTest instance from the coroutine locals, can be in `args` when decorated
+            coro_locals = getcoroutinelocals(coro)
+            test = coro_locals.get("self") or coro_locals.get("args", (None))[0]
+            if isinstance(test, AntaTest):
+                ctx.manager.add(test.result)
+            else:
+                logger.error("Coroutine %s does not have an AntaTest instance.", coro)
+            coro.close()
+
+    def _log_run_information(self, ctx: AntaRunContext) -> None:
+        """Log ANTA run information and potential resource limit warnings."""
+        # 34 is an estimate of the combined length of timestamp, log level name, filename and spacing added by the Rich logger
+        width = min(int(console.width) - 34, len("  Potential connections needed: 100000000\n"))
+
+        # Build device information
+        device_lines = [
+            "Devices:",
+            f"  Total in initial inventory: {ctx.total_devices_in_inventory}",
+        ]
+        if ctx.total_devices_filtered_by_tags > 0:
+            device_lines.append(f"  Excluded by tags: {ctx.total_devices_filtered_by_tags}")
+        if ctx.total_devices_unreachable > 0:
+            device_lines.append(f"  Failed to connect: {ctx.total_devices_unreachable}")
+        device_lines.append(f"  Selected for testing: {ctx.total_devices_selected_for_testing}")
+        joined_device_lines = "\n".join(device_lines)
+
+        # Build title
+        title = " ANTA NRFU Dry Run Information " if ctx.dry_run else " ANTA NRFU Run Information "
+        formatted_title_line = f"{title:-^{width}}"
+
+        # Log run information
+        run_info = "\n".join(
+            [
+                f"{formatted_title_line}",
+                f"{joined_device_lines}",
+                f"Total number of selected tests: {ctx.total_tests_scheduled}",
+                f"{'':-^{width}}",
+            ]
+        )
+        logger.info(run_info)
+        logger.debug("Max concurrent tests: %d", self._settings.max_concurrency)
+        if potential_connections := ctx.selected_inventory.max_potential_connections:
+            logger.debug("Potential connections needed: %d", potential_connections)
+        logger.debug("File descriptors limit: %d", self._settings.file_descriptor_limit)
+
+        # Log warnings for potential resource limits
+        if ctx.total_tests_scheduled > self._settings.max_concurrency:
+            msg = (
+                f"Tests count ({ctx.total_tests_scheduled}) exceeds concurrent limit ({self._settings.max_concurrency}). "
+                "Tests will be throttled. Please consult the ANTA FAQ."
+            )
+            self._log_warning_msg(msg=msg, ctx=ctx)
+        if potential_connections is not None and potential_connections > self._settings.file_descriptor_limit:
+            msg = (
+                f"Potential connections ({potential_connections}) exceeds file descriptor limit ({self._settings.file_descriptor_limit}). "
+                "Connection errors may occur. Please consult the ANTA FAQ."
+            )
+            self._log_warning_msg(msg=msg, ctx=ctx)
+
+    def _log_cache_statistics(self, ctx: AntaRunContext) -> None:
+        """Log cache statistics for each device in the inventory."""
+        for device in ctx.selected_inventory.devices:
+            if device.cache_statistics is not None:
+                msg = (
+                    f"Cache statistics for '{device.name}': "
+                    f"{device.cache_statistics['cache_hits']} hits / {device.cache_statistics['total_commands_sent']} "
+                    f"command(s) ({device.cache_statistics['cache_hit_ratio']})"
+                )
+                logger.debug(msg)
+            else:
+                logger.debug("Caching is not enabled on %s", device.name)
+
+    def _log_warning_msg(self, msg: str, ctx: AntaRunContext) -> None:
+        """Log the provided message at WARNING level and add it to the context warnings_at_setup list."""
+        logger.warning(msg)
+        ctx.warnings_at_setup.append(msg)
diff --git a/anta/cli/nrfu/utils.py b/anta/cli/nrfu/utils.py
index 6b5077bb7..9227bfead 100644
--- a/anta/cli/nrfu/utils.py
+++ b/anta/cli/nrfu/utils.py
@@ -50,6 +50,7 @@ def run_tests(ctx: click.Context) -> None:
 
     print_settings(inventory, catalog)
     with anta_progress_bar() as AntaTest.progress:
+        # TODO: Use AntaRunner directly in ANTA v2.0.0
         asyncio.run(
             main(
                 ctx.obj["result_manager"],
diff --git a/anta/cli/utils.py b/anta/cli/utils.py
index 34b96b3fd..019c54ad1 100644
--- a/anta/cli/utils.py
+++ b/anta/cli/utils.py
@@ -163,6 +163,7 @@ def core_options(f: Callable[..., Any]) -> Callable[..., Any]:
         show_envvar=True,
         envvar="ANTA_TIMEOUT",
         show_default=True,
+        type=float,
     )
     @click.option(
         "--insecure",
diff --git a/anta/device.py b/anta/device.py
index 8ca4e3f30..544f63ee8 100644
--- a/anta/device.py
+++ b/anta/device.py
@@ -114,16 +114,19 @@ class AntaDevice(ABC):
         True if the device IP is reachable and a port can be open.
     established : bool
         True if remote command execution succeeds.
-    hw_model : str
+    hw_model : str | None
         Hardware model of the device.
     tags : set[str]
         Tags for this device.
     cache : AntaCache | None
         In-memory cache for this device (None if cache is disabled).
-    cache_locks : dict
+    cache_locks : defaultdict[str, asyncio.Lock] | None
         Dictionary mapping keys to asyncio locks to guarantee exclusive access to the cache if not disabled.
         Deprecated, will be removed in ANTA v2.0.0, use self.cache.locks instead.
-
+    max_connections : int | None
+        For informational/logging purposes only. Can be used by the runner to verify that
+        the total potential connections of a run do not exceed the system file descriptor limit.
+        This does **not** affect the actual device configuration. None if not available.
     """
 
     def __init__(self, name: str, tags: set[str] | None = None, *, disable_cache: bool = False) -> None:
@@ -159,6 +162,11 @@ def __init__(self, name: str, tags: set[str] | None = None, *, disable_cache: bo
     def _keys(self) -> tuple[Any, ...]:
         """Read-only property to implement hashing and equality for AntaDevice classes."""
 
+    @property
+    def max_connections(self) -> int | None:
+        """Maximum number of concurrent connections allowed by the device. Can be overridden by subclasses, returns None if not available."""
+        return None
+
     def __eq__(self, other: object) -> bool:
         """Implement equality for AntaDevice objects."""
         return self._keys == other._keys if isinstance(other, self.__class__) else False
@@ -302,9 +310,8 @@ async def copy(self, sources: list[Path], destination: Path, direction: Literal[
         raise NotImplementedError(msg)
 
 
-# pylint: disable=too-many-instance-attributes
 class AsyncEOSDevice(AntaDevice):
-    """Implementation of AntaDevice for EOS using aio-eapi.
+    """Implementation of AntaDevice for EOS using the `asynceapi` library, which is built on HTTPX.
 
     Attributes
     ----------
@@ -318,7 +325,6 @@ class AsyncEOSDevice(AntaDevice):
         Hardware model of the device.
     tags : set[str]
         Tags for this device.
-
     """
 
     def __init__(  # noqa: PLR0913
@@ -329,7 +335,7 @@ def __init__(  # noqa: PLR0913
         name: str | None = None,
         enable_password: str | None = None,
         port: int | None = None,
-        ssh_port: int | None = 22,
+        ssh_port: int = 22,
         tags: set[str] | None = None,
         timeout: float | None = None,
         proto: Literal["http", "https"] = "https",
@@ -350,8 +356,6 @@ def __init__(  # noqa: PLR0913
             Password to connect to eAPI and SSH.
         name
             Device name.
-        enable
-            Collect commands using privileged mode.
         enable_password
             Password used to gain privileged access on EOS.
         port
@@ -361,14 +365,15 @@ def __init__(  # noqa: PLR0913
         tags
             Tags for this device.
         timeout
-            Timeout value in seconds for outgoing API calls.
-        insecure
-            Disable SSH Host Key validation.
+            Global timeout value in seconds for outgoing eAPI calls. None means no timeout.
         proto
             eAPI protocol. Value can be 'http' or 'https'.
+        enable
+            Collect commands using privileged mode.
+        insecure
+            Disable SSH Host Key validation.
         disable_cache
             Disable caching for all commands for this device.
-
         """
         if host is None:
             message = "'host' is required to create an AsyncEOSDevice"
@@ -417,6 +422,7 @@ def __rich_repr__(self) -> Iterator[tuple[str, Any]]:
             _ssh_opts["kwargs"]["password"] = removed_pw
             yield ("_session", vars(self._session))
             yield ("_ssh_opts", _ssh_opts)
+            yield ("max_connections", self.max_connections) if self.max_connections is not None else ("max_connections", "N/A")
 
     def __repr__(self) -> str:
         """Return a printable representation of an AsyncEOSDevice."""
@@ -442,6 +448,14 @@ def _keys(self) -> tuple[Any, ...]:
         """
         return (self._session.host, self._session.port)
 
+    @property
+    def max_connections(self) -> int | None:
+        """Maximum number of concurrent connections allowed by the device. Returns None if not available."""
+        try:
+            return self._session._transport._pool._max_connections  # type: ignore[attr-defined]  # noqa: SLF001
+        except AttributeError:
+            return None
+
     async def _get_semaphore(self) -> asyncio.Semaphore:
         """Return the semaphore, initializing it if needed.
 
diff --git a/anta/inventory/__init__.py b/anta/inventory/__init__.py
index a74638ebe..72fca4b7f 100644
--- a/anta/inventory/__init__.py
+++ b/anta/inventory/__init__.py
@@ -200,7 +200,7 @@ def parse(
         enable_password
             Enable password to use if required.
         timeout
-            Timeout value in seconds for outgoing API calls.
+            Global timeout value in seconds for outgoing eAPI calls. None means no timeout.
         file_format
             Whether the inventory file is in JSON or YAML.
         enable
@@ -265,6 +265,11 @@ def devices(self) -> list[AntaDevice]:
         """List of AntaDevice in this inventory."""
         return list(self.values())
 
+    @property
+    def max_potential_connections(self) -> int | None:
+        """Max potential connections of this inventory."""
+        return self._get_potential_connections()
+
     ###########################################################################
     # Public methods
     ###########################################################################
@@ -305,6 +310,29 @@ def _filter_devices(device: AntaDevice) -> bool:
             result.add_device(device)
         return result
 
+    def _get_potential_connections(self) -> int | None:
+        """Calculate the total potential concurrent connections for the current inventory.
+
+        This method sums the maximum concurrent connections allowed for each
+        AntaDevice in the inventory.
+
+        Returns
+        -------
+        int | None
+            The total sum of the `max_connections` attribute for all AntaDevice objects
+            in the inventory. Returns None if any AntaDevice does not have a `max_connections`
+            attribute or if its value is None, as the total count cannot be determined.
+        """
+        potential_connections = 0
+        all_have_connections = True
+        for device in self.devices:
+            if device.max_connections is None:
+                all_have_connections = False
+                logger.debug("Device %s 'max_connections' is not available", device.name)
+                break
+            potential_connections += device.max_connections
+        return None if not all_have_connections else potential_connections
+
     ###########################################################################
     # SET methods
     ###########################################################################
diff --git a/anta/reporter/__init__.py b/anta/reporter/__init__.py
index 5156ea7e8..b6f061c10 100644
--- a/anta/reporter/__init__.py
+++ b/anta/reporter/__init__.py
@@ -29,7 +29,7 @@ class ReportTable:
     """TableReport Generate a Table based on TestResult."""
 
     @dataclass
-    class Headers:  # pylint: disable=too-many-instance-attributes
+    class Headers:
         """Headers for the table report."""
 
         device: str = "Device"
diff --git a/anta/result_manager/__init__.py b/anta/result_manager/__init__.py
index ca3f1c4d9..32e9da486 100644
--- a/anta/result_manager/__init__.py
+++ b/anta/result_manager/__init__.py
@@ -21,7 +21,6 @@
 logger = logging.getLogger(__name__)
 
 
-# pylint: disable=too-many-instance-attributes
 class ResultManager:
     """Manager of ANTA Results.
 
diff --git a/anta/result_manager/models.py b/anta/result_manager/models.py
index a18ff579c..a19c969de 100644
--- a/anta/result_manager/models.py
+++ b/anta/result_manager/models.py
@@ -122,8 +122,6 @@ def __str__(self) -> str:
         return f"Test '{self.test}' (on '{self.name}'): Result '{self.result}'\nMessages: {self.messages}"
 
 
-# Pylint does not treat dataclasses differently: https://github.com/pylint-dev/pylint/issues/9058
-# pylint: disable=too-many-instance-attributes
 @dataclass
 class DeviceStats:
     """Device statistics for a run of tests."""
diff --git a/anta/runner.py b/anta/runner.py
index c5880ff64..736da50d1 100644
--- a/anta/runner.py
+++ b/anta/runner.py
@@ -5,16 +5,16 @@
 
 from __future__ import annotations
 
-import asyncio
 import logging
 import os
-import sys
 from collections import defaultdict
 from typing import TYPE_CHECKING, Any
 
+from typing_extensions import deprecated
+
 from anta import GITHUB_SUGGESTION
+from anta._runner import AntaRunFilters, AntaRunner
 from anta.logger import anta_log_exception, exc_to_str
-from anta.models import AntaTest
 from anta.tools import Catchtime, cprofile
 
 if TYPE_CHECKING:
@@ -31,6 +31,7 @@
 
     DEFAULT_NOFILE = 16384
 
+    @deprecated("This function is deprecated and will be removed in ANTA v2.0.0. Use AntaRunner class instead.", category=DeprecationWarning)
     def adjust_rlimit_nofile() -> tuple[int, int]:
         """Adjust the maximum number of open file descriptors for the ANTA process.
 
@@ -63,6 +64,7 @@ def adjust_rlimit_nofile() -> tuple[int, int]:
 logger = logging.getLogger(__name__)
 
 
+@deprecated("This function is deprecated and will be removed in ANTA v2.0.0. Use AntaRunner class instead.", category=DeprecationWarning)
 def log_cache_statistics(devices: list[AntaDevice]) -> None:
     """Log cache statistics for each device in the inventory.
 
@@ -83,6 +85,7 @@ def log_cache_statistics(devices: list[AntaDevice]) -> None:
             logger.info("Caching is not enabled on %s", device.name)
 
 
+@deprecated("This function is deprecated and will be removed in ANTA v2.0.0. Use AntaRunner class instead.", category=DeprecationWarning)
 async def setup_inventory(inventory: AntaInventory, tags: set[str] | None, devices: set[str] | None, *, established_only: bool) -> AntaInventory | None:
     """Set up the inventory for the ANTA run.
 
@@ -125,6 +128,7 @@ async def setup_inventory(inventory: AntaInventory, tags: set[str] | None, devic
     return selected_inventory
 
 
+@deprecated("This function is deprecated and will be removed in ANTA v2.0.0. Use AntaRunner class instead.", category=DeprecationWarning)
 def prepare_tests(
     inventory: AntaInventory, catalog: AntaCatalog, tests: set[str] | None, tags: set[str] | None
 ) -> defaultdict[AntaDevice, set[AntaTestDefinition]] | None:
@@ -181,6 +185,7 @@ def prepare_tests(
     return device_to_tests
 
 
+@deprecated("This function is deprecated and will be removed in ANTA v2.0.0. Use AntaRunner class instead.", category=DeprecationWarning)
 def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]], manager: ResultManager | None = None) -> list[Coroutine[Any, Any, TestResult]]:
     """Get the coroutines for the ANTA run.
 
@@ -253,62 +258,11 @@ async def main(
     dry_run
         Build the list of coroutine to run and stop before test execution.
     """
-    if not catalog.tests:
-        logger.info("The list of tests is empty, exiting")
-        return
-
-    with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
-        # Setup the inventory
-        selected_inventory = inventory if dry_run else await setup_inventory(inventory, tags, devices, established_only=established_only)
-        if selected_inventory is None:
-            return
-
-        with Catchtime(logger=logger, message="Preparing the tests"):
-            selected_tests = prepare_tests(selected_inventory, catalog, tests, tags)
-            if selected_tests is None:
-                return
-            final_tests_count = sum(len(tests) for tests in selected_tests.values())
-
-        run_info = (
-            "--- ANTA NRFU Run Information ---\n"
-            f"Number of devices: {len(inventory)} ({len(selected_inventory)} established)\n"
-            f"Total number of selected tests: {final_tests_count}\n"
-        )
-
-        if os.name == "posix":
-            # Adjust the maximum number of open file descriptors for the ANTA process
-            limits = adjust_rlimit_nofile()
-            run_info += f"Maximum number of open file descriptors for the current ANTA process: {limits[0]}\n"
-        else:
-            # Running on non-Posix system, cannot manage the resource.
-            limits = (sys.maxsize, sys.maxsize)
-            run_info += "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors.\n"
-
-        run_info += "---------------------------------"
-
-        logger.info(run_info)
-
-        if final_tests_count > limits[0]:
-            logger.warning(
-                "The number of concurrent tests is higher than the open file descriptors limit for this ANTA process.\n"
-                "Errors may occur while running the tests.\n"
-                "Please consult the ANTA FAQ."
-            )
-
-        coroutines = get_coroutines(selected_tests, manager if dry_run else None)
-
-    if dry_run:
-        logger.info("Dry-run mode, exiting before running the tests.")
-        for coro in coroutines:
-            coro.close()
-        return
-
-    if AntaTest.progress is not None:
-        AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=len(coroutines))
-
-    with Catchtime(logger=logger, message="Running ANTA tests"):
-        results = await asyncio.gather(*coroutines)
-        for result in results:
-            manager.add(result)
-
-    log_cache_statistics(selected_inventory.devices)
+    runner = AntaRunner()
+    filters = AntaRunFilters(
+        devices=devices,
+        tests=tests,
+        tags=tags,
+        established_only=established_only,
+    )
+    await runner.run(inventory, catalog, manager, filters, dry_run=dry_run)
diff --git a/anta/settings.py b/anta/settings.py
new file mode 100644
index 000000000..88fdf442d
--- /dev/null
+++ b/anta/settings.py
@@ -0,0 +1,86 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from typing import Any
+
+from pydantic import Field, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+from anta.logger import exc_to_str
+
+logger = logging.getLogger(__name__)
+
+DEFAULT_MAX_CONCURRENCY = 50000
+"""Default value for the maximum number of concurrent tests in the event loop."""
+
+DEFAULT_NOFILE = 16384
+"""Default value for the maximum number of open file descriptors for the ANTA process."""
+
+
+class AntaRunnerSettings(BaseSettings):
+    """Environment variables for configuring the ANTA runner.
+
+    When initialized, relevant environment variables are loaded. If not set, default values are used.
+
+    On POSIX systems, also adjusts the process soft limit based on the `ANTA_NOFILE` environment variable
+    while respecting the system hard limit, meaning the new soft limit cannot exceed the system's hard limit.
+
+    On non-POSIX systems (Windows), sets the limit to `sys.maxsize`.
+
+    The adjusted limit is available with the `file_descriptor_limit` property after initialization.
+
+    Attributes
+    ----------
+    nofile : PositiveInt
+        Environment variable: ANTA_NOFILE
+
+        The maximum number of open file descriptors for the ANTA process. Defaults to 16384.
+
+    max_concurrency : PositiveInt
+        Environment variable: ANTA_MAX_CONCURRENCY
+
+        The maximum number of concurrent tests that can run in the event loop. Defaults to 50000.
+    """
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    nofile: PositiveInt = Field(default=DEFAULT_NOFILE)
+    max_concurrency: PositiveInt = Field(default=DEFAULT_MAX_CONCURRENCY)
+
+    # Computed in post-init
+    _file_descriptor_limit: PositiveInt
+
+    # pylint: disable=arguments-differ
+    def model_post_init(self, _context: Any) -> None:  # noqa: ANN401
+        """Post-initialization method to set the file descriptor limit for the current ANTA process."""
+        if os.name != "posix":
+            logger.warning("Running on a non-POSIX system, cannot adjust the maximum number of file descriptors.")
+            self._file_descriptor_limit = sys.maxsize
+            return
+
+        import resource
+
+        limits = resource.getrlimit(resource.RLIMIT_NOFILE)
+        logger.debug("Initial file descriptor limits for the current ANTA process: Soft Limit: %s | Hard Limit: %s", limits[0], limits[1])
+
+        # Set new soft limit to minimum of requested and hard limit
+        new_soft_limit = min(limits[1], self.nofile)
+        logger.debug("Setting file descriptor soft limit to %s", new_soft_limit)
+        try:
+            resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft_limit, limits[1]))
+        except ValueError as exception:
+            logger.warning("Failed to set file descriptor soft limit for the current ANTA process: %s", exc_to_str(exception))
+
+        self._file_descriptor_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
+
+    @property
+    def file_descriptor_limit(self) -> PositiveInt:
+        """The maximum number of file descriptors available to the process."""
+        return self._file_descriptor_limit
diff --git a/anta/tools.py b/anta/tools.py
index cbcfe0b5f..45efb13d4 100644
--- a/anta/tools.py
+++ b/anta/tools.py
@@ -290,7 +290,7 @@ def __enter__(self) -> Self:
         """__enter__ method."""
         self.start = perf_counter()
         if self.logger and self.message:
-            self.logger.info("%s ...", self.message)
+            self.logger.debug("%s ...", self.message)
         return self
 
     def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None) -> None:
@@ -298,7 +298,7 @@ def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException
         self.raw_time = perf_counter() - self.start
         self.time = format_td(self.raw_time, 3)
         if self.logger and self.message:
-            self.logger.info("%s completed in: %s.", self.message, self.time)
+            self.logger.debug("%s completed in: %s.", self.message, self.time)
 
 
 def cprofile(sort_by: str = "cumtime") -> Callable[[F], F]:
diff --git a/asynceapi/_models.py b/asynceapi/_models.py
index 0572a2f39..21ad661c1 100644
--- a/asynceapi/_models.py
+++ b/asynceapi/_models.py
@@ -21,7 +21,6 @@
 LOGGER = getLogger(__name__)
 
 
-# pylint: disable=too-many-instance-attributes
 @dataclass(frozen=True)
 class EapiRequest:
     """Model for an eAPI request.
diff --git a/docs/api/settings.md b/docs/api/settings.md
new file mode 100644
index 000000000..bbc45cae4
--- /dev/null
+++ b/docs/api/settings.md
@@ -0,0 +1,13 @@
+---
+anta_title: ANTA Settings
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+### ::: anta.settings
+
+    options:
+      show_root_full_path: true
diff --git a/docs/faq.md b/docs/faq.md
index 204972452..c61082fa8 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -30,7 +30,7 @@ anta_title: Frequently Asked Questions (FAQ)
 
     This usually means that the operating system refused to open a new file descriptor (or socket) for the ANTA process. This might be due to the hard limit for open file descriptors currently set for the ANTA process.
 
-    At startup, ANTA sets the soft limit of its process to the hard limit up to 16384. This is because the soft limit is usually 1024 and the hard limit is usually higher (depends on the system). If the hard limit of the ANTA process is still lower than the number of selected tests in ANTA, the ANTA process may request to the operating system too many file descriptors and get an error, a WARNING is displayed at startup if this is the case.
+    At startup, ANTA sets the soft limit of its process to the hard limit up to 16384. This is because the soft limit is usually 1024 and the hard limit is usually higher (depends on the system). If the hard limit of the ANTA process is still lower than the potential connections of all devices, the ANTA process may request to the operating system too many file descriptors and get an error, a WARNING is displayed at startup if this is the case.
 
     ### Solution
 
@@ -43,11 +43,35 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **50000** tests concurrently. This should be sufficient for most use cases, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA executes the first 50000 tests and waits for some tests to complete before executing more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system CPU usage, memory consumption, and file descriptor limits.
+
+    !!! warning
+
+        Increasing the maximum concurrency limit can lead to system instability if the system is not able to handle the increased load. Monitor system resources and adjust the limit accordingly.
+
+    !!! info "Device Connection Limits"
+
+        Each EOS device is limited to a maximum of **100** concurrent connections. This means that, even if ANTA schedules a high number of tests, it will only attempt to open up to 100 connections at a time towards each device.
+
+    !!! tip
+        If you run ANTA on a large fabric or encounter issues related to resource limits, consider tuning `ANTA_MAX_CONCURRENCY`.
+        Test different values to find the optimal setting for your environment.
+
 ## `Timeout` error in the logs
 
 ???+ faq "`Timeout` error in the logs"
 
-    When running ANTA, you can receive `<Foo>Timeout` errors in the logs (could be ReadTimeout, WriteTimeout, ConnectTimeout or PoolTimeout). More details on the timeouts of the underlying library are available here: https://www.python-httpx.org/advanced/timeouts.
+    When running ANTA, you can receive `<Foo>Timeout` errors in the logs (could be `ReadTimeout`, `WriteTimeout`, `ConnectTimeout` or `PoolTimeout`). More details on the timeouts of the underlying library are available here: https://www.python-httpx.org/advanced/timeouts.
 
     This might be due to the time the host on which ANTA is run takes to reach the target devices (for instance if going through firewalls, NATs, ...) or when a lot of tests are being run at the same time on a device (eAPI has a queue mechanism to avoid exhausting EOS resources because of a high number of simultaneous eAPI requests).
 
@@ -59,8 +83,7 @@ anta_title: Frequently Asked Questions (FAQ)
     anta nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml --timeout 50 text
     ```
 
-    The previous command set a couple of options for ANTA NRFU, one them being the `timeout` command, by default, when running ANTA from CLI, it is set to 30s.
-    The timeout is increased to 50s to allow ANTA to wait for API calls a little longer.
+    In this command, ANTA NRFU is configured with several options. Notably, the `--timeout` parameter is set to 50 seconds (instead of the default 30 seconds) to allow extra time for API calls to complete.
 
 ## `ImportError` related to `urllib3`
 
diff --git a/mkdocs.yml b/mkdocs.yml
index b8e70ab80..ee29a925a 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -2,7 +2,7 @@
 site_name: Arista Network Test Automation - ANTA
 site_author: Khelil Sator
 site_description: Arista Network Test Automation
-copyright: Copyright &copy; 2019 - 2024 Arista Networks
+copyright: Copyright &copy; 2019 - 2025 Arista Networks
 
 # Repository
 repo_name: ANTA on Github
@@ -185,9 +185,9 @@ nav:
       - Debug commands: cli/debug.md
       - Tag Management: cli/tag-management.md
   - Advanced Usages:
-      - Caching in ANTA: advanced_usages/caching.md
-      - Developing ANTA tests: advanced_usages/custom-tests.md
-      - ANTA as a Python Library: advanced_usages/as-python-lib.md
+    - Caching in ANTA: advanced_usages/caching.md
+    - Developing ANTA tests: advanced_usages/custom-tests.md
+    - ANTA as a Python Library: advanced_usages/as-python-lib.md
   - Tests Documentation:
       - Overview: api/tests.md
       - AAA: api/tests/aaa.md
@@ -240,6 +240,7 @@ nav:
           - CSV: api/reporter/csv.md
           - Jinja: api/reporter/jinja.md
       - Runner: api/runner.md
+      - Settings: api/settings.md
   - Troubleshooting ANTA: troubleshooting.md
   - Contributions: contribution.md
   - FAQ: faq.md
diff --git a/pyproject.toml b/pyproject.toml
index 88df7efc8..60bf3d393 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,6 +25,7 @@ dependencies = [
   "Jinja2>=3.1.2",
   "pydantic>=2.7",
   "pydantic-extra-types>=2.3.0",
+  "pydantic-settings>=2.6.0",
   "PyYAML>=6.0",
   "requests>=2.31.0",
   "rich>=13.5.2,<15",
@@ -492,3 +493,5 @@ min-similarity-lines=10
 signature-mutators="click.decorators.option"
 load-plugins="pylint_pydantic"
 extension-pkg-whitelist="pydantic"
+# Pylint does not treat dataclasses differently: https://github.com/pylint-dev/pylint/issues/9058
+max-attributes=15
diff --git a/tests/benchmark/test_anta.py b/tests/benchmark/test_anta.py
index 1daf7f369..eb128ce42 100644
--- a/tests/benchmark/test_anta.py
+++ b/tests/benchmark/test_anta.py
@@ -37,6 +37,7 @@ def test_anta_dry_run(
 
     results = session_results[request.node.callspec.id]
 
+    # TODO: Use AntaRunner directly in ANTA v2.0.0
     @benchmark
     def _() -> None:
         results.reset()
@@ -69,6 +70,7 @@ def test_anta(
 
     results = session_results[request.node.callspec.id]
 
+    # TODO: Use AntaRunner directly in ANTA v2.0.0
     @benchmark
     def _() -> None:
         results.reset()
diff --git a/tests/benchmark/test_runner.py b/tests/benchmark/test_runner.py
index 9aa54df27..f3b713e39 100644
--- a/tests/benchmark/test_runner.py
+++ b/tests/benchmark/test_runner.py
@@ -7,6 +7,9 @@
 
 from typing import TYPE_CHECKING, Any
 
+import pytest
+
+from anta._runner import AntaRunContext, AntaRunFilters, AntaRunner
 from anta.result_manager import ResultManager
 from anta.runner import get_coroutines, prepare_tests
 
@@ -22,6 +25,8 @@
     from anta.result_manager.models import TestResult
 
 
+# TODO: Remove this in ANTA v2.0.0
+@pytest.mark.filterwarnings("ignore::DeprecationWarning")
 def test_prepare_tests(benchmark: BenchmarkFixture, catalog: AntaCatalog, inventory: AntaInventory) -> None:
     """Benchmark `anta.runner.prepare_tests`."""
 
@@ -36,6 +41,8 @@ def _() -> defaultdict[AntaDevice, set[AntaTestDefinition]] | None:
     assert sum(len(tests) for tests in selected_tests.values()) == len(inventory) * len(catalog.tests)
 
 
+# TODO: Remove this in ANTA v2.0.0
+@pytest.mark.filterwarnings("ignore::DeprecationWarning")
 def test_get_coroutines(benchmark: BenchmarkFixture, catalog: AntaCatalog, inventory: AntaInventory) -> None:
     """Benchmark `anta.runner.get_coroutines`."""
     selected_tests = prepare_tests(inventory=inventory, catalog=catalog, tests=None, tags=None)
@@ -52,3 +59,38 @@ def bench() -> list[Coroutine[Any, Any, TestResult]]:
 
     count = sum(len(tests) for tests in selected_tests.values())
     assert count == len(coroutines)
+
+
+def test__setup_tests(benchmark: BenchmarkFixture, catalog: AntaCatalog, inventory: AntaInventory) -> None:
+    """Benchmark `anta._runner.AntaRunner._setup_tests`."""
+    runner = AntaRunner()
+    ctx = AntaRunContext(inventory=inventory, catalog=catalog, manager=ResultManager(), filters=AntaRunFilters(), selected_inventory=inventory)
+
+    def bench() -> None:
+        catalog.clear_indexes()
+        runner._setup_tests(ctx)
+
+    benchmark(bench)
+
+    assert ctx.total_tests_scheduled != 0
+    assert ctx.total_devices_selected_for_testing == len(inventory)
+    assert ctx.total_tests_scheduled == len(inventory) * len(catalog.tests)
+
+
+def test__get_test_coroutines(benchmark: BenchmarkFixture, catalog: AntaCatalog, inventory: AntaInventory) -> None:
+    """Benchmark `anta._runner.AntaRunner._get_test_coroutines`."""
+    runner = AntaRunner()
+    ctx = AntaRunContext(inventory=inventory, catalog=catalog, manager=ResultManager(), filters=AntaRunFilters(), selected_inventory=inventory)
+    runner._setup_tests(ctx)
+
+    assert ctx.selected_tests is not None
+
+    def bench() -> list[Coroutine[Any, Any, TestResult]]:
+        coros = runner._get_test_coroutines(ctx)
+        for c in coros:
+            c.close()
+        return coros
+
+    coroutines = benchmark(bench)
+
+    assert ctx.total_tests_scheduled == len(coroutines)
diff --git a/tests/units/conftest.py b/tests/units/conftest.py
index 49c786f00..398772295 100644
--- a/tests/units/conftest.py
+++ b/tests/units/conftest.py
@@ -5,8 +5,10 @@
 
 from __future__ import annotations
 
+import os
 from pathlib import Path
 from typing import TYPE_CHECKING, Any
+from unittest import mock
 from unittest.mock import patch
 
 import pytest
@@ -15,7 +17,7 @@
 from anta.device import AntaDevice, AsyncEOSDevice
 
 if TYPE_CHECKING:
-    from collections.abc import Iterator
+    from collections.abc import Generator, Iterator
 
     from anta.models import AntaCommand
 
@@ -83,3 +85,10 @@ def yaml_file(request: pytest.FixtureRequest, tmp_path: Path) -> Path:
     content: dict[str, Any] = request.param
     file.write_text(yaml.dump(content, allow_unicode=True))
     return file
+
+
+@pytest.fixture
+def setenvvar(monkeypatch: pytest.MonkeyPatch) -> Generator[pytest.MonkeyPatch, None, None]:
+    """Fixture to set environment variables for testing."""
+    with mock.patch.dict(os.environ, clear=True):
+        yield monkeypatch
diff --git a/tests/units/inventory/test__init__.py b/tests/units/inventory/test__init__.py
index 461f03797..45cdb27d3 100644
--- a/tests/units/inventory/test__init__.py
+++ b/tests/units/inventory/test__init__.py
@@ -19,6 +19,9 @@
 
     from _pytest.mark.structures import ParameterSet
 
+    from anta.device import AntaDevice, AsyncEOSDevice
+
+
 INIT_VALID_PARAMS: list[ParameterSet] = [
     pytest.param(
         {"anta_inventory": {"hosts": [{"host": "192.168.0.17"}, {"host": "192.168.0.2"}, {"host": "my.awesome.host.com"}]}},
@@ -87,3 +90,22 @@ def test_parse_os_error(self, caplog: pytest.LogCaptureFixture) -> None:
         with pytest.raises(OSError, match="No such file or directory"):
             _ = AntaInventory.parse(filename="dummy.yml", username="arista", password="arista123")
         assert "Unable to parse ANTA Device Inventory file" in caplog.records[0].message
+
+    @pytest.mark.parametrize(("inventory"), [{"count": 3}], indirect=True)
+    def test_max_potential_connections(self, inventory: AntaInventory) -> None:
+        """Test max_potential_connections property with regular AsyncEOSDevice objects in the inventory."""
+        # Each AsyncEOSDevice has a max_connections of 100
+        assert inventory.max_potential_connections == 300
+
+    @pytest.mark.parametrize(("device"), [{"name": "anta_device"}], indirect=True)
+    def test_get_potential_connections_custom_anta_device(self, caplog: pytest.LogCaptureFixture, async_device: AsyncEOSDevice, device: AntaDevice) -> None:
+        """Test max_potential_connections property with an AntaDevice with no max_connections in the inventory."""
+        caplog.set_level(logging.DEBUG)
+
+        inventory = AntaInventory()
+        inventory.add_device(async_device)
+        inventory.add_device(device)
+
+        assert len(inventory) == 2
+        assert inventory.max_potential_connections is None
+        assert "Device anta_device 'max_connections' is not available" in caplog.messages
diff --git a/tests/units/test__runner.py b/tests/units/test__runner.py
new file mode 100644
index 000000000..46c63ed02
--- /dev/null
+++ b/tests/units/test__runner.py
@@ -0,0 +1,414 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Test anta._runner.py."""
+
+from __future__ import annotations
+
+import logging
+import os
+from collections import defaultdict
+from pathlib import Path
+from typing import ClassVar
+
+import pytest
+import respx
+from pydantic import ValidationError
+
+from anta._runner import AntaRunContext, AntaRunFilters, AntaRunner
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.inventory import AntaInventory
+from anta.models import AntaCommand, AntaTemplate, AntaTest
+from anta.result_manager import ResultManager
+from anta.result_manager.models import TestResult as AntaTestResult
+from anta.settings import DEFAULT_MAX_CONCURRENCY, DEFAULT_NOFILE, AntaRunnerSettings
+from anta.tests.routing.generic import VerifyRoutingTableEntry
+
+DATA_DIR: Path = Path(__file__).parent.parent.resolve() / "data"
+
+
+class TestAntaRunner:
+    """Test AntaRunner class."""
+
+    def test_init_with_default_settings(self, caplog: pytest.LogCaptureFixture) -> None:
+        """Test initialization with default settings."""
+        caplog.set_level(logging.DEBUG)
+        default_settings = {"nofile": DEFAULT_NOFILE, "max_concurrency": DEFAULT_MAX_CONCURRENCY}
+
+        runner = AntaRunner()
+
+        assert f"AntaRunner initialized with settings: {default_settings}" in caplog.messages
+        assert runner._settings
+
+    def test_init_with_custom_env_settings(self, caplog: pytest.LogCaptureFixture, setenvvar: pytest.MonkeyPatch) -> None:
+        """Test initialization with custom env settings."""
+        caplog.set_level(logging.DEBUG)
+        desired_settings = {"nofile": 1048576, "max_concurrency": 10000}
+        setenvvar.setenv("ANTA_NOFILE", str(desired_settings["nofile"]))
+        setenvvar.setenv("ANTA_MAX_CONCURRENCY", str(desired_settings["max_concurrency"]))
+
+        runner = AntaRunner()
+
+        assert f"AntaRunner initialized with settings: {desired_settings}" in caplog.messages
+        assert runner._settings
+
+    def test_init_with_provided_settings(self, caplog: pytest.LogCaptureFixture) -> None:
+        """Test initialization with provided settings."""
+        caplog.set_level(logging.DEBUG)
+        desired_settings = AntaRunnerSettings(nofile=1048576, max_concurrency=10000)
+
+        runner = AntaRunner(settings=desired_settings)
+
+        assert f"AntaRunner initialized with settings: {desired_settings.model_dump()}" in caplog.messages
+        assert runner._settings
+
+    async def test_dry_run(self, caplog: pytest.LogCaptureFixture) -> None:
+        """Test AntaRunner.run() in dry-run."""
+        caplog.set_level(logging.INFO)
+
+        inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta")
+        catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml")
+        runner = AntaRunner()
+        ctx = await runner.run(inventory, catalog, dry_run=True)
+
+        # Validate the final context attributes
+        assert ctx.selected_inventory == ctx.inventory == inventory
+        assert len(ctx.manager) > 0
+        assert ctx.manager.status == "unset"
+        assert ctx.total_tests_scheduled > 0
+        assert ctx.total_devices_filtered_by_tags == 0
+        assert ctx.total_devices_unreachable == 0
+        assert ctx.total_devices_selected_for_testing == ctx.total_devices_in_inventory == len(inventory)
+        assert ctx.duration is not None
+
+        assert "Dry-run mode, exiting before running the tests." in caplog.messages
+
+    @pytest.mark.parametrize(
+        ("filters", "expected_devices", "expected_tests"),
+        [
+            pytest.param(
+                AntaRunFilters(devices=None, tests=None, tags=None),
+                3,
+                27,
+                id="all-tests",
+            ),
+            pytest.param(
+                AntaRunFilters(devices=None, tests=None, tags={"leaf"}),
+                2,
+                6,
+                id="1-tag",
+            ),
+            pytest.param(
+                AntaRunFilters(devices=None, tests=None, tags={"leaf", "spine"}),
+                3,
+                9,
+                id="2-tags",
+            ),
+            pytest.param(
+                AntaRunFilters(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags=None),
+                3,
+                5,
+                id="filtered-tests",
+            ),
+            pytest.param(
+                AntaRunFilters(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags={"leaf"}),
+                2,
+                4,
+                id="1-tag-filtered-tests",
+            ),
+            pytest.param(
+                AntaRunFilters(devices={"leaf1"}, tests=None, tags=None),
+                1,
+                9,
+                id="filtered-devices",
+            ),
+            pytest.param(
+                AntaRunFilters(devices=None, tests=None, tags={"invalid"}),
+                0,
+                0,
+                id="invalid-tag",
+            ),
+            pytest.param(
+                AntaRunFilters(devices={"invalid"}, tests=None, tags=None),
+                0,
+                0,
+                id="invalid-device",
+            ),
+            pytest.param(
+                AntaRunFilters(devices=None, tests={"invalid"}, tags=None),
+                3,
+                0,
+                id="invalid-test",
+            ),
+            pytest.param(
+                AntaRunFilters(devices=None, tests=None, tags={"dc1"}),
+                1,
+                0,
+                id="device-tag-no-tests",
+            ),
+        ],
+    )
+    async def test_run_filters(self, caplog: pytest.LogCaptureFixture, filters: AntaRunFilters, expected_devices: int, expected_tests: int) -> None:
+        """Test AntaRunner.run() with different filters."""
+        caplog.set_level(logging.WARNING)
+
+        inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta")
+        catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml")
+        runner = AntaRunner()
+        ctx = await runner.run(inventory, catalog, filters=filters, dry_run=True)
+
+        # Gather the warning message
+        warning_msg = None
+        if expected_devices == 0:
+            msg = "The inventory is empty after filtering by tags/devices. "
+            if filters.devices:
+                msg += f"Devices filter: {', '.join(sorted(filters.devices))}. "
+            if filters.tags:
+                msg += f"Tags filter: {', '.join(sorted(filters.tags))}. "
+            msg += "Exiting ..."
+        elif expected_tests == 0:
+            msg = "No tests scheduled to run after filtering by tags/tests. "
+            if filters.tests:
+                msg += f"Tests filter: {', '.join(sorted(filters.tests))}. "
+            if filters.tags:
+                msg += f"Tags filter: {', '.join(sorted(filters.tags))}. "
+            msg += "Exiting ..."
+
+        if warning_msg is not None:
+            assert msg in ctx.warnings_at_setup
+            assert msg in caplog.messages
+
+        assert ctx.total_tests_scheduled == expected_tests
+        assert ctx.total_devices_selected_for_testing == expected_devices
+
+    async def test_run_invalid_filters(self) -> None:
+        """Test AntaRunner.run() with invalid filters."""
+        inventory = AntaInventory()
+        catalog = AntaCatalog()
+        runner = AntaRunner()
+
+        with pytest.raises(ValidationError, match="1 validation error for AntaRunFilters"):
+            await runner.run(inventory, catalog, filters=AntaRunFilters(devices="invalid"), dry_run=True)  # type: ignore[arg-type]
+
+    async def test_run_provided_manager(self) -> None:
+        """Test AntaRunner.run() with a provided ResultManager instance."""
+        inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta")
+        catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml")
+        manager = ResultManager()
+        runner = AntaRunner()
+
+        ctx = await runner.run(inventory, catalog, manager, dry_run=True)
+        assert isinstance(ctx.manager, ResultManager)
+        assert ctx.manager is manager
+        assert len(manager) == 27
+
+    async def test_run_provided_manager_not_empty(self, caplog: pytest.LogCaptureFixture) -> None:
+        """Test AntaRunner.run() with a provided non-empty ResultManager instance."""
+        caplog.set_level(logging.WARNING)
+
+        inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta")
+        catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml")
+        manager = ResultManager()
+        test = AntaTestResult(name="DC1-LEAF1A", test="VerifyNTP", categories=["system"], description="NTP Test")
+        runner = AntaRunner()
+        manager.add(test)
+
+        ctx = await runner.run(inventory, catalog, manager, dry_run=True)
+        assert isinstance(ctx.manager, ResultManager)
+        assert ctx.manager is manager
+        assert len(manager) == 28
+        assert len(manager.device_stats) == ctx.total_devices_selected_for_testing + 1
+
+        warning_msg = (
+            "Appending new results to the provided ResultManager which already holds 1 results. Statistics in this run context are for the current execution only."
+        )
+        assert warning_msg in ctx.warnings_at_setup
+        assert warning_msg in caplog.messages
+
+    async def test_run_empty_catalog(self, caplog: pytest.LogCaptureFixture) -> None:
+        """Test AntaRunner.run() with an empty AntaCatalog."""
+        caplog.set_level(logging.WARNING)
+
+        inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta")
+        catalog = AntaCatalog()
+        runner = AntaRunner()
+
+        ctx = await runner.run(inventory, catalog)
+
+        warning_msg = "The list of tests is empty. Exiting ..."
+        assert warning_msg in ctx.warnings_at_setup
+        assert warning_msg in caplog.messages
+
+    async def test_run_empty_inventory(self, caplog: pytest.LogCaptureFixture) -> None:
+        """Test AntaRunner.run() with an empty AntaInventory."""
+        caplog.set_level(logging.WARNING)
+
+        inventory = AntaInventory()
+        catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml")
+        runner = AntaRunner()
+
+        ctx = await runner.run(inventory, catalog)
+
+        warning_msg = "The initial inventory is empty. Exiting ..."
+        assert warning_msg in ctx.warnings_at_setup
+        assert warning_msg in caplog.messages
+
+    @pytest.mark.parametrize("inventory", [{"reachable": False}], indirect=True)
+    async def test_run_no_reachable_devices(self, caplog: pytest.LogCaptureFixture, inventory: AntaInventory) -> None:
+        """Test AntaRunner.run() with an empty AntaInventory."""
+        caplog.set_level(logging.WARNING)
+
+        catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml")
+        runner = AntaRunner()
+
+        ctx = await runner.run(inventory, catalog)
+        assert ctx.total_devices_unreachable == ctx.total_devices_in_inventory
+        assert "device-0" in ctx.devices_unreachable_at_setup
+
+        warning_msg = "No reachable devices found for testing after connectivity checks. Exiting ..."
+        assert warning_msg in ctx.warnings_at_setup
+        assert warning_msg in caplog.messages
+
+    async def test_run_invalid_anta_test(self, caplog: pytest.LogCaptureFixture) -> None:
+        """Test AntaRunner.run() with a provided non-empty ResultManager instance."""
+        caplog.set_level(logging.CRITICAL)
+
+        class InvalidTest(AntaTest):
+            """ANTA test that raises an exception when test is called."""
+
+            categories: ClassVar[list[str]] = []
+            commands: ClassVar[list[AntaCommand | AntaTemplate]] = []
+
+            def test(self) -> None:  # type: ignore[override]
+                """Test function."""
+                msg = "Test not implemented"
+                raise NotImplementedError(msg)
+
+        inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta")
+        test_definition = AntaTestDefinition(test=InvalidTest, inputs=None)
+        catalog = AntaCatalog(tests=[test_definition])
+        runner = AntaRunner()
+
+        ctx = await runner.run(inventory, catalog, dry_run=True)
+        assert len(ctx.manager) == 0
+
+        error_msg = (
+            f"There is an error when creating test {__name__}.InvalidTest.\n"
+            "If this is not a custom test implementation: "
+            "Please reach out to the maintainer team or open an issue on Github: https://github.com/aristanetworks/anta.\n"
+            "NotImplementedError: Test not implemented"
+        )
+        assert error_msg in caplog.messages
+
+    async def test_log_run_information_default(self, caplog: pytest.LogCaptureFixture) -> None:
+        """Test AntaRunner._log_run_information with default settings."""
+        caplog.set_level(logging.INFO)
+
+        inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta")
+        catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml")
+        runner = AntaRunner()
+        await runner.run(inventory, catalog, dry_run=True)
+
+        expected_output = [
+            "ANTA NRFU Dry Run Information",
+            "Devices:",
+            "  Total in initial inventory: 3",
+            "  Selected for testing: 3",
+            "Total number of selected tests: 27",
+        ]
+        for line in expected_output:
+            assert line in caplog.text
+
+    async def test_log_run_information_concurrency_limit(self, caplog: pytest.LogCaptureFixture) -> None:
+        """Test AntaRunner._log_run_information with higher tests count than concurrency limit."""
+        caplog.set_level(logging.WARNING)
+
+        inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta")
+        catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml")
+        runner_settings = AntaRunnerSettings(max_concurrency=20)
+        runner = AntaRunner(settings=runner_settings)
+
+        ctx = await runner.run(inventory, catalog, dry_run=True)
+
+        warning_msg = "Tests count (27) exceeds concurrent limit (20). Tests will be throttled. Please consult the ANTA FAQ."
+        assert warning_msg in ctx.warnings_at_setup
+        assert warning_msg in caplog.messages
+
+    @pytest.mark.skipif(os.name != "posix", reason="Very unlikely to happen on non-POSIX systems due to sys.maxsize")
+    async def test_log_run_information_file_descriptor_limit(self, caplog: pytest.LogCaptureFixture) -> None:
+        """Test AntaRunner._log_run_information with higher connections count than file descriptor limit."""
+        caplog.set_level(logging.WARNING)
+
+        inventory = AntaInventory.parse(filename=DATA_DIR / "test_inventory_with_tags.yml", username="anta", password="anta")
+        catalog = AntaCatalog.parse(filename=DATA_DIR / "test_catalog_with_tags.yml")
+        runner_settings = AntaRunnerSettings(nofile=128)
+        runner = AntaRunner(settings=runner_settings)
+
+        ctx = await runner.run(inventory, catalog, dry_run=True)
+
+        warning_msg = "Potential connections (300) exceeds file descriptor limit (128). Connection errors may occur. Please consult the ANTA FAQ."
+        assert warning_msg in ctx.warnings_at_setup
+        assert warning_msg in caplog.messages
+
+    @pytest.mark.parametrize(("inventory"), [{"count": 3}], indirect=True)
+    @respx.mock
+    async def test_run(self, inventory: AntaInventory) -> None:
+        """Test AntaRunner.run()."""
+        # Mock the eAPI requests
+        respx.post(path="/command-api", headers={"Content-Type": "application/json-rpc"}, json__params__cmds__0__cmd="show ip route vrf default").respond(
+            json={"result": [{"vrfs": {"default": {"routes": {}}}}]}
+        )
+        tests = [AntaTestDefinition(test=VerifyRoutingTableEntry, inputs={"routes": [f"10.1.0.{i}"], "collect": "all"}) for i in range(5)]
+        catalog = AntaCatalog(tests=tests)
+        runner = AntaRunner()
+
+        ctx = await runner.run(inventory, catalog)
+
+        assert ctx.total_devices_selected_for_testing == 3
+        assert ctx.total_tests_scheduled == 15
+        assert len(ctx.warnings_at_setup) == 0
+        assert len(ctx.manager) == 15
+        for result in ctx.manager.results:
+            assert result.result == "failure"
+
+
+# pylint: disable=too-few-public-methods
+class TestAntaRunContext:
+    """Test AntaRunContext class."""
+
+    def test_init(self) -> None:
+        """Test initialization."""
+        inventory = AntaInventory()
+        catalog = AntaCatalog()
+        manager = ResultManager()
+        filters = AntaRunFilters()
+
+        ctx = AntaRunContext(inventory, catalog, manager, filters)
+
+        # Test initialized attributes
+        assert ctx.inventory is inventory
+        assert ctx.catalog is catalog
+        assert ctx.manager is manager
+        assert ctx.filters is filters
+        assert not ctx.dry_run
+
+        assert isinstance(ctx.selected_inventory, AntaInventory)
+        assert len(ctx.selected_inventory) == 0
+        assert isinstance(ctx.selected_tests, defaultdict)
+        assert len(ctx.selected_tests) == 0
+        assert isinstance(ctx.devices_filtered_at_setup, list)
+        assert len(ctx.devices_filtered_at_setup) == 0
+        assert isinstance(ctx.devices_unreachable_at_setup, list)
+        assert len(ctx.devices_unreachable_at_setup) == 0
+        assert isinstance(ctx.warnings_at_setup, list)
+        assert len(ctx.warnings_at_setup) == 0
+        assert ctx.start_time is None
+        assert ctx.end_time is None
+
+        # Test properties
+        assert ctx.total_devices_in_inventory == 0
+        assert ctx.total_devices_filtered_by_tags == 0
+        assert ctx.total_devices_unreachable == 0
+        assert ctx.total_devices_selected_for_testing == 0
+        assert ctx.total_tests_scheduled == 0
+        assert ctx.duration is None
diff --git a/tests/units/test_device.py b/tests/units/test_device.py
index 0ecba5558..ee3e53875 100644
--- a/tests/units/test_device.py
+++ b/tests/units/test_device.py
@@ -592,6 +592,10 @@ def test_cache_statistics(self, device: AntaDevice, expected: dict[str, Any] | N
         """
         assert device.cache_statistics == expected
 
+    def test_max_connections(self, device: AntaDevice) -> None:
+        """Test max_connections property."""
+        assert device.max_connections is None
+
 
 class TestAsyncEOSDevice:
     """Test for anta.device.AsyncEOSDevice."""
@@ -625,6 +629,16 @@ def test__eq(self, device1: dict[str, Any], device2: dict[str, Any], expected: b
         else:
             assert dev1 != dev2
 
+    def test_max_connections(self, async_device: AsyncEOSDevice) -> None:
+        """Test max_connections property."""
+        # HTTPX uses a max_connections of 100 by default
+        assert async_device.max_connections == 100
+
+    def test_max_connections_none(self, async_device: AsyncEOSDevice) -> None:
+        """Test max_connections property when not available in the session object."""
+        with patch.object(async_device, "_session", None):
+            assert async_device.max_connections is None
+
     @pytest.mark.parametrize(
         ("async_device", "patch_kwargs", "expected"),
         REFRESH_PARAMS,
diff --git a/tests/units/test_runner.py b/tests/units/test_runner.py
index ca7948639..dc18d23d1 100644
--- a/tests/units/test_runner.py
+++ b/tests/units/test_runner.py
@@ -7,18 +7,19 @@
 
 import logging
 import os
-import sys
 from pathlib import Path
+from typing import TYPE_CHECKING
 from unittest.mock import patch
 
 import pytest
 
 from anta.catalog import AntaCatalog
-from anta.inventory import AntaInventory
-from anta.result_manager import ResultManager
-from anta.runner import main, prepare_tests
+from anta.runner import prepare_tests
 
-from .test_models import FakeTest, FakeTestWithMissingTest
+from .test_models import FakeTest
+
+if TYPE_CHECKING:
+    from anta.inventory import AntaInventory
 
 if os.name == "posix":
     # The function is not defined on non-POSIX system
@@ -30,47 +31,8 @@
 FAKE_CATALOG: AntaCatalog = AntaCatalog.from_list([(FakeTest, None)])
 
 
-async def test_empty_tests(caplog: pytest.LogCaptureFixture, inventory: AntaInventory) -> None:
-    """Test that when the list of tests is empty, a log is raised."""
-    caplog.set_level(logging.INFO)
-    manager = ResultManager()
-    await main(manager, inventory, AntaCatalog())
-
-    assert len(caplog.record_tuples) == 1
-    assert "The list of tests is empty, exiting" in caplog.records[0].message
-
-
-async def test_empty_inventory(caplog: pytest.LogCaptureFixture) -> None:
-    """Test that when the Inventory is empty, a log is raised."""
-    caplog.set_level(logging.INFO)
-    manager = ResultManager()
-    await main(manager, AntaInventory(), FAKE_CATALOG)
-    assert len(caplog.record_tuples) == 3
-    assert "The inventory is empty, exiting" in caplog.records[1].message
-
-
-@pytest.mark.parametrize(
-    ("inventory", "tags", "devices"),
-    [
-        pytest.param({"count": 1, "reachable": False}, None, None, id="not-reachable"),
-        pytest.param({"filename": "test_inventory_with_tags.yml", "reachable": False}, {"leaf"}, None, id="not-reachable-with-tag"),
-        pytest.param({"count": 1, "reachable": True}, {"invalid-tag"}, None, id="reachable-with-invalid-tag"),
-        pytest.param({"filename": "test_inventory_with_tags.yml", "reachable": True}, None, {"invalid-device"}, id="reachable-with-invalid-device"),
-        pytest.param({"filename": "test_inventory_with_tags.yml", "reachable": False}, None, {"leaf1"}, id="not-reachable-with-device"),
-        pytest.param({"filename": "test_inventory_with_tags.yml", "reachable": False}, {"leaf"}, {"leaf1"}, id="not-reachable-with-device-and-tag"),
-        pytest.param({"filename": "test_inventory_with_tags.yml", "reachable": False}, {"invalid"}, {"invalid-device"}, id="reachable-with-invalid-tag-and-device"),
-    ],
-    indirect=["inventory"],
-)
-async def test_no_selected_device(caplog: pytest.LogCaptureFixture, inventory: AntaInventory, tags: set[str], devices: set[str]) -> None:
-    """Test that when the list of established devices is empty a log is raised."""
-    caplog.set_level(logging.WARNING)
-    manager = ResultManager()
-    await main(manager, inventory, FAKE_CATALOG, tags=tags, devices=devices)
-    msg = f"No reachable device {f'matching the tags {tags} ' if tags else ''}was found.{f' Selected devices: {devices} ' if devices is not None else ''}"
-    assert msg in caplog.messages
-
-
+# TODO: Remove this in ANTA v2.0.0
+@pytest.mark.filterwarnings("ignore::DeprecationWarning")
 @pytest.mark.skipif(os.name != "posix", reason="Cannot run this test on Windows")
 def test_adjust_rlimit_nofile_valid_env(caplog: pytest.LogCaptureFixture) -> None:
     """Test adjust_rlimit_nofile with valid environment variables."""
@@ -104,6 +66,8 @@ def side_effect_setrlimit(resource_id: int, limits: tuple[int, int]) -> None:
         setrlimit_mock.assert_called_once_with(resource.RLIMIT_NOFILE, (20480, 1048576))
 
 
+# TODO: Remove this in ANTA v2.0.0
+@pytest.mark.filterwarnings("ignore::DeprecationWarning")
 @pytest.mark.skipif(os.name != "posix", reason="Cannot run this test on Windows")
 def test_adjust_rlimit_nofile_invalid_env(caplog: pytest.LogCaptureFixture) -> None:
     """Test adjust_rlimit_nofile with invalid environment variables."""
@@ -138,6 +102,7 @@ def side_effect_setrlimit(resource_id: int, limits: tuple[int, int]) -> None:
         setrlimit_mock.assert_called_once_with(resource.RLIMIT_NOFILE, (16384, 1048576))
 
 
+# TODO: Remove this in ANTA v2.0.0
 @pytest.mark.skipif(os.name != "posix", reason="Cannot run this test on Windows")
 def test_adjust_rlimit_nofile_value_error(caplog: pytest.LogCaptureFixture) -> None:
     """Test adjust_rlimit_nofile with invalid environment variables."""
@@ -171,31 +136,8 @@ def side_effect_setrlimit(_resource_id: int, _limits: tuple[int, int]) -> None:
         setrlimit_mock.assert_called_once_with(resource.RLIMIT_NOFILE, (666, 1048576))
 
 
-@pytest.mark.skipif(os.name == "posix", reason="Run this test on Windows only")
-async def test_check_runner_log_for_windows(caplog: pytest.LogCaptureFixture, inventory: AntaInventory) -> None:
-    """Test log output for Windows host regarding rlimit."""
-    caplog.set_level(logging.INFO)
-    manager = ResultManager()
-    # Using dry-run to shorten the test
-    await main(manager, inventory, FAKE_CATALOG, dry_run=True)
-    assert "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors." in caplog.records[-3].message
-
-
-# We could instead merge multiple coverage report together but that requires more work than just this.
-@pytest.mark.skipif(os.name != "posix", reason="Fake non-posix for coverage")
-async def test_check_runner_log_for_windows_fake(caplog: pytest.LogCaptureFixture, inventory: AntaInventory) -> None:
-    """Test log output for Windows host regarding rlimit."""
-    with patch("os.name", new="win32"):
-        del sys.modules["anta.runner"]
-        from anta.runner import main  # pylint: disable=W0621
-
-        caplog.set_level(logging.INFO)
-        manager = ResultManager()
-        # Using dry-run to shorten the test
-        await main(manager, inventory, FAKE_CATALOG, dry_run=True)
-        assert "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors." in caplog.records[-3].message
-
-
+# TODO: Remove this in ANTA v2.0.0
+@pytest.mark.filterwarnings("ignore::DeprecationWarning")
 @pytest.mark.parametrize(
     ("inventory", "tags", "tests", "devices_count", "tests_count"),
     [
@@ -223,29 +165,3 @@ async def test_prepare_tests(
     assert selected_tests is not None
     assert len(selected_tests) == devices_count
     assert sum(len(tests) for tests in selected_tests.values()) == tests_count
-
-
-async def test_dry_run(caplog: pytest.LogCaptureFixture, inventory: AntaInventory) -> None:
-    """Test that when dry_run is True, no tests are run."""
-    caplog.set_level(logging.INFO)
-    manager = ResultManager()
-    await main(manager, inventory, FAKE_CATALOG, dry_run=True)
-    assert "Dry-run mode, exiting before running the tests." in caplog.records[-1].message
-
-
-async def test_cannot_create_test(caplog: pytest.LogCaptureFixture, inventory: AntaInventory) -> None:
-    """Test that when an Exception is raised during test instantiation, it is caught and a log is raised."""
-    caplog.set_level(logging.CRITICAL)
-    manager = ResultManager()
-    catalog = AntaCatalog.from_list([(FakeTestWithMissingTest, None)])  # type: ignore[type-abstract]
-    await main(manager, inventory, catalog)
-    msg = (
-        "There is an error when creating test tests.units.test_models.FakeTestWithMissingTest.\nIf this is not a custom test implementation: "
-        "Please reach out to the maintainer team or open an issue on Github: https://github.com/aristanetworks/anta.\nTypeError: "
-    )
-    msg += (
-        "Can't instantiate abstract class FakeTestWithMissingTest without an implementation for abstract method 'test'"
-        if sys.version_info >= (3, 12)
-        else "Can't instantiate abstract class FakeTestWithMissingTest with abstract method test"
-    )
-    assert msg in caplog.messages
diff --git a/tests/units/test_settings.py b/tests/units/test_settings.py
new file mode 100644
index 000000000..43d3ddca1
--- /dev/null
+++ b/tests/units/test_settings.py
@@ -0,0 +1,126 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Unit tests for the anta.settings module."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from unittest.mock import patch
+
+import pytest
+from pydantic import ValidationError
+
+from anta.settings import DEFAULT_MAX_CONCURRENCY, DEFAULT_NOFILE, AntaRunnerSettings
+
+if os.name == "posix":
+    # The function is not defined on non-POSIX system
+    import resource
+
+
+class TestAntaRunnerSettings:
+    """Tests for the AntaRunnerSettings class."""
+
+    def test_defaults(self, setenvvar: pytest.MonkeyPatch) -> None:
+        """Test defaults for ANTA runner settings."""
+        settings = AntaRunnerSettings()
+        assert settings.nofile == DEFAULT_NOFILE
+        assert settings.max_concurrency == DEFAULT_MAX_CONCURRENCY
+
+    def test_env_var(self, setenvvar: pytest.MonkeyPatch) -> None:
+        """Test setting different ANTA runner settings."""
+        setenvvar.setenv("ANTA_NOFILE", "20480")
+        settings = AntaRunnerSettings()
+        assert settings.nofile == 20480
+        assert settings.max_concurrency == DEFAULT_MAX_CONCURRENCY
+
+    def test_validation(self, setenvvar: pytest.MonkeyPatch) -> None:
+        """Test validation of ANTA runner settings."""
+        setenvvar.setenv("ANTA_NOFILE", "-1")
+        with pytest.raises(ValidationError):
+            AntaRunnerSettings()
+
+        setenvvar.setenv("ANTA_MAX_CONCURRENCY", "0")
+        with pytest.raises(ValidationError):
+            AntaRunnerSettings()
+
+    @pytest.mark.skipif(os.name == "posix", reason="Run this test on Windows only")
+    def test_file_descriptor_limit_windows(self, caplog: pytest.LogCaptureFixture) -> None:
+        """Test file_descriptor_limit on Windows."""
+        caplog.set_level(logging.INFO)
+        settings = AntaRunnerSettings()
+        assert settings.file_descriptor_limit == sys.maxsize
+        assert "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors." in caplog.text
+
+    @pytest.mark.skipif(os.name != "posix", reason="Fake non-posix for coverage")
+    async def test_file_descriptor_limit_fake_windows(self, caplog: pytest.LogCaptureFixture) -> None:
+        """Test file_descriptor_limit on fake Windows."""
+        caplog.set_level(logging.INFO)
+        with patch("os.name", new="win32"):
+            settings = AntaRunnerSettings()
+            assert settings.file_descriptor_limit == sys.maxsize
+            assert "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors." in caplog.records[0].message
+
+    @pytest.mark.skipif(os.name != "posix", reason="Cannot run this test on Windows")
+    def test_file_descriptor_limit_posix(self, caplog: pytest.LogCaptureFixture) -> None:
+        """Test file_descriptor_limit on POSIX systems."""
+        with (
+            caplog.at_level(logging.DEBUG),
+            patch.dict("os.environ", {"ANTA_NOFILE": "20480"}),
+            patch("resource.getrlimit") as getrlimit_mock,
+            patch("resource.setrlimit") as setrlimit_mock,
+        ):
+            # Simulate the default system limits
+            system_limits = (8192, 1048576)
+
+            # Setup getrlimit mock return value
+            getrlimit_mock.return_value = system_limits
+
+            # Simulate setrlimit behavior
+            def side_effect_setrlimit(_resource_id: int, limits: tuple[int, int]) -> None:
+                getrlimit_mock.return_value = (limits[0], limits[1])
+
+            setrlimit_mock.side_effect = side_effect_setrlimit
+
+            settings = AntaRunnerSettings()
+
+            # Assert the limits were updated as expected
+            assert settings.file_descriptor_limit == 20480
+            assert "Initial file descriptor limits for the current ANTA process: Soft Limit: 8192 | Hard Limit: 1048576" in caplog.text
+            assert "Setting file descriptor soft limit to 20480" in caplog.text
+
+            setrlimit_mock.assert_called_once_with(resource.RLIMIT_NOFILE, (20480, 1048576))  # pylint: disable=possibly-used-before-assignment
+
+    @pytest.mark.skipif(os.name != "posix", reason="Cannot run this test on Windows")
+    def test_file_descriptor_limit_value_error(self, caplog: pytest.LogCaptureFixture) -> None:
+        """Test file_descriptor_limit with invalid environment variables."""
+        with (
+            caplog.at_level(logging.DEBUG),
+            patch.dict("os.environ", {"ANTA_NOFILE": "666"}),
+            patch("resource.getrlimit") as getrlimit_mock,
+            patch("resource.setrlimit") as setrlimit_mock,
+        ):
+            # Simulate the default system limits
+            system_limits = (32768, 131072)
+
+            # Setup getrlimit mock return value
+            getrlimit_mock.return_value = system_limits
+
+            # Simulate setrlimit behavior raising ValueError
+            def side_effect_setrlimit(_resource_id: int, _limits: tuple[int, int]) -> None:
+                msg = "not allowed to raise maximum limit"
+                raise ValueError(msg)
+
+            setrlimit_mock.side_effect = side_effect_setrlimit
+
+            settings = AntaRunnerSettings()
+
+            # Assert the limits were *NOT* updated as expected
+            assert settings.file_descriptor_limit == 32768
+            assert "Initial file descriptor limits for the current ANTA process: Soft Limit: 32768 | Hard Limit: 131072" in caplog.text
+            assert caplog.records[-1].levelname == "WARNING"
+            assert "Failed to set file descriptor soft limit for the current ANTA process" in caplog.records[-1].getMessage()
+
+            setrlimit_mock.assert_called_once_with(resource.RLIMIT_NOFILE, (666, 131072))


Reviews:
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-08-01T08:32:17Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-08-01T08:32:35Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-04T23:11:22Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-05T00:17:48Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-05T08:53:26Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-05T08:54:04Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-14T12:41:39Z
user: login: gmuloc
body: Lots on comment on the guide but a very big **Thank You** for this amazing guide :) 

🎉 🎈 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-11-14T14:23:34Z
user: login: mtache
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T10:36:10Z
user: login: mtache
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T10:49:26Z
user: login: mtache
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T10:50:52Z
user: login: mtache
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T11:14:05Z
user: login: mtache
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T11:14:33Z
user: login: mtache
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T11:15:11Z
user: login: mtache
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T11:29:00Z
user: login: mtache
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T15:06:59Z
user: login: mtache
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T15:10:12Z
user: login: mtache
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T15:10:44Z
user: login: mtache
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T15:13:23Z
user: login: mtache
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T15:15:24Z
user: login: mtache
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T15:15:30Z
user: login: mtache
body: Default value code could be simplified/cleaned.
Good idea to have this `anta.settings` module, we should use it for `ANTA_DEBUG`, in another PR ;)
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T15:23:44Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-21T15:27:30Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-22T02:58:57Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-22T03:03:06Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-22T03:23:31Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-22T03:39:58Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-22T03:46:17Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-22T03:47:34Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-22T03:49:28Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-22T03:50:38Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-22T03:59:15Z
user: login: mtache
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-01-29T14:32:44Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-07T19:57:25Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-07T19:58:03Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-07T19:58:11Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-07T19:59:19Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-07T19:59:56Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-07T20:00:06Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-07T20:13:44Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-10T16:05:01Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-10T16:05:34Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-10T16:22:00Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-10T16:22:09Z
user: login: gmuloc
body: only the guide
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-10T17:01:23Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:53:51Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:53:58Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:54:03Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:54:05Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:54:21Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:54:26Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:54:31Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:54:37Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:54:43Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:54:48Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:54:53Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:54:58Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:55:02Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:55:10Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:55:12Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T12:55:52Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T13:40:58Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T14:53:47Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T15:57:05Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T15:57:12Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:21:08Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:46:34Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:49:52Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:50:58Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:53:45Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:54:47Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:54:53Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:55:13Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:55:21Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:56:15Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:56:21Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:56:38Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:58:00Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:58:43Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:58:52Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:58:59Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T21:59:39Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T22:00:01Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T22:00:14Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T22:00:22Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T22:00:47Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T09:26:04Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T09:34:44Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T14:48:09Z
user: login: gmuloc
body: some comments .. not done!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T14:04:38Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T14:37:07Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T14:40:21Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T14:40:38Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T14:49:28Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T14:49:40Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T14:49:47Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T14:49:53Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T14:55:00Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T09:06:38Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T12:57:21Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:58:40Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:58:49Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:59:03Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:59:17Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:59:32Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T15:59:57Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T16:00:05Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T16:00:20Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T16:00:28Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T13:05:02Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T13:48:19Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T14:00:20Z
user: login: gmuloc
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T14:00:26Z
user: login: carl-baillargeon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T14:02:09Z
user: login: gmuloc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T14:30:41Z

Review comments:
diff_hunk: @@ -75,6 +76,57 @@ def log_cache_statistics(devices: list[AntaDevice]) -> None:
             logger.info("Caching is not enabled on %s", device.name)
 
 
+async def run_tests(tests_generator: AsyncGenerator[Coroutine[Any, Any, TestResult], None], limit: int) -> AsyncGenerator[TestResult, None]:
+    """Run tests with a concurrency limit.
+
+    This function takes an asynchronous generator of test coroutines and runs them
+    with a limit on the number of concurrent tests. It yields test results as each
+    test completes.
+
+    Args:
+    ----
path: anta/runner.py
user: login: gmuloc
body: ```suggestion
    Parameters
    -----------
```
created_at: 2024-08-01T08:32:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -157,23 +209,24 @@ def prepare_tests(
     return device_to_tests
 
 
-def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]]) -> list[Coroutine[Any, Any, TestResult]]:
-    """Get the coroutines for the ANTA run.
+async def generate_tests(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]]) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+    """Generate the coroutines for the ANTA run.
+
+    It creates an async generator of coroutines which are created by the `test` method of the AntaTest instances. Each coroutine is a test to run.
 
     Args:
     ----
path: anta/runner.py
user: login: gmuloc
body: ```suggestion
    Parameters
    -----------
```
created_at: 2024-08-01T08:32:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,6 +104,61 @@ def log_cache_statistics(devices: list[AntaDevice]) -> None:
             logger.info("Caching is not enabled on %s", device.name)
 
 
+async def run(tests_generator: AsyncGenerator[Coroutine[Any, Any, TestResult], None], limit: int) -> AsyncGenerator[TestResult, None]:
+    """Run tests with a concurrency limit.
+
+    This function takes an asynchronous generator of test coroutines and runs them
+    with a limit on the number of concurrent tests. It yields test results as each
+    test completes.
+
+    Inspired by: https://death.andgravity.com/limit-concurrency
+
+    Parameters
+    ----------
+    tests_generator
+        An asynchronous generator that yields test coroutines.
+    limit
+        The maximum number of concurrent tests to run.
+
+    Yields
+    ------
+        The result of each completed test.
+    """
+    # NOTE: The `aiter` built-in function is not available in Python 3.9
path: anta/runner.py
user: login: gmuloc
body: so this will fail in 3.9?
created_at: 2024-11-04T23:11:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,6 +104,61 @@ def log_cache_statistics(devices: list[AntaDevice]) -> None:
             logger.info("Caching is not enabled on %s", device.name)
 
 
+async def run(tests_generator: AsyncGenerator[Coroutine[Any, Any, TestResult], None], limit: int) -> AsyncGenerator[TestResult, None]:
+    """Run tests with a concurrency limit.
+
+    This function takes an asynchronous generator of test coroutines and runs them
+    with a limit on the number of concurrent tests. It yields test results as each
+    test completes.
+
+    Inspired by: https://death.andgravity.com/limit-concurrency
+
+    Parameters
+    ----------
+    tests_generator
+        An asynchronous generator that yields test coroutines.
+    limit
+        The maximum number of concurrent tests to run.
+
+    Yields
+    ------
+        The result of each completed test.
+    """
+    # NOTE: The `aiter` built-in function is not available in Python 3.9
path: anta/runner.py
user: login: carl-baillargeon
body: `aiter()` and `anext()` were released in 3.10: https://docs.python.org/3/library/functions.html#aiter so we need to use the dunders instead (since we support 3.9 RIP)
created_at: 2024-11-05T00:17:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,6 +104,61 @@ def log_cache_statistics(devices: list[AntaDevice]) -> None:
             logger.info("Caching is not enabled on %s", device.name)
 
 
+async def run(tests_generator: AsyncGenerator[Coroutine[Any, Any, TestResult], None], limit: int) -> AsyncGenerator[TestResult, None]:
+    """Run tests with a concurrency limit.
+
+    This function takes an asynchronous generator of test coroutines and runs them
+    with a limit on the number of concurrent tests. It yields test results as each
+    test completes.
+
+    Inspired by: https://death.andgravity.com/limit-concurrency
+
+    Parameters
+    ----------
+    tests_generator
+        An asynchronous generator that yields test coroutines.
+    limit
+        The maximum number of concurrent tests to run.
+
+    Yields
+    ------
+        The result of each completed test.
+    """
+    # NOTE: The `aiter` built-in function is not available in Python 3.9
path: anta/runner.py
user: login: gmuloc
body: not RIP yet: https://devguide.python.org/versions/
created_at: 2024-11-05T08:53:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,6 +104,61 @@ def log_cache_statistics(devices: list[AntaDevice]) -> None:
             logger.info("Caching is not enabled on %s", device.name)
 
 
+async def run(tests_generator: AsyncGenerator[Coroutine[Any, Any, TestResult], None], limit: int) -> AsyncGenerator[TestResult, None]:
+    """Run tests with a concurrency limit.
+
+    This function takes an asynchronous generator of test coroutines and runs them
+    with a limit on the number of concurrent tests. It yields test results as each
+    test completes.
+
+    Inspired by: https://death.andgravity.com/limit-concurrency
+
+    Parameters
+    ----------
+    tests_generator
+        An asynchronous generator that yields test coroutines.
+    limit
+        The maximum number of concurrent tests to run.
+
+    Yields
+    ------
+        The result of each completed test.
+    """
+    # NOTE: The `aiter` built-in function is not available in Python 3.9
path: anta/runner.py
user: login: gmuloc
body: ok so this can be like this until we drop 3.9 understood thanks!
created_at: 2024-11-05T08:54:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,6 +34,78 @@
 # https://github.com/pyca/cryptography/issues/7236#issuecomment-1131908472
 CLIENT_KEYS = asyncssh.public_key.load_default_keypairs()
 
+ANTA_DEFAULT_TIMEOUT = 30.0
+
+
+def get_httpx_limits() -> Limits:
+    """Adjust the underlying HTTPX client resource limits.
+
+    The limits are set using the following environment variables:
+        - ANTA_MAX_CONNECTIONS: Maximum number of allowable connections.
+        - ANTA_MAX_KEEPALIVE_CONNECTIONS: Number of allowable keep-alive connections.
+        - ANTA_KEEPALIVE_EXPIRY: Time limit on idle keep-alive connections in seconds.
path: anta/device.py
user: login: gmuloc
body: Link to httpx doc
created_at: 2024-11-14T12:41:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,14 +33,94 @@
 logger = logging.getLogger(__name__)
 
 DEFAULT_NOFILE = 16384
+"""Default number of open file descriptors for the ANTA process."""
+DEFAULT_MAX_CONCURRENCY = 10000
+"""Default maximum number of tests to run concurrently."""
path: anta/runner.py
user: login: gmuloc
body: We may need to move all these to constants for simpler way to know where stuff is
created_at: 2024-11-14T12:55:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,14 +33,94 @@
 logger = logging.getLogger(__name__)
 
 DEFAULT_NOFILE = 16384
+"""Default number of open file descriptors for the ANTA process."""
+DEFAULT_MAX_CONCURRENCY = 10000
+"""Default maximum number of tests to run concurrently."""
+
+
+def log_run_information(
+    device_count: tuple[int, int],
+    test_count: int,
+    max_concurrency: int,
+    max_connections: int | None,
+    file_descriptor_limit: int,
+) -> None:
+    """Log ANTA run information and potential resource limit warnings.
+
+    Parameters
+    ----------
+    device_count : tuple[int, int]
+        Total number of devices in inventory and number of established devices.
+    test_count : int
+        Total number of tests to run.
+    max_concurrency : int
+        Maximum number of concurrent tests.
+    max_connections : int | None
+        Maximum connections per device. None means unlimited.
+    file_descriptor_limit : int
+        System file descriptor limit.
+    """
+    # TODO: 34 is a magic numbers from RichHandler formatting catering for date, level and path
+    width = min(int(console.width) - 34, len("Maximum number of open file descriptors for the current ANTA process: 0000000000\n"))
+
+    devices_total, devices_established = device_count
+
+    run_info = (
+        f"{' ANTA NRFU Run Information ':-^{width}}\n"
+        f"Devices: {devices_total} total, {devices_established} established\n"
+        f"Tests: {test_count} total\n"
+        f"Limits:\n"
+        f"  Max concurrent tests: {max_concurrency}\n"
+        f"  Max connections per device: {'Unlimited' if max_connections is None else max_connections}\n"
+        f"  Max file descriptors: {file_descriptor_limit}\n"
+        f"{'':-^{width}}"
+    )
+
+    logger.info(run_info)
+
+    # Log warnings for potential resource limits
+    if test_count > max_concurrency:
+        logger.warning("Tests count (%s) exceeds concurrent limit (%s). Tests will be throttled. See Scaling ANTA documentation.", test_count, max_concurrency)
+
+    if max_connections is None:
+        logger.warning(
+            "Running with unlimited HTTP connections. Connection errors may occur due to file descriptor limit (%s). See Scaling ANTA documentation.",
+            file_descriptor_limit,
+        )
+    elif devices_established * max_connections > file_descriptor_limit:
+        logger.warning(
+            "Potential connections (%s) exceeds file descriptor limit (%s). Connection errors may occur. See Scaling ANTA documentation.",
+            devices_established * max_connections,
+            file_descriptor_limit,
+        )
+
+
+def get_max_concurrency() -> int:
+    """Adjust the maximum number of tests (coroutines) to run concurrently.
+
+    The limit is set to the value of the ANTA_MAX_CONCURRENCY environment variable.
+
+    If the `ANTA_MAX_CONCURRENCY` environment variable is not set or is invalid, `DEFAULT_MAX_CONCURRENCY` is used (10000).
+
+    Returns
+    -------
+    int
+        The maximum number of tests to run concurrently.
+    """
+    try:
+        max_concurrency = int(os.environ.get("ANTA_MAX_CONCURRENCY", DEFAULT_MAX_CONCURRENCY))
+    except ValueError as exception:
+        logger.warning("The ANTA_MAX_CONCURRENCY environment variable value is invalid: %s\nDefault to %s.", exc_to_str(exception), DEFAULT_MAX_CONCURRENCY)
+        return DEFAULT_MAX_CONCURRENCY
+    return max_concurrency
 
 
 def adjust_rlimit_nofile() -> tuple[int, int]:
     """Adjust the maximum number of open file descriptors for the ANTA process.
 
     The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable.
 
-    If the `ANTA_NOFILE` environment variable is not set or is invalid, `DEFAULT_NOFILE` is used.
+    If the `ANTA_NOFILE` environment variable is not set or is invalid, `DEFAULT_NOFILE` is used (16384).
path: anta/runner.py
user: login: gmuloc
body: proabbly best to not put the value here - we then forget to update it :p

created_at: 2024-11-14T12:59:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,6 +161,61 @@ def log_cache_statistics(devices: list[AntaDevice]) -> None:
             logger.info("Caching is not enabled on %s", device.name)
 
 
+async def run(tests_generator: AsyncGenerator[Coroutine[Any, Any, TestResult], None], limit: int) -> AsyncGenerator[TestResult, None]:
+    """Run tests with a concurrency limit.
+
+    This function takes an asynchronous generator of test coroutines and runs them
+    with a limit on the number of concurrent tests. It yields test results as each
+    test completes.
+
+    Inspired by: https://death.andgravity.com/limit-concurrency
+
+    Parameters
+    ----------
+    tests_generator
+        An asynchronous generator that yields test coroutines.
+    limit
+        The maximum number of concurrent tests to run.
+
+    Yields
+    ------
+        The result of each completed test.
+    """
+    # NOTE: The `aiter` built-in function is not available in Python 3.9
+    aws = tests_generator.__aiter__()  # pylint: disable=unnecessary-dunder-call
path: anta/runner.py
user: login: gmuloc
body: what does aws stands for here? :)
created_at: 2024-11-14T12:59:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: do we need this? this is redundant with 
![Screenshot 2024-11-14 at 14 48 12](https://github.com/user-attachments/assets/411c4033-8635-4f48-95bd-1e89b3684aa0)

created_at: 2024-11-14T13:15:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -179,6 +179,7 @@ nav:
     - Debug commands: cli/debug.md
     - Tag Management: cli/tag-management.md
   - Advanced Usages:
+    - Scaling ANTA: advanced_usages/scaling.md
path: mkdocs.yml
user: login: gmuloc
body: not first - should be last or let's use alphabetical order
created_at: 2024-11-14T13:49:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
```
created_at: 2024-11-14T13:49:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
It covers two main approaches:
```
created_at: 2024-11-14T13:50:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: Link is broken 
```suggestion
2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#multi-process-scaling))
```
created_at: 2024-11-14T13:51:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
!!! tip
    For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
```

maybe it is tips

created_at: 2024-11-14T13:52:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: Remove this and use instead the anta_title ninja trick above
```suggestion
```

this allows you to use H1 titles for Introductiob / Single Process ...
created_at: 2024-11-14T13:53:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
---
anta_title: 🚀 Scaling ANTA: A Comprehensive Guide
---
<!--
```
created_at: 2024-11-14T13:54:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: once using `anta_title` you can remove one layer of header
```suggestion
# 📖 Introduction
```
created_at: 2024-11-14T13:55:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
ANTA uses Python `asyncio` library for concurrent execution:
```
created_at: 2024-11-14T13:55:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: emoji for concurrently is a bit surprising :) 
created_at: 2024-11-14T13:55:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
!!! important "Key Concept"
    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
```
created_at: 2024-11-14T13:56:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
This section described the environment variables to control resource usage provided by ANTA.
```
created_at: 2024-11-14T13:57:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: Use titles instead of numbered list
created_at: 2024-11-14T13:58:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: text before example?
created_at: 2024-11-14T13:58:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
    ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
```
created_at: 2024-11-14T14:00:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: Same I would first described ANTA behabior and then states that this variable can be used
created_at: 2024-11-14T14:00:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
```

Also add a note that it does not work on Windows system today.
Maybve we can show the warning
created_at: 2024-11-14T14:01:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: only for UNIX systems add a note that this is system dependent
created_at: 2024-11-14T14:02:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: specify for `AsyncEapiDevice`
created_at: 2024-11-14T14:03:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: make this a box

```
!!! tip "Best Practices"
   ..
```
created_at: 2024-11-14T14:04:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: Same would put the text first with ANTA behavior and then explains how it can be finetuned and link to HTTPX
created_at: 2024-11-14T14:05:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: hopw do I determine what is best?runtime?
created_at: 2024-11-14T14:06:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
+
+2. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=5
+    ```
+
+    These values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+3. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_TIMEOUT=3600.0
+    ```
+
+    This is **very** important for large fabrics. Increase the global timeout to prevent test failures due to timeouts. You shouldn't need to adjust the other timeout values unless you have specific requirements.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: Hwo does this manifest?
I feel very important should be all bold and potentially this should be number 1 in the list :)
created_at: 2024-11-14T14:07:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
+
+2. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=5
+    ```
+
+    These values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+3. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_TIMEOUT=3600.0
+    ```
+
+    This is **very** important for large fabrics. Increase the global timeout to prevent test failures due to timeouts. You shouldn't need to adjust the other timeout values unless you have specific requirements.
+
+## 🔀 Multi-Process Scaling
+
+When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section will provide a few examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section will provide a few examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
```
```suggestion
When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section provides a couple of examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
```
created_at: 2024-11-14T14:07:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
+
+2. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=5
+    ```
+
+    These values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+3. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_TIMEOUT=3600.0
+    ```
+
+    This is **very** important for large fabrics. Increase the global timeout to prevent test failures due to timeouts. You shouldn't need to adjust the other timeout values unless you have specific requirements.
+
+## 🔀 Multi-Process Scaling
+
+When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section will provide a few examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
+
+By leveraging `GNU Parallel` and proper test organization, we can significantly reduce execution time and improve overall performance while maintaining accurate test results.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
By leveraging `GNU Parallel` and proper test organization, execution time can be significantly reduced, improving overall performance while maintaining accurate test results.
```
created_at: 2024-11-14T14:08:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
+
+2. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=5
+    ```
+
+    These values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+3. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_TIMEOUT=3600.0
+    ```
+
+    This is **very** important for large fabrics. Increase the global timeout to prevent test failures due to timeouts. You shouldn't need to adjust the other timeout values unless you have specific requirements.
+
+## 🔀 Multi-Process Scaling
+
+When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section will provide a few examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
+
+By leveraging `GNU Parallel` and proper test organization, we can significantly reduce execution time and improve overall performance while maintaining accurate test results.
+
+### Prerequisites
+
+Before implementing multi-process scaling:
+
+- 🏗️ Understand your fabric topology
+- 🎯 Identify natural test boundaries (e.g., devices, PODs, roles, etc.)
+- 📋 Plan your test catalog(s) accordingly
+- 💻 Understand basic Linux command-line operations
+
+### Installation Requirements
+
+!!! info "System Requirements"
+    The following procedures and examples were tested on **Ubuntu 22.04**, using `GNU Parallel` version `20241022` and `yq` version `v4.44.3`. Please refer the tool's documentation for specific installation instructions.
+
+#### Installing Required Tools
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: title is not needed as there is no other sub title (or maybe should be called `Ubuntu`
created_at: 2024-11-14T14:10:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
+
+2. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=5
+    ```
+
+    These values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+3. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_TIMEOUT=3600.0
+    ```
+
+    This is **very** important for large fabrics. Increase the global timeout to prevent test failures due to timeouts. You shouldn't need to adjust the other timeout values unless you have specific requirements.
+
+## 🔀 Multi-Process Scaling
+
+When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section will provide a few examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
+
+By leveraging `GNU Parallel` and proper test organization, we can significantly reduce execution time and improve overall performance while maintaining accurate test results.
+
+### Prerequisites
+
+Before implementing multi-process scaling:
+
+- 🏗️ Understand your fabric topology
+- 🎯 Identify natural test boundaries (e.g., devices, PODs, roles, etc.)
+- 📋 Plan your test catalog(s) accordingly
+- 💻 Understand basic Linux command-line operations
+
+### Installation Requirements
+
+!!! info "System Requirements"
+    The following procedures and examples were tested on **Ubuntu 22.04**, using `GNU Parallel` version `20241022` and `yq` version `v4.44.3`. Please refer the tool's documentation for specific installation instructions.
+
+#### Installing Required Tools
+
+1. Install `GNU Parallel` using the following commands:
+
+    ```bash
+    apt-get install parallel
+    ```
+
+2. Install `yq` using the following commands:
+
+    ```bash
+    wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
+        chmod +x /usr/bin/yq
+    ```
+
+3. Confirm the installation by running:
+
+    ```bash
+    parallel --version
+    yq --version
+    ```
+
+    You should see the version information for both tools if the installation was successful:
+
+    ```bash
+    (.dev) ~ parallel --version
+    GNU parallel 20241022
+    Copyright (C) 2007-2024 Ole Tange, http://ole.tange.dk and Free Software
+    Foundation, Inc.
+    License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
+    This is free software: you are free to change and redistribute it.
+    GNU parallel comes with no warranty.
+
+    Web site: https://www.gnu.org/software/parallel
+
+    When using programs that use GNU Parallel to process data for publication
+    please cite as described in 'parallel --citation'.
+
+    (.dev) ~ yq --version
+    yq (https://github.com/mikefarah/yq/) version v4.44.3
+    ```
+
+### Scaling Strategies
+
+#### Device-Based Parallelization
+
+In this approach, we run ANTA instances in parallel, with each instance handling a specific device. This strategy is effective when:
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
In this approach, instances of ANTA targeting a single device each are run in parallel. This strategy is effective when:
```
created_at: 2024-11-14T14:12:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
+
+2. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=5
+    ```
+
+    These values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+3. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_TIMEOUT=3600.0
+    ```
+
+    This is **very** important for large fabrics. Increase the global timeout to prevent test failures due to timeouts. You shouldn't need to adjust the other timeout values unless you have specific requirements.
+
+## 🔀 Multi-Process Scaling
+
+When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section will provide a few examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
+
+By leveraging `GNU Parallel` and proper test organization, we can significantly reduce execution time and improve overall performance while maintaining accurate test results.
+
+### Prerequisites
+
+Before implementing multi-process scaling:
+
+- 🏗️ Understand your fabric topology
+- 🎯 Identify natural test boundaries (e.g., devices, PODs, roles, etc.)
+- 📋 Plan your test catalog(s) accordingly
+- 💻 Understand basic Linux command-line operations
+
+### Installation Requirements
+
+!!! info "System Requirements"
+    The following procedures and examples were tested on **Ubuntu 22.04**, using `GNU Parallel` version `20241022` and `yq` version `v4.44.3`. Please refer the tool's documentation for specific installation instructions.
+
+#### Installing Required Tools
+
+1. Install `GNU Parallel` using the following commands:
+
+    ```bash
+    apt-get install parallel
+    ```
+
+2. Install `yq` using the following commands:
+
+    ```bash
+    wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
+        chmod +x /usr/bin/yq
+    ```
+
+3. Confirm the installation by running:
+
+    ```bash
+    parallel --version
+    yq --version
+    ```
+
+    You should see the version information for both tools if the installation was successful:
+
+    ```bash
+    (.dev) ~ parallel --version
+    GNU parallel 20241022
+    Copyright (C) 2007-2024 Ole Tange, http://ole.tange.dk and Free Software
+    Foundation, Inc.
+    License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
+    This is free software: you are free to change and redistribute it.
+    GNU parallel comes with no warranty.
+
+    Web site: https://www.gnu.org/software/parallel
+
+    When using programs that use GNU Parallel to process data for publication
+    please cite as described in 'parallel --citation'.
+
+    (.dev) ~ yq --version
+    yq (https://github.com/mikefarah/yq/) version v4.44.3
+    ```
+
+### Scaling Strategies
+
+#### Device-Based Parallelization
+
+In this approach, we run ANTA instances in parallel, with each instance handling a specific device. This strategy is effective when:
+
+- Each device has unique test requirements, i.e., one catalog per device
+- You want to maintain separate results per device
+- You need fine-grained control over test execution
+
+Example inventory structure:
+
+```yaml
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+  # ... more devices
+```
+
+Example test catalog structure (one catalog per device):
+
+```yaml
+# DC1-SPINE2.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet1
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet1'
+
+# DC1-SPINE1.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet2
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet2'
+```
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: Maybe lets use tabs or columns ?
````suggestion
```yaml
# DC1-SPINE2.yml
anta.tests.connectivity:
- VerifyLLDPNeighbors:
    neighbors:
    - neighbor_device: DC1-LEAF1A
      neighbor_port: Ethernet1
      port: Ethernet1
    result_overwrite:
      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet1'
```

```yaml
# DC1-SPINE1.yml
anta.tests.connectivity:
- VerifyLLDPNeighbors:
    neighbors:
    - neighbor_device: DC1-LEAF1A
      neighbor_port: Ethernet2
      port: Ethernet1
    result_overwrite:
      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet2'
```
````
created_at: 2024-11-14T14:13:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
+
+2. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=5
+    ```
+
+    These values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+3. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_TIMEOUT=3600.0
+    ```
+
+    This is **very** important for large fabrics. Increase the global timeout to prevent test failures due to timeouts. You shouldn't need to adjust the other timeout values unless you have specific requirements.
+
+## 🔀 Multi-Process Scaling
+
+When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section will provide a few examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
+
+By leveraging `GNU Parallel` and proper test organization, we can significantly reduce execution time and improve overall performance while maintaining accurate test results.
+
+### Prerequisites
+
+Before implementing multi-process scaling:
+
+- 🏗️ Understand your fabric topology
+- 🎯 Identify natural test boundaries (e.g., devices, PODs, roles, etc.)
+- 📋 Plan your test catalog(s) accordingly
+- 💻 Understand basic Linux command-line operations
+
+### Installation Requirements
+
+!!! info "System Requirements"
+    The following procedures and examples were tested on **Ubuntu 22.04**, using `GNU Parallel` version `20241022` and `yq` version `v4.44.3`. Please refer the tool's documentation for specific installation instructions.
+
+#### Installing Required Tools
+
+1. Install `GNU Parallel` using the following commands:
+
+    ```bash
+    apt-get install parallel
+    ```
+
+2. Install `yq` using the following commands:
+
+    ```bash
+    wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
+        chmod +x /usr/bin/yq
+    ```
+
+3. Confirm the installation by running:
+
+    ```bash
+    parallel --version
+    yq --version
+    ```
+
+    You should see the version information for both tools if the installation was successful:
+
+    ```bash
+    (.dev) ~ parallel --version
+    GNU parallel 20241022
+    Copyright (C) 2007-2024 Ole Tange, http://ole.tange.dk and Free Software
+    Foundation, Inc.
+    License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
+    This is free software: you are free to change and redistribute it.
+    GNU parallel comes with no warranty.
+
+    Web site: https://www.gnu.org/software/parallel
+
+    When using programs that use GNU Parallel to process data for publication
+    please cite as described in 'parallel --citation'.
+
+    (.dev) ~ yq --version
+    yq (https://github.com/mikefarah/yq/) version v4.44.3
+    ```
+
+### Scaling Strategies
+
+#### Device-Based Parallelization
+
+In this approach, we run ANTA instances in parallel, with each instance handling a specific device. This strategy is effective when:
+
+- Each device has unique test requirements, i.e., one catalog per device
+- You want to maintain separate results per device
+- You need fine-grained control over test execution
+
+Example inventory structure:
+
+```yaml
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+  # ... more devices
+```
+
+Example test catalog structure (one catalog per device):
+
+```yaml
+# DC1-SPINE2.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet1
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet1'
+
+# DC1-SPINE1.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet2
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet2'
+```
+
+#### Tag-Based Parallelization
+
+This approach runs parallel instances based on device tags, useful when:
+
+- Devices in the same role share similar test requirements
+- You want to test entire PODs or device groups simultaneously
+- You need to organize tests by network function or location
+
+Example inventory structure:
+
+```yaml
+---
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.111
+    name: DC1-LEAF1A
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.112
+    name: DC1-LEAF1B
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.201
+    name: DC2-SPINE1
+    tags: ["SPINE", "DC2"]
+
+  - host: 172.20.20.202
+    name: DC2-SPINE2
+    tags: ["SPINE", "DC2"]
+  # ... more devices
+```
+
+Example test catalog structure (one catalog for all devices):
+
+```yaml
+anta.tests.vxlan:
+  - VerifyVxlan1Interface:
+      filters:
+        tags: ["BL", "LEAF"]
+  - VerifyVxlanConfigSanity:
+      filters:
+        tags: ["BL", "LEAF"]
+
+anta.tests.routing:
+  generic:
+    - VerifyRoutingProtocolModel:
+        model: multi-agent
+        filters:
+          tags: ["SPINE"]
+```
+
+#### Optimizing Catalog Distribution
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: Should probably come first in the section?
created_at: 2024-11-14T14:14:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
+
+2. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=5
+    ```
+
+    These values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+3. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_TIMEOUT=3600.0
+    ```
+
+    This is **very** important for large fabrics. Increase the global timeout to prevent test failures due to timeouts. You shouldn't need to adjust the other timeout values unless you have specific requirements.
+
+## 🔀 Multi-Process Scaling
+
+When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section will provide a few examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
+
+By leveraging `GNU Parallel` and proper test organization, we can significantly reduce execution time and improve overall performance while maintaining accurate test results.
+
+### Prerequisites
+
+Before implementing multi-process scaling:
+
+- 🏗️ Understand your fabric topology
+- 🎯 Identify natural test boundaries (e.g., devices, PODs, roles, etc.)
+- 📋 Plan your test catalog(s) accordingly
+- 💻 Understand basic Linux command-line operations
+
+### Installation Requirements
+
+!!! info "System Requirements"
+    The following procedures and examples were tested on **Ubuntu 22.04**, using `GNU Parallel` version `20241022` and `yq` version `v4.44.3`. Please refer the tool's documentation for specific installation instructions.
+
+#### Installing Required Tools
+
+1. Install `GNU Parallel` using the following commands:
+
+    ```bash
+    apt-get install parallel
+    ```
+
+2. Install `yq` using the following commands:
+
+    ```bash
+    wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
+        chmod +x /usr/bin/yq
+    ```
+
+3. Confirm the installation by running:
+
+    ```bash
+    parallel --version
+    yq --version
+    ```
+
+    You should see the version information for both tools if the installation was successful:
+
+    ```bash
+    (.dev) ~ parallel --version
+    GNU parallel 20241022
+    Copyright (C) 2007-2024 Ole Tange, http://ole.tange.dk and Free Software
+    Foundation, Inc.
+    License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
+    This is free software: you are free to change and redistribute it.
+    GNU parallel comes with no warranty.
+
+    Web site: https://www.gnu.org/software/parallel
+
+    When using programs that use GNU Parallel to process data for publication
+    please cite as described in 'parallel --citation'.
+
+    (.dev) ~ yq --version
+    yq (https://github.com/mikefarah/yq/) version v4.44.3
+    ```
+
+### Scaling Strategies
+
+#### Device-Based Parallelization
+
+In this approach, we run ANTA instances in parallel, with each instance handling a specific device. This strategy is effective when:
+
+- Each device has unique test requirements, i.e., one catalog per device
+- You want to maintain separate results per device
+- You need fine-grained control over test execution
+
+Example inventory structure:
+
+```yaml
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+  # ... more devices
+```
+
+Example test catalog structure (one catalog per device):
+
+```yaml
+# DC1-SPINE2.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet1
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet1'
+
+# DC1-SPINE1.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet2
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet2'
+```
+
+#### Tag-Based Parallelization
+
+This approach runs parallel instances based on device tags, useful when:
+
+- Devices in the same role share similar test requirements
+- You want to test entire PODs or device groups simultaneously
+- You need to organize tests by network function or location
+
+Example inventory structure:
+
+```yaml
+---
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.111
+    name: DC1-LEAF1A
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.112
+    name: DC1-LEAF1B
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.201
+    name: DC2-SPINE1
+    tags: ["SPINE", "DC2"]
+
+  - host: 172.20.20.202
+    name: DC2-SPINE2
+    tags: ["SPINE", "DC2"]
+  # ... more devices
+```
+
+Example test catalog structure (one catalog for all devices):
+
+```yaml
+anta.tests.vxlan:
+  - VerifyVxlan1Interface:
+      filters:
+        tags: ["BL", "LEAF"]
+  - VerifyVxlanConfigSanity:
+      filters:
+        tags: ["BL", "LEAF"]
+
+anta.tests.routing:
+  generic:
+    - VerifyRoutingProtocolModel:
+        model: multi-agent
+        filters:
+          tags: ["SPINE"]
+```
+
+#### Optimizing Catalog Distribution
+
+For large fabrics, consider:
+
+1. Creating separate catalogs per device type
+2. Organizing catalogs by POD or datacenter
+3. Using tag-based filtering within catalogs
+
+Benefits:
+
+- Reduced memory usage per ANTA instance
+- Faster catalog processing
+- More efficient test execution
+
+### Implementation
+
+#### Device-Based Implementation
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: I would move Implementation under each stratgey in a subtitle for easier navigation
created_at: 2024-11-14T14:15:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
+
+2. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=5
+    ```
+
+    These values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+3. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_TIMEOUT=3600.0
+    ```
+
+    This is **very** important for large fabrics. Increase the global timeout to prevent test failures due to timeouts. You shouldn't need to adjust the other timeout values unless you have specific requirements.
+
+## 🔀 Multi-Process Scaling
+
+When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section will provide a few examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
+
+By leveraging `GNU Parallel` and proper test organization, we can significantly reduce execution time and improve overall performance while maintaining accurate test results.
+
+### Prerequisites
+
+Before implementing multi-process scaling:
+
+- 🏗️ Understand your fabric topology
+- 🎯 Identify natural test boundaries (e.g., devices, PODs, roles, etc.)
+- 📋 Plan your test catalog(s) accordingly
+- 💻 Understand basic Linux command-line operations
+
+### Installation Requirements
+
+!!! info "System Requirements"
+    The following procedures and examples were tested on **Ubuntu 22.04**, using `GNU Parallel` version `20241022` and `yq` version `v4.44.3`. Please refer the tool's documentation for specific installation instructions.
+
+#### Installing Required Tools
+
+1. Install `GNU Parallel` using the following commands:
+
+    ```bash
+    apt-get install parallel
+    ```
+
+2. Install `yq` using the following commands:
+
+    ```bash
+    wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
+        chmod +x /usr/bin/yq
+    ```
+
+3. Confirm the installation by running:
+
+    ```bash
+    parallel --version
+    yq --version
+    ```
+
+    You should see the version information for both tools if the installation was successful:
+
+    ```bash
+    (.dev) ~ parallel --version
+    GNU parallel 20241022
+    Copyright (C) 2007-2024 Ole Tange, http://ole.tange.dk and Free Software
+    Foundation, Inc.
+    License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
+    This is free software: you are free to change and redistribute it.
+    GNU parallel comes with no warranty.
+
+    Web site: https://www.gnu.org/software/parallel
+
+    When using programs that use GNU Parallel to process data for publication
+    please cite as described in 'parallel --citation'.
+
+    (.dev) ~ yq --version
+    yq (https://github.com/mikefarah/yq/) version v4.44.3
+    ```
+
+### Scaling Strategies
+
+#### Device-Based Parallelization
+
+In this approach, we run ANTA instances in parallel, with each instance handling a specific device. This strategy is effective when:
+
+- Each device has unique test requirements, i.e., one catalog per device
+- You want to maintain separate results per device
+- You need fine-grained control over test execution
+
+Example inventory structure:
+
+```yaml
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+  # ... more devices
+```
+
+Example test catalog structure (one catalog per device):
+
+```yaml
+# DC1-SPINE2.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet1
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet1'
+
+# DC1-SPINE1.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet2
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet2'
+```
+
+#### Tag-Based Parallelization
+
+This approach runs parallel instances based on device tags, useful when:
+
+- Devices in the same role share similar test requirements
+- You want to test entire PODs or device groups simultaneously
+- You need to organize tests by network function or location
+
+Example inventory structure:
+
+```yaml
+---
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.111
+    name: DC1-LEAF1A
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.112
+    name: DC1-LEAF1B
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.201
+    name: DC2-SPINE1
+    tags: ["SPINE", "DC2"]
+
+  - host: 172.20.20.202
+    name: DC2-SPINE2
+    tags: ["SPINE", "DC2"]
+  # ... more devices
+```
+
+Example test catalog structure (one catalog for all devices):
+
+```yaml
+anta.tests.vxlan:
+  - VerifyVxlan1Interface:
+      filters:
+        tags: ["BL", "LEAF"]
+  - VerifyVxlanConfigSanity:
+      filters:
+        tags: ["BL", "LEAF"]
+
+anta.tests.routing:
+  generic:
+    - VerifyRoutingProtocolModel:
+        model: multi-agent
+        filters:
+          tags: ["SPINE"]
+```
+
+#### Optimizing Catalog Distribution
+
+For large fabrics, consider:
+
+1. Creating separate catalogs per device type
+2. Organizing catalogs by POD or datacenter
+3. Using tag-based filtering within catalogs
+
+Benefits:
+
+- Reduced memory usage per ANTA instance
+- Faster catalog processing
+- More efficient test execution
+
+### Implementation
+
+#### Device-Based Implementation
+
+1. Create the device-specific test runner BASH script (`nrfu.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    set -euo pipefail
+
+    exec 2>&1
+
+    usage() {
+        echo "Usage: $0 <node-name> [<nrfu-reports-path>]"
+        exit 1
+    }
+
+    ANTA_INVENTORY="anta_inventory.yml"
+    TEST_CATALOGS_PATH="intended/test_catalogs"
+    NODE_NAME=${1:-}
+    NRFU_REPORT_PATH=${2:-nrfu_reports}
+
+
+    if [[ -z "$NODE_NAME" ]]; then
+        usage
+    fi
+
+    anta nrfu \
+        -d "$NODE_NAME" \
+        -i "$ANTA_INVENTORY" \
+        -c "$TEST_CATALOGS_PATH/$NODE_NAME-catalog.yml" \
+        json -o "$NRFU_REPORT_PATH/$NODE_NAME.json"
+    ```
+
+    Change `ANTA_INVENTORY` and `TEST_CATALOGS_PATH` to match your environment.
+
+2. Create the parallel execution script (`run.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    mkdir -p nrfu_reports
+
+    NODE_NAMES=$(yq '.anta_inventory.hosts[].name' < "anta_inventory.yml" | tr -d '"')
+
+    parallel --tag './nrfu.sh {} nrfu_reports' ::: $NODE_NAMES || NRFU_STATUS=$?
+
+    exit $NRFU_STATUS
+    ```
+
+    Also change `anta_inventory.yml` to match your inventory file specified in `ANTA_INVENTORY` of `nrfu.sh`.
+
+3. Make both scripts executable:
+
+    ```bash
+    chmod +x nrfu.sh run.sh
+    ```
+
+4. Run the script:
+
+    ```bash
+    ./run.sh
+    ```
+
+5. The script will execute ANTA tests for each device in parallel, generating a JSON report for each device in the `nrfu_reports` directory.
+
+#### Tag-Based Implementation
+
+1. Create the tag-specific test runner (`nrfu-tag.sh`):
+
+    ```bash
+    #!/bin/bash
+    set -euo pipefail
+
+    exec 2>&1
+
+    usage() {
+        echo "Usage: $0 <tag-name> [<nrfu-reports-path>]"
+        exit 1
+    }
+
+    ANTA_INVENTORY="anta_inventory.yml"
+    ANTA_CATALOG="anta_catalog.yml"
+    TAG_NAME=${1:-}
+    NRFU_REPORT_PATH=${2:-nrfu_reports}
+
+    if [[ -z "$TAG_NAME" ]]; then
+        usage
+    fi
+
+    anta nrfu \
+        --tags "$TAG_NAME" \
+        -i "$ANTA_INVENTORY" \
+        -c "$ANTA_CATALOG" \
+        json -o "$NRFU_REPORT_PATH/${TAG_NAME}.json"
+    ```
+
+    Same as before, adjust `ANTA_INVENTORY`, `ANTA_CATALOG` to match your environment.
+
+2. Create the parallel execution script (`run-tag.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    mkdir -p nrfu_reports
+
+    TAGS=$(yq -r '.. | select(.filters?) | .filters.tags[]' < "anta_catalog.yml" | sort -u)
+
+    parallel --tag './nrfu_tag.sh {} nrfu_reports' ::: $TAGS || NRFU_STATUS=$?
+
+    exit $NRFU_STATUS
+    ```
+
+    Change `anta_catalog.yml` to match your catalog file specified in `ANTA_CATALOG` of `nrfu-tag.sh`.
+
+3. Make both scripts executable:
+
+    ```bash
+    chmod +x nrfu-tag.sh run-tag.sh
+    ```
+
+4. Run the script:
+
+    ```bash
+    ./run-tag.sh
+    ```
+
+5. The script will execute ANTA tests for each tag in parallel, generating a JSON report for each tag in the `nrfu_reports` directory.
+
+### Performance Considerations
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: same this secrtion shoul probably come before Scaling strategies
created_at: 2024-11-14T14:16:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
+
+2. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=5
+    ```
+
+    These values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+3. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_TIMEOUT=3600.0
+    ```
+
+    This is **very** important for large fabrics. Increase the global timeout to prevent test failures due to timeouts. You shouldn't need to adjust the other timeout values unless you have specific requirements.
+
+## 🔀 Multi-Process Scaling
+
+When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section will provide a few examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
+
+By leveraging `GNU Parallel` and proper test organization, we can significantly reduce execution time and improve overall performance while maintaining accurate test results.
+
+### Prerequisites
+
+Before implementing multi-process scaling:
+
+- 🏗️ Understand your fabric topology
+- 🎯 Identify natural test boundaries (e.g., devices, PODs, roles, etc.)
+- 📋 Plan your test catalog(s) accordingly
+- 💻 Understand basic Linux command-line operations
+
+### Installation Requirements
+
+!!! info "System Requirements"
+    The following procedures and examples were tested on **Ubuntu 22.04**, using `GNU Parallel` version `20241022` and `yq` version `v4.44.3`. Please refer the tool's documentation for specific installation instructions.
+
+#### Installing Required Tools
+
+1. Install `GNU Parallel` using the following commands:
+
+    ```bash
+    apt-get install parallel
+    ```
+
+2. Install `yq` using the following commands:
+
+    ```bash
+    wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
+        chmod +x /usr/bin/yq
+    ```
+
+3. Confirm the installation by running:
+
+    ```bash
+    parallel --version
+    yq --version
+    ```
+
+    You should see the version information for both tools if the installation was successful:
+
+    ```bash
+    (.dev) ~ parallel --version
+    GNU parallel 20241022
+    Copyright (C) 2007-2024 Ole Tange, http://ole.tange.dk and Free Software
+    Foundation, Inc.
+    License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
+    This is free software: you are free to change and redistribute it.
+    GNU parallel comes with no warranty.
+
+    Web site: https://www.gnu.org/software/parallel
+
+    When using programs that use GNU Parallel to process data for publication
+    please cite as described in 'parallel --citation'.
+
+    (.dev) ~ yq --version
+    yq (https://github.com/mikefarah/yq/) version v4.44.3
+    ```
+
+### Scaling Strategies
+
+#### Device-Based Parallelization
+
+In this approach, we run ANTA instances in parallel, with each instance handling a specific device. This strategy is effective when:
+
+- Each device has unique test requirements, i.e., one catalog per device
+- You want to maintain separate results per device
+- You need fine-grained control over test execution
+
+Example inventory structure:
+
+```yaml
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+  # ... more devices
+```
+
+Example test catalog structure (one catalog per device):
+
+```yaml
+# DC1-SPINE2.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet1
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet1'
+
+# DC1-SPINE1.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet2
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet2'
+```
+
+#### Tag-Based Parallelization
+
+This approach runs parallel instances based on device tags, useful when:
+
+- Devices in the same role share similar test requirements
+- You want to test entire PODs or device groups simultaneously
+- You need to organize tests by network function or location
+
+Example inventory structure:
+
+```yaml
+---
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.111
+    name: DC1-LEAF1A
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.112
+    name: DC1-LEAF1B
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.201
+    name: DC2-SPINE1
+    tags: ["SPINE", "DC2"]
+
+  - host: 172.20.20.202
+    name: DC2-SPINE2
+    tags: ["SPINE", "DC2"]
+  # ... more devices
+```
+
+Example test catalog structure (one catalog for all devices):
+
+```yaml
+anta.tests.vxlan:
+  - VerifyVxlan1Interface:
+      filters:
+        tags: ["BL", "LEAF"]
+  - VerifyVxlanConfigSanity:
+      filters:
+        tags: ["BL", "LEAF"]
+
+anta.tests.routing:
+  generic:
+    - VerifyRoutingProtocolModel:
+        model: multi-agent
+        filters:
+          tags: ["SPINE"]
+```
+
+#### Optimizing Catalog Distribution
+
+For large fabrics, consider:
+
+1. Creating separate catalogs per device type
+2. Organizing catalogs by POD or datacenter
+3. Using tag-based filtering within catalogs
+
+Benefits:
+
+- Reduced memory usage per ANTA instance
+- Faster catalog processing
+- More efficient test execution
+
+### Implementation
+
+#### Device-Based Implementation
+
+1. Create the device-specific test runner BASH script (`nrfu.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    set -euo pipefail
+
+    exec 2>&1
+
+    usage() {
+        echo "Usage: $0 <node-name> [<nrfu-reports-path>]"
+        exit 1
+    }
+
+    ANTA_INVENTORY="anta_inventory.yml"
+    TEST_CATALOGS_PATH="intended/test_catalogs"
+    NODE_NAME=${1:-}
+    NRFU_REPORT_PATH=${2:-nrfu_reports}
+
+
+    if [[ -z "$NODE_NAME" ]]; then
+        usage
+    fi
+
+    anta nrfu \
+        -d "$NODE_NAME" \
+        -i "$ANTA_INVENTORY" \
+        -c "$TEST_CATALOGS_PATH/$NODE_NAME-catalog.yml" \
+        json -o "$NRFU_REPORT_PATH/$NODE_NAME.json"
+    ```
+
+    Change `ANTA_INVENTORY` and `TEST_CATALOGS_PATH` to match your environment.
+
+2. Create the parallel execution script (`run.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    mkdir -p nrfu_reports
+
+    NODE_NAMES=$(yq '.anta_inventory.hosts[].name' < "anta_inventory.yml" | tr -d '"')
+
+    parallel --tag './nrfu.sh {} nrfu_reports' ::: $NODE_NAMES || NRFU_STATUS=$?
+
+    exit $NRFU_STATUS
+    ```
+
+    Also change `anta_inventory.yml` to match your inventory file specified in `ANTA_INVENTORY` of `nrfu.sh`.
+
+3. Make both scripts executable:
+
+    ```bash
+    chmod +x nrfu.sh run.sh
+    ```
+
+4. Run the script:
+
+    ```bash
+    ./run.sh
+    ```
+
+5. The script will execute ANTA tests for each device in parallel, generating a JSON report for each device in the `nrfu_reports` directory.
+
+#### Tag-Based Implementation
+
+1. Create the tag-specific test runner (`nrfu-tag.sh`):
+
+    ```bash
+    #!/bin/bash
+    set -euo pipefail
+
+    exec 2>&1
+
+    usage() {
+        echo "Usage: $0 <tag-name> [<nrfu-reports-path>]"
+        exit 1
+    }
+
+    ANTA_INVENTORY="anta_inventory.yml"
+    ANTA_CATALOG="anta_catalog.yml"
+    TAG_NAME=${1:-}
+    NRFU_REPORT_PATH=${2:-nrfu_reports}
+
+    if [[ -z "$TAG_NAME" ]]; then
+        usage
+    fi
+
+    anta nrfu \
+        --tags "$TAG_NAME" \
+        -i "$ANTA_INVENTORY" \
+        -c "$ANTA_CATALOG" \
+        json -o "$NRFU_REPORT_PATH/${TAG_NAME}.json"
+    ```
+
+    Same as before, adjust `ANTA_INVENTORY`, `ANTA_CATALOG` to match your environment.
+
+2. Create the parallel execution script (`run-tag.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    mkdir -p nrfu_reports
+
+    TAGS=$(yq -r '.. | select(.filters?) | .filters.tags[]' < "anta_catalog.yml" | sort -u)
+
+    parallel --tag './nrfu_tag.sh {} nrfu_reports' ::: $TAGS || NRFU_STATUS=$?
+
+    exit $NRFU_STATUS
+    ```
+
+    Change `anta_catalog.yml` to match your catalog file specified in `ANTA_CATALOG` of `nrfu-tag.sh`.
+
+3. Make both scripts executable:
+
+    ```bash
+    chmod +x nrfu-tag.sh run-tag.sh
+    ```
+
+4. Run the script:
+
+    ```bash
+    ./run-tag.sh
+    ```
+
+5. The script will execute ANTA tests for each tag in parallel, generating a JSON report for each tag in the `nrfu_reports` directory.
+
+### Performance Considerations
+
+#### Parallel Job Control
+
+The `-j` option in `GNU Parallel` controls the number of concurrent jobs. Consider these factors when setting the job limit:
+
+1. **Available CPU cores**:
+
+     - Recommend: jobs = number of cores - 1
+     - Example: For 8 cores, use `-j7`
+
+2. **Memory requirements**:
+
+     - Each ANTA instance requires memory
+     - Monitor system resources during execution using `htop` or similar tools
+
+#### JSON vs YAML
+
+When working with large catalogs and inventories, using JSON format instead of YAML can provide better performance as ANTA loads JSON files more efficiently. If you choose to use JSON:
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: I think thats fair?
```suggestion
When working with large catalogs and inventories, using JSON format instead of YAML can provide better performance as Python loads JSON files more efficiently. If you choose to use JSON:
```
created_at: 2024-11-14T14:16:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
+
+2. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=5
+    ```
+
+    These values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+3. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_TIMEOUT=3600.0
+    ```
+
+    This is **very** important for large fabrics. Increase the global timeout to prevent test failures due to timeouts. You shouldn't need to adjust the other timeout values unless you have specific requirements.
+
+## 🔀 Multi-Process Scaling
+
+When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section will provide a few examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
+
+By leveraging `GNU Parallel` and proper test organization, we can significantly reduce execution time and improve overall performance while maintaining accurate test results.
+
+### Prerequisites
+
+Before implementing multi-process scaling:
+
+- 🏗️ Understand your fabric topology
+- 🎯 Identify natural test boundaries (e.g., devices, PODs, roles, etc.)
+- 📋 Plan your test catalog(s) accordingly
+- 💻 Understand basic Linux command-line operations
+
+### Installation Requirements
+
+!!! info "System Requirements"
+    The following procedures and examples were tested on **Ubuntu 22.04**, using `GNU Parallel` version `20241022` and `yq` version `v4.44.3`. Please refer the tool's documentation for specific installation instructions.
+
+#### Installing Required Tools
+
+1. Install `GNU Parallel` using the following commands:
+
+    ```bash
+    apt-get install parallel
+    ```
+
+2. Install `yq` using the following commands:
+
+    ```bash
+    wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
+        chmod +x /usr/bin/yq
+    ```
+
+3. Confirm the installation by running:
+
+    ```bash
+    parallel --version
+    yq --version
+    ```
+
+    You should see the version information for both tools if the installation was successful:
+
+    ```bash
+    (.dev) ~ parallel --version
+    GNU parallel 20241022
+    Copyright (C) 2007-2024 Ole Tange, http://ole.tange.dk and Free Software
+    Foundation, Inc.
+    License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
+    This is free software: you are free to change and redistribute it.
+    GNU parallel comes with no warranty.
+
+    Web site: https://www.gnu.org/software/parallel
+
+    When using programs that use GNU Parallel to process data for publication
+    please cite as described in 'parallel --citation'.
+
+    (.dev) ~ yq --version
+    yq (https://github.com/mikefarah/yq/) version v4.44.3
+    ```
+
+### Scaling Strategies
+
+#### Device-Based Parallelization
+
+In this approach, we run ANTA instances in parallel, with each instance handling a specific device. This strategy is effective when:
+
+- Each device has unique test requirements, i.e., one catalog per device
+- You want to maintain separate results per device
+- You need fine-grained control over test execution
+
+Example inventory structure:
+
+```yaml
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+  # ... more devices
+```
+
+Example test catalog structure (one catalog per device):
+
+```yaml
+# DC1-SPINE2.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet1
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet1'
+
+# DC1-SPINE1.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet2
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet2'
+```
+
+#### Tag-Based Parallelization
+
+This approach runs parallel instances based on device tags, useful when:
+
+- Devices in the same role share similar test requirements
+- You want to test entire PODs or device groups simultaneously
+- You need to organize tests by network function or location
+
+Example inventory structure:
+
+```yaml
+---
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.111
+    name: DC1-LEAF1A
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.112
+    name: DC1-LEAF1B
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.201
+    name: DC2-SPINE1
+    tags: ["SPINE", "DC2"]
+
+  - host: 172.20.20.202
+    name: DC2-SPINE2
+    tags: ["SPINE", "DC2"]
+  # ... more devices
+```
+
+Example test catalog structure (one catalog for all devices):
+
+```yaml
+anta.tests.vxlan:
+  - VerifyVxlan1Interface:
+      filters:
+        tags: ["BL", "LEAF"]
+  - VerifyVxlanConfigSanity:
+      filters:
+        tags: ["BL", "LEAF"]
+
+anta.tests.routing:
+  generic:
+    - VerifyRoutingProtocolModel:
+        model: multi-agent
+        filters:
+          tags: ["SPINE"]
+```
+
+#### Optimizing Catalog Distribution
+
+For large fabrics, consider:
+
+1. Creating separate catalogs per device type
+2. Organizing catalogs by POD or datacenter
+3. Using tag-based filtering within catalogs
+
+Benefits:
+
+- Reduced memory usage per ANTA instance
+- Faster catalog processing
+- More efficient test execution
+
+### Implementation
+
+#### Device-Based Implementation
+
+1. Create the device-specific test runner BASH script (`nrfu.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    set -euo pipefail
+
+    exec 2>&1
+
+    usage() {
+        echo "Usage: $0 <node-name> [<nrfu-reports-path>]"
+        exit 1
+    }
+
+    ANTA_INVENTORY="anta_inventory.yml"
+    TEST_CATALOGS_PATH="intended/test_catalogs"
+    NODE_NAME=${1:-}
+    NRFU_REPORT_PATH=${2:-nrfu_reports}
+
+
+    if [[ -z "$NODE_NAME" ]]; then
+        usage
+    fi
+
+    anta nrfu \
+        -d "$NODE_NAME" \
+        -i "$ANTA_INVENTORY" \
+        -c "$TEST_CATALOGS_PATH/$NODE_NAME-catalog.yml" \
+        json -o "$NRFU_REPORT_PATH/$NODE_NAME.json"
+    ```
+
+    Change `ANTA_INVENTORY` and `TEST_CATALOGS_PATH` to match your environment.
+
+2. Create the parallel execution script (`run.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    mkdir -p nrfu_reports
+
+    NODE_NAMES=$(yq '.anta_inventory.hosts[].name' < "anta_inventory.yml" | tr -d '"')
+
+    parallel --tag './nrfu.sh {} nrfu_reports' ::: $NODE_NAMES || NRFU_STATUS=$?
+
+    exit $NRFU_STATUS
+    ```
+
+    Also change `anta_inventory.yml` to match your inventory file specified in `ANTA_INVENTORY` of `nrfu.sh`.
+
+3. Make both scripts executable:
+
+    ```bash
+    chmod +x nrfu.sh run.sh
+    ```
+
+4. Run the script:
+
+    ```bash
+    ./run.sh
+    ```
+
+5. The script will execute ANTA tests for each device in parallel, generating a JSON report for each device in the `nrfu_reports` directory.
+
+#### Tag-Based Implementation
+
+1. Create the tag-specific test runner (`nrfu-tag.sh`):
+
+    ```bash
+    #!/bin/bash
+    set -euo pipefail
+
+    exec 2>&1
+
+    usage() {
+        echo "Usage: $0 <tag-name> [<nrfu-reports-path>]"
+        exit 1
+    }
+
+    ANTA_INVENTORY="anta_inventory.yml"
+    ANTA_CATALOG="anta_catalog.yml"
+    TAG_NAME=${1:-}
+    NRFU_REPORT_PATH=${2:-nrfu_reports}
+
+    if [[ -z "$TAG_NAME" ]]; then
+        usage
+    fi
+
+    anta nrfu \
+        --tags "$TAG_NAME" \
+        -i "$ANTA_INVENTORY" \
+        -c "$ANTA_CATALOG" \
+        json -o "$NRFU_REPORT_PATH/${TAG_NAME}.json"
+    ```
+
+    Same as before, adjust `ANTA_INVENTORY`, `ANTA_CATALOG` to match your environment.
+
+2. Create the parallel execution script (`run-tag.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    mkdir -p nrfu_reports
+
+    TAGS=$(yq -r '.. | select(.filters?) | .filters.tags[]' < "anta_catalog.yml" | sort -u)
+
+    parallel --tag './nrfu_tag.sh {} nrfu_reports' ::: $TAGS || NRFU_STATUS=$?
+
+    exit $NRFU_STATUS
+    ```
+
+    Change `anta_catalog.yml` to match your catalog file specified in `ANTA_CATALOG` of `nrfu-tag.sh`.
+
+3. Make both scripts executable:
+
+    ```bash
+    chmod +x nrfu-tag.sh run-tag.sh
+    ```
+
+4. Run the script:
+
+    ```bash
+    ./run-tag.sh
+    ```
+
+5. The script will execute ANTA tests for each tag in parallel, generating a JSON report for each tag in the `nrfu_reports` directory.
+
+### Performance Considerations
+
+#### Parallel Job Control
+
+The `-j` option in `GNU Parallel` controls the number of concurrent jobs. Consider these factors when setting the job limit:
+
+1. **Available CPU cores**:
+
+     - Recommend: jobs = number of cores - 1
+     - Example: For 8 cores, use `-j7`
+
+2. **Memory requirements**:
+
+     - Each ANTA instance requires memory
+     - Monitor system resources during execution using `htop` or similar tools
+
+#### JSON vs YAML
+
+When working with large catalogs and inventories, using JSON format instead of YAML can provide better performance as ANTA loads JSON files more efficiently. If you choose to use JSON:
+
+- Convert your YAML files to JSON (you can use `yq` for this)
+- Modify the scripts to use `jq` instead of `yq`:
+
+  ```bash
+  # For device-based implementation
+  NODE_NAMES=$(jq -r '.anta_inventory.hosts[].name' < "anta_inventory.json")
+
+  # For tag-based implementation
+  TAGS=$(jq -r '.. | select(.filters?) | .filters.tags[]' < "anta_catalog.json" | sort -u)
+  ```
+
+  Example of converting YAML to JSON:
+
+  ```bash
+  yq -o=json eval 'anta_inventory.yaml' > anta_inventory.json
+  yq -o=json eval 'anta_catalog.yaml' > anta_catalog.json
+  ```
+
+### Results Management
+
+ANTA can output results in various formats:
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: do you need to repeat these? I would just point to the NRFU page documentatin and say you use JSON (less maintenance)
created_at: 2024-11-14T14:17:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
+
+2. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=5
+    ```
+
+    These values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+3. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_TIMEOUT=3600.0
+    ```
+
+    This is **very** important for large fabrics. Increase the global timeout to prevent test failures due to timeouts. You shouldn't need to adjust the other timeout values unless you have specific requirements.
+
+## 🔀 Multi-Process Scaling
+
+When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section will provide a few examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
+
+By leveraging `GNU Parallel` and proper test organization, we can significantly reduce execution time and improve overall performance while maintaining accurate test results.
+
+### Prerequisites
+
+Before implementing multi-process scaling:
+
+- 🏗️ Understand your fabric topology
+- 🎯 Identify natural test boundaries (e.g., devices, PODs, roles, etc.)
+- 📋 Plan your test catalog(s) accordingly
+- 💻 Understand basic Linux command-line operations
+
+### Installation Requirements
+
+!!! info "System Requirements"
+    The following procedures and examples were tested on **Ubuntu 22.04**, using `GNU Parallel` version `20241022` and `yq` version `v4.44.3`. Please refer the tool's documentation for specific installation instructions.
+
+#### Installing Required Tools
+
+1. Install `GNU Parallel` using the following commands:
+
+    ```bash
+    apt-get install parallel
+    ```
+
+2. Install `yq` using the following commands:
+
+    ```bash
+    wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
+        chmod +x /usr/bin/yq
+    ```
+
+3. Confirm the installation by running:
+
+    ```bash
+    parallel --version
+    yq --version
+    ```
+
+    You should see the version information for both tools if the installation was successful:
+
+    ```bash
+    (.dev) ~ parallel --version
+    GNU parallel 20241022
+    Copyright (C) 2007-2024 Ole Tange, http://ole.tange.dk and Free Software
+    Foundation, Inc.
+    License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
+    This is free software: you are free to change and redistribute it.
+    GNU parallel comes with no warranty.
+
+    Web site: https://www.gnu.org/software/parallel
+
+    When using programs that use GNU Parallel to process data for publication
+    please cite as described in 'parallel --citation'.
+
+    (.dev) ~ yq --version
+    yq (https://github.com/mikefarah/yq/) version v4.44.3
+    ```
+
+### Scaling Strategies
+
+#### Device-Based Parallelization
+
+In this approach, we run ANTA instances in parallel, with each instance handling a specific device. This strategy is effective when:
+
+- Each device has unique test requirements, i.e., one catalog per device
+- You want to maintain separate results per device
+- You need fine-grained control over test execution
+
+Example inventory structure:
+
+```yaml
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+  # ... more devices
+```
+
+Example test catalog structure (one catalog per device):
+
+```yaml
+# DC1-SPINE2.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet1
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet1'
+
+# DC1-SPINE1.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet2
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet2'
+```
+
+#### Tag-Based Parallelization
+
+This approach runs parallel instances based on device tags, useful when:
+
+- Devices in the same role share similar test requirements
+- You want to test entire PODs or device groups simultaneously
+- You need to organize tests by network function or location
+
+Example inventory structure:
+
+```yaml
+---
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.111
+    name: DC1-LEAF1A
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.112
+    name: DC1-LEAF1B
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.201
+    name: DC2-SPINE1
+    tags: ["SPINE", "DC2"]
+
+  - host: 172.20.20.202
+    name: DC2-SPINE2
+    tags: ["SPINE", "DC2"]
+  # ... more devices
+```
+
+Example test catalog structure (one catalog for all devices):
+
+```yaml
+anta.tests.vxlan:
+  - VerifyVxlan1Interface:
+      filters:
+        tags: ["BL", "LEAF"]
+  - VerifyVxlanConfigSanity:
+      filters:
+        tags: ["BL", "LEAF"]
+
+anta.tests.routing:
+  generic:
+    - VerifyRoutingProtocolModel:
+        model: multi-agent
+        filters:
+          tags: ["SPINE"]
+```
+
+#### Optimizing Catalog Distribution
+
+For large fabrics, consider:
+
+1. Creating separate catalogs per device type
+2. Organizing catalogs by POD or datacenter
+3. Using tag-based filtering within catalogs
+
+Benefits:
+
+- Reduced memory usage per ANTA instance
+- Faster catalog processing
+- More efficient test execution
+
+### Implementation
+
+#### Device-Based Implementation
+
+1. Create the device-specific test runner BASH script (`nrfu.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    set -euo pipefail
+
+    exec 2>&1
+
+    usage() {
+        echo "Usage: $0 <node-name> [<nrfu-reports-path>]"
+        exit 1
+    }
+
+    ANTA_INVENTORY="anta_inventory.yml"
+    TEST_CATALOGS_PATH="intended/test_catalogs"
+    NODE_NAME=${1:-}
+    NRFU_REPORT_PATH=${2:-nrfu_reports}
+
+
+    if [[ -z "$NODE_NAME" ]]; then
+        usage
+    fi
+
+    anta nrfu \
+        -d "$NODE_NAME" \
+        -i "$ANTA_INVENTORY" \
+        -c "$TEST_CATALOGS_PATH/$NODE_NAME-catalog.yml" \
+        json -o "$NRFU_REPORT_PATH/$NODE_NAME.json"
+    ```
+
+    Change `ANTA_INVENTORY` and `TEST_CATALOGS_PATH` to match your environment.
+
+2. Create the parallel execution script (`run.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    mkdir -p nrfu_reports
+
+    NODE_NAMES=$(yq '.anta_inventory.hosts[].name' < "anta_inventory.yml" | tr -d '"')
+
+    parallel --tag './nrfu.sh {} nrfu_reports' ::: $NODE_NAMES || NRFU_STATUS=$?
+
+    exit $NRFU_STATUS
+    ```
+
+    Also change `anta_inventory.yml` to match your inventory file specified in `ANTA_INVENTORY` of `nrfu.sh`.
+
+3. Make both scripts executable:
+
+    ```bash
+    chmod +x nrfu.sh run.sh
+    ```
+
+4. Run the script:
+
+    ```bash
+    ./run.sh
+    ```
+
+5. The script will execute ANTA tests for each device in parallel, generating a JSON report for each device in the `nrfu_reports` directory.
+
+#### Tag-Based Implementation
+
+1. Create the tag-specific test runner (`nrfu-tag.sh`):
+
+    ```bash
+    #!/bin/bash
+    set -euo pipefail
+
+    exec 2>&1
+
+    usage() {
+        echo "Usage: $0 <tag-name> [<nrfu-reports-path>]"
+        exit 1
+    }
+
+    ANTA_INVENTORY="anta_inventory.yml"
+    ANTA_CATALOG="anta_catalog.yml"
+    TAG_NAME=${1:-}
+    NRFU_REPORT_PATH=${2:-nrfu_reports}
+
+    if [[ -z "$TAG_NAME" ]]; then
+        usage
+    fi
+
+    anta nrfu \
+        --tags "$TAG_NAME" \
+        -i "$ANTA_INVENTORY" \
+        -c "$ANTA_CATALOG" \
+        json -o "$NRFU_REPORT_PATH/${TAG_NAME}.json"
+    ```
+
+    Same as before, adjust `ANTA_INVENTORY`, `ANTA_CATALOG` to match your environment.
+
+2. Create the parallel execution script (`run-tag.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    mkdir -p nrfu_reports
+
+    TAGS=$(yq -r '.. | select(.filters?) | .filters.tags[]' < "anta_catalog.yml" | sort -u)
+
+    parallel --tag './nrfu_tag.sh {} nrfu_reports' ::: $TAGS || NRFU_STATUS=$?
+
+    exit $NRFU_STATUS
+    ```
+
+    Change `anta_catalog.yml` to match your catalog file specified in `ANTA_CATALOG` of `nrfu-tag.sh`.
+
+3. Make both scripts executable:
+
+    ```bash
+    chmod +x nrfu-tag.sh run-tag.sh
+    ```
+
+4. Run the script:
+
+    ```bash
+    ./run-tag.sh
+    ```
+
+5. The script will execute ANTA tests for each tag in parallel, generating a JSON report for each tag in the `nrfu_reports` directory.
+
+### Performance Considerations
+
+#### Parallel Job Control
+
+The `-j` option in `GNU Parallel` controls the number of concurrent jobs. Consider these factors when setting the job limit:
+
+1. **Available CPU cores**:
+
+     - Recommend: jobs = number of cores - 1
+     - Example: For 8 cores, use `-j7`
+
+2. **Memory requirements**:
+
+     - Each ANTA instance requires memory
+     - Monitor system resources during execution using `htop` or similar tools
+
+#### JSON vs YAML
+
+When working with large catalogs and inventories, using JSON format instead of YAML can provide better performance as ANTA loads JSON files more efficiently. If you choose to use JSON:
+
+- Convert your YAML files to JSON (you can use `yq` for this)
+- Modify the scripts to use `jq` instead of `yq`:
+
+  ```bash
+  # For device-based implementation
+  NODE_NAMES=$(jq -r '.anta_inventory.hosts[].name' < "anta_inventory.json")
+
+  # For tag-based implementation
+  TAGS=$(jq -r '.. | select(.filters?) | .filters.tags[]' < "anta_catalog.json" | sort -u)
+  ```
+
+  Example of converting YAML to JSON:
+
+  ```bash
+  yq -o=json eval 'anta_inventory.yaml' > anta_inventory.json
+  yq -o=json eval 'anta_catalog.yaml' > anta_catalog.json
+  ```
+
+### Results Management
+
+ANTA can output results in various formats:
+
+1. JSON format (default in our scripts):
+
+    ```bash
+    anta nrfu -d <device> -i <inventory> -c <catalog> json -o <output-file>
+    ```
+
+2. Markdown format:
+
+    ```bash
+    anta nrfu -d <device> -i <inventory> -c <catalog> md-report --md-output <output-file>
+    ```
+
+    Please refer to the [ANTA CLI documentation](https://anta.arista.com/stable/cli/nrfu/) for more information on result output formats.
+
+    !!! note "Output Formats"
+        The scripts provided in this guide output JSON files for each device or tag. You can modify them to output in other formats as needed.
+
+3. Optional: You can merge the JSON results and generate a JUnit report using the following Python script (requires `junitparser`):
+
+    ```python
+    from pathlib import Path
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: please use snippets for this and we will try to test it :)
created_at: 2024-11-14T14:18:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
+
+2. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=5
+    ```
+
+    These values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+3. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_TIMEOUT=3600.0
+    ```
+
+    This is **very** important for large fabrics. Increase the global timeout to prevent test failures due to timeouts. You shouldn't need to adjust the other timeout values unless you have specific requirements.
+
+## 🔀 Multi-Process Scaling
+
+When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section will provide a few examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
+
+By leveraging `GNU Parallel` and proper test organization, we can significantly reduce execution time and improve overall performance while maintaining accurate test results.
+
+### Prerequisites
+
+Before implementing multi-process scaling:
+
+- 🏗️ Understand your fabric topology
+- 🎯 Identify natural test boundaries (e.g., devices, PODs, roles, etc.)
+- 📋 Plan your test catalog(s) accordingly
+- 💻 Understand basic Linux command-line operations
+
+### Installation Requirements
+
+!!! info "System Requirements"
+    The following procedures and examples were tested on **Ubuntu 22.04**, using `GNU Parallel` version `20241022` and `yq` version `v4.44.3`. Please refer the tool's documentation for specific installation instructions.
+
+#### Installing Required Tools
+
+1. Install `GNU Parallel` using the following commands:
+
+    ```bash
+    apt-get install parallel
+    ```
+
+2. Install `yq` using the following commands:
+
+    ```bash
+    wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
+        chmod +x /usr/bin/yq
+    ```
+
+3. Confirm the installation by running:
+
+    ```bash
+    parallel --version
+    yq --version
+    ```
+
+    You should see the version information for both tools if the installation was successful:
+
+    ```bash
+    (.dev) ~ parallel --version
+    GNU parallel 20241022
+    Copyright (C) 2007-2024 Ole Tange, http://ole.tange.dk and Free Software
+    Foundation, Inc.
+    License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
+    This is free software: you are free to change and redistribute it.
+    GNU parallel comes with no warranty.
+
+    Web site: https://www.gnu.org/software/parallel
+
+    When using programs that use GNU Parallel to process data for publication
+    please cite as described in 'parallel --citation'.
+
+    (.dev) ~ yq --version
+    yq (https://github.com/mikefarah/yq/) version v4.44.3
+    ```
+
+### Scaling Strategies
+
+#### Device-Based Parallelization
+
+In this approach, we run ANTA instances in parallel, with each instance handling a specific device. This strategy is effective when:
+
+- Each device has unique test requirements, i.e., one catalog per device
+- You want to maintain separate results per device
+- You need fine-grained control over test execution
+
+Example inventory structure:
+
+```yaml
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+  # ... more devices
+```
+
+Example test catalog structure (one catalog per device):
+
+```yaml
+# DC1-SPINE2.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet1
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet1'
+
+# DC1-SPINE1.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet2
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet2'
+```
+
+#### Tag-Based Parallelization
+
+This approach runs parallel instances based on device tags, useful when:
+
+- Devices in the same role share similar test requirements
+- You want to test entire PODs or device groups simultaneously
+- You need to organize tests by network function or location
+
+Example inventory structure:
+
+```yaml
+---
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.111
+    name: DC1-LEAF1A
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.112
+    name: DC1-LEAF1B
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.201
+    name: DC2-SPINE1
+    tags: ["SPINE", "DC2"]
+
+  - host: 172.20.20.202
+    name: DC2-SPINE2
+    tags: ["SPINE", "DC2"]
+  # ... more devices
+```
+
+Example test catalog structure (one catalog for all devices):
+
+```yaml
+anta.tests.vxlan:
+  - VerifyVxlan1Interface:
+      filters:
+        tags: ["BL", "LEAF"]
+  - VerifyVxlanConfigSanity:
+      filters:
+        tags: ["BL", "LEAF"]
+
+anta.tests.routing:
+  generic:
+    - VerifyRoutingProtocolModel:
+        model: multi-agent
+        filters:
+          tags: ["SPINE"]
+```
+
+#### Optimizing Catalog Distribution
+
+For large fabrics, consider:
+
+1. Creating separate catalogs per device type
+2. Organizing catalogs by POD or datacenter
+3. Using tag-based filtering within catalogs
+
+Benefits:
+
+- Reduced memory usage per ANTA instance
+- Faster catalog processing
+- More efficient test execution
+
+### Implementation
+
+#### Device-Based Implementation
+
+1. Create the device-specific test runner BASH script (`nrfu.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    set -euo pipefail
+
+    exec 2>&1
+
+    usage() {
+        echo "Usage: $0 <node-name> [<nrfu-reports-path>]"
+        exit 1
+    }
+
+    ANTA_INVENTORY="anta_inventory.yml"
+    TEST_CATALOGS_PATH="intended/test_catalogs"
+    NODE_NAME=${1:-}
+    NRFU_REPORT_PATH=${2:-nrfu_reports}
+
+
+    if [[ -z "$NODE_NAME" ]]; then
+        usage
+    fi
+
+    anta nrfu \
+        -d "$NODE_NAME" \
+        -i "$ANTA_INVENTORY" \
+        -c "$TEST_CATALOGS_PATH/$NODE_NAME-catalog.yml" \
+        json -o "$NRFU_REPORT_PATH/$NODE_NAME.json"
+    ```
+
+    Change `ANTA_INVENTORY` and `TEST_CATALOGS_PATH` to match your environment.
+
+2. Create the parallel execution script (`run.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    mkdir -p nrfu_reports
+
+    NODE_NAMES=$(yq '.anta_inventory.hosts[].name' < "anta_inventory.yml" | tr -d '"')
+
+    parallel --tag './nrfu.sh {} nrfu_reports' ::: $NODE_NAMES || NRFU_STATUS=$?
+
+    exit $NRFU_STATUS
+    ```
+
+    Also change `anta_inventory.yml` to match your inventory file specified in `ANTA_INVENTORY` of `nrfu.sh`.
+
+3. Make both scripts executable:
+
+    ```bash
+    chmod +x nrfu.sh run.sh
+    ```
+
+4. Run the script:
+
+    ```bash
+    ./run.sh
+    ```
+
+5. The script will execute ANTA tests for each device in parallel, generating a JSON report for each device in the `nrfu_reports` directory.
+
+#### Tag-Based Implementation
+
+1. Create the tag-specific test runner (`nrfu-tag.sh`):
+
+    ```bash
+    #!/bin/bash
+    set -euo pipefail
+
+    exec 2>&1
+
+    usage() {
+        echo "Usage: $0 <tag-name> [<nrfu-reports-path>]"
+        exit 1
+    }
+
+    ANTA_INVENTORY="anta_inventory.yml"
+    ANTA_CATALOG="anta_catalog.yml"
+    TAG_NAME=${1:-}
+    NRFU_REPORT_PATH=${2:-nrfu_reports}
+
+    if [[ -z "$TAG_NAME" ]]; then
+        usage
+    fi
+
+    anta nrfu \
+        --tags "$TAG_NAME" \
+        -i "$ANTA_INVENTORY" \
+        -c "$ANTA_CATALOG" \
+        json -o "$NRFU_REPORT_PATH/${TAG_NAME}.json"
+    ```
+
+    Same as before, adjust `ANTA_INVENTORY`, `ANTA_CATALOG` to match your environment.
+
+2. Create the parallel execution script (`run-tag.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    mkdir -p nrfu_reports
+
+    TAGS=$(yq -r '.. | select(.filters?) | .filters.tags[]' < "anta_catalog.yml" | sort -u)
+
+    parallel --tag './nrfu_tag.sh {} nrfu_reports' ::: $TAGS || NRFU_STATUS=$?
+
+    exit $NRFU_STATUS
+    ```
+
+    Change `anta_catalog.yml` to match your catalog file specified in `ANTA_CATALOG` of `nrfu-tag.sh`.
+
+3. Make both scripts executable:
+
+    ```bash
+    chmod +x nrfu-tag.sh run-tag.sh
+    ```
+
+4. Run the script:
+
+    ```bash
+    ./run-tag.sh
+    ```
+
+5. The script will execute ANTA tests for each tag in parallel, generating a JSON report for each tag in the `nrfu_reports` directory.
+
+### Performance Considerations
+
+#### Parallel Job Control
+
+The `-j` option in `GNU Parallel` controls the number of concurrent jobs. Consider these factors when setting the job limit:
+
+1. **Available CPU cores**:
+
+     - Recommend: jobs = number of cores - 1
+     - Example: For 8 cores, use `-j7`
+
+2. **Memory requirements**:
+
+     - Each ANTA instance requires memory
+     - Monitor system resources during execution using `htop` or similar tools
+
+#### JSON vs YAML
+
+When working with large catalogs and inventories, using JSON format instead of YAML can provide better performance as ANTA loads JSON files more efficiently. If you choose to use JSON:
+
+- Convert your YAML files to JSON (you can use `yq` for this)
+- Modify the scripts to use `jq` instead of `yq`:
+
+  ```bash
+  # For device-based implementation
+  NODE_NAMES=$(jq -r '.anta_inventory.hosts[].name' < "anta_inventory.json")
+
+  # For tag-based implementation
+  TAGS=$(jq -r '.. | select(.filters?) | .filters.tags[]' < "anta_catalog.json" | sort -u)
+  ```
+
+  Example of converting YAML to JSON:
+
+  ```bash
+  yq -o=json eval 'anta_inventory.yaml' > anta_inventory.json
+  yq -o=json eval 'anta_catalog.yaml' > anta_catalog.json
+  ```
+
+### Results Management
+
+ANTA can output results in various formats:
+
+1. JSON format (default in our scripts):
+
+    ```bash
+    anta nrfu -d <device> -i <inventory> -c <catalog> json -o <output-file>
+    ```
+
+2. Markdown format:
+
+    ```bash
+    anta nrfu -d <device> -i <inventory> -c <catalog> md-report --md-output <output-file>
+    ```
+
+    Please refer to the [ANTA CLI documentation](https://anta.arista.com/stable/cli/nrfu/) for more information on result output formats.
+
+    !!! note "Output Formats"
+        The scripts provided in this guide output JSON files for each device or tag. You can modify them to output in other formats as needed.
+
+3. Optional: You can merge the JSON results and generate a JUnit report using the following Python script (requires `junitparser`):
+
+    ```python
+    from pathlib import Path
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: Should we provide this by default in ANTA as a feature?
created_at: 2024-11-14T14:18:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
+
+2. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=5
+    ```
+
+    These values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+3. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_TIMEOUT=3600.0
+    ```
+
+    This is **very** important for large fabrics. Increase the global timeout to prevent test failures due to timeouts. You shouldn't need to adjust the other timeout values unless you have specific requirements.
+
+## 🔀 Multi-Process Scaling
+
+When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section will provide a few examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
+
+By leveraging `GNU Parallel` and proper test organization, we can significantly reduce execution time and improve overall performance while maintaining accurate test results.
+
+### Prerequisites
+
+Before implementing multi-process scaling:
+
+- 🏗️ Understand your fabric topology
+- 🎯 Identify natural test boundaries (e.g., devices, PODs, roles, etc.)
+- 📋 Plan your test catalog(s) accordingly
+- 💻 Understand basic Linux command-line operations
+
+### Installation Requirements
+
+!!! info "System Requirements"
+    The following procedures and examples were tested on **Ubuntu 22.04**, using `GNU Parallel` version `20241022` and `yq` version `v4.44.3`. Please refer the tool's documentation for specific installation instructions.
+
+#### Installing Required Tools
+
+1. Install `GNU Parallel` using the following commands:
+
+    ```bash
+    apt-get install parallel
+    ```
+
+2. Install `yq` using the following commands:
+
+    ```bash
+    wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
+        chmod +x /usr/bin/yq
+    ```
+
+3. Confirm the installation by running:
+
+    ```bash
+    parallel --version
+    yq --version
+    ```
+
+    You should see the version information for both tools if the installation was successful:
+
+    ```bash
+    (.dev) ~ parallel --version
+    GNU parallel 20241022
+    Copyright (C) 2007-2024 Ole Tange, http://ole.tange.dk and Free Software
+    Foundation, Inc.
+    License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
+    This is free software: you are free to change and redistribute it.
+    GNU parallel comes with no warranty.
+
+    Web site: https://www.gnu.org/software/parallel
+
+    When using programs that use GNU Parallel to process data for publication
+    please cite as described in 'parallel --citation'.
+
+    (.dev) ~ yq --version
+    yq (https://github.com/mikefarah/yq/) version v4.44.3
+    ```
+
+### Scaling Strategies
+
+#### Device-Based Parallelization
+
+In this approach, we run ANTA instances in parallel, with each instance handling a specific device. This strategy is effective when:
+
+- Each device has unique test requirements, i.e., one catalog per device
+- You want to maintain separate results per device
+- You need fine-grained control over test execution
+
+Example inventory structure:
+
+```yaml
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+  # ... more devices
+```
+
+Example test catalog structure (one catalog per device):
+
+```yaml
+# DC1-SPINE2.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet1
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet1'
+
+# DC1-SPINE1.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet2
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet2'
+```
+
+#### Tag-Based Parallelization
+
+This approach runs parallel instances based on device tags, useful when:
+
+- Devices in the same role share similar test requirements
+- You want to test entire PODs or device groups simultaneously
+- You need to organize tests by network function or location
+
+Example inventory structure:
+
+```yaml
+---
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.111
+    name: DC1-LEAF1A
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.112
+    name: DC1-LEAF1B
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.201
+    name: DC2-SPINE1
+    tags: ["SPINE", "DC2"]
+
+  - host: 172.20.20.202
+    name: DC2-SPINE2
+    tags: ["SPINE", "DC2"]
+  # ... more devices
+```
+
+Example test catalog structure (one catalog for all devices):
+
+```yaml
+anta.tests.vxlan:
+  - VerifyVxlan1Interface:
+      filters:
+        tags: ["BL", "LEAF"]
+  - VerifyVxlanConfigSanity:
+      filters:
+        tags: ["BL", "LEAF"]
+
+anta.tests.routing:
+  generic:
+    - VerifyRoutingProtocolModel:
+        model: multi-agent
+        filters:
+          tags: ["SPINE"]
+```
+
+#### Optimizing Catalog Distribution
+
+For large fabrics, consider:
+
+1. Creating separate catalogs per device type
+2. Organizing catalogs by POD or datacenter
+3. Using tag-based filtering within catalogs
+
+Benefits:
+
+- Reduced memory usage per ANTA instance
+- Faster catalog processing
+- More efficient test execution
+
+### Implementation
+
+#### Device-Based Implementation
+
+1. Create the device-specific test runner BASH script (`nrfu.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    set -euo pipefail
+
+    exec 2>&1
+
+    usage() {
+        echo "Usage: $0 <node-name> [<nrfu-reports-path>]"
+        exit 1
+    }
+
+    ANTA_INVENTORY="anta_inventory.yml"
+    TEST_CATALOGS_PATH="intended/test_catalogs"
+    NODE_NAME=${1:-}
+    NRFU_REPORT_PATH=${2:-nrfu_reports}
+
+
+    if [[ -z "$NODE_NAME" ]]; then
+        usage
+    fi
+
+    anta nrfu \
+        -d "$NODE_NAME" \
+        -i "$ANTA_INVENTORY" \
+        -c "$TEST_CATALOGS_PATH/$NODE_NAME-catalog.yml" \
+        json -o "$NRFU_REPORT_PATH/$NODE_NAME.json"
+    ```
+
+    Change `ANTA_INVENTORY` and `TEST_CATALOGS_PATH` to match your environment.
+
+2. Create the parallel execution script (`run.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    mkdir -p nrfu_reports
+
+    NODE_NAMES=$(yq '.anta_inventory.hosts[].name' < "anta_inventory.yml" | tr -d '"')
+
+    parallel --tag './nrfu.sh {} nrfu_reports' ::: $NODE_NAMES || NRFU_STATUS=$?
+
+    exit $NRFU_STATUS
+    ```
+
+    Also change `anta_inventory.yml` to match your inventory file specified in `ANTA_INVENTORY` of `nrfu.sh`.
+
+3. Make both scripts executable:
+
+    ```bash
+    chmod +x nrfu.sh run.sh
+    ```
+
+4. Run the script:
+
+    ```bash
+    ./run.sh
+    ```
+
+5. The script will execute ANTA tests for each device in parallel, generating a JSON report for each device in the `nrfu_reports` directory.
+
+#### Tag-Based Implementation
+
+1. Create the tag-specific test runner (`nrfu-tag.sh`):
+
+    ```bash
+    #!/bin/bash
+    set -euo pipefail
+
+    exec 2>&1
+
+    usage() {
+        echo "Usage: $0 <tag-name> [<nrfu-reports-path>]"
+        exit 1
+    }
+
+    ANTA_INVENTORY="anta_inventory.yml"
+    ANTA_CATALOG="anta_catalog.yml"
+    TAG_NAME=${1:-}
+    NRFU_REPORT_PATH=${2:-nrfu_reports}
+
+    if [[ -z "$TAG_NAME" ]]; then
+        usage
+    fi
+
+    anta nrfu \
+        --tags "$TAG_NAME" \
+        -i "$ANTA_INVENTORY" \
+        -c "$ANTA_CATALOG" \
+        json -o "$NRFU_REPORT_PATH/${TAG_NAME}.json"
+    ```
+
+    Same as before, adjust `ANTA_INVENTORY`, `ANTA_CATALOG` to match your environment.
+
+2. Create the parallel execution script (`run-tag.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    mkdir -p nrfu_reports
+
+    TAGS=$(yq -r '.. | select(.filters?) | .filters.tags[]' < "anta_catalog.yml" | sort -u)
+
+    parallel --tag './nrfu_tag.sh {} nrfu_reports' ::: $TAGS || NRFU_STATUS=$?
+
+    exit $NRFU_STATUS
+    ```
+
+    Change `anta_catalog.yml` to match your catalog file specified in `ANTA_CATALOG` of `nrfu-tag.sh`.
+
+3. Make both scripts executable:
+
+    ```bash
+    chmod +x nrfu-tag.sh run-tag.sh
+    ```
+
+4. Run the script:
+
+    ```bash
+    ./run-tag.sh
+    ```
+
+5. The script will execute ANTA tests for each tag in parallel, generating a JSON report for each tag in the `nrfu_reports` directory.
+
+### Performance Considerations
+
+#### Parallel Job Control
+
+The `-j` option in `GNU Parallel` controls the number of concurrent jobs. Consider these factors when setting the job limit:
+
+1. **Available CPU cores**:
+
+     - Recommend: jobs = number of cores - 1
+     - Example: For 8 cores, use `-j7`
+
+2. **Memory requirements**:
+
+     - Each ANTA instance requires memory
+     - Monitor system resources during execution using `htop` or similar tools
+
+#### JSON vs YAML
+
+When working with large catalogs and inventories, using JSON format instead of YAML can provide better performance as ANTA loads JSON files more efficiently. If you choose to use JSON:
+
+- Convert your YAML files to JSON (you can use `yq` for this)
+- Modify the scripts to use `jq` instead of `yq`:
+
+  ```bash
+  # For device-based implementation
+  NODE_NAMES=$(jq -r '.anta_inventory.hosts[].name' < "anta_inventory.json")
+
+  # For tag-based implementation
+  TAGS=$(jq -r '.. | select(.filters?) | .filters.tags[]' < "anta_catalog.json" | sort -u)
+  ```
+
+  Example of converting YAML to JSON:
+
+  ```bash
+  yq -o=json eval 'anta_inventory.yaml' > anta_inventory.json
+  yq -o=json eval 'anta_catalog.yaml' > anta_catalog.json
+  ```
+
+### Results Management
+
+ANTA can output results in various formats:
+
+1. JSON format (default in our scripts):
+
+    ```bash
+    anta nrfu -d <device> -i <inventory> -c <catalog> json -o <output-file>
+    ```
+
+2. Markdown format:
+
+    ```bash
+    anta nrfu -d <device> -i <inventory> -c <catalog> md-report --md-output <output-file>
+    ```
+
+    Please refer to the [ANTA CLI documentation](https://anta.arista.com/stable/cli/nrfu/) for more information on result output formats.
+
+    !!! note "Output Formats"
+        The scripts provided in this guide output JSON files for each device or tag. You can modify them to output in other formats as needed.
+
+3. Optional: You can merge the JSON results and generate a JUnit report using the following Python script (requires `junitparser`):
+
+    ```python
+    from pathlib import Path
+
+    from junitparser import TestCase, TestSuite, JUnitXml, Skipped, Error, Failure
+    import json
+
+    xml = JUnitXml()
+
+    for report_path in Path("nrfu_reports").glob("*.json"):
+        node_name = report_path.stem
+        test_case_results = {}
+
+        print(f"Processing {report_path}")
+        with open(report_path, mode="r", encoding="utf-8") as fd:
+            nrfu_report = json.load(fd)
+
+        for report_idx, report in enumerate(nrfu_report):
+            report_test = report["test"]
+            test_case = test_case_results.setdefault(report_test, [])
+
+            report_result = report["result"]
+            if report_result == "success":
+                continue
+
+            report_message = ",".join(report["messages"])
+
+            result_cls = {
+                "error": Error,
+                "skipped": Skipped,
+                "failure": Failure,
+            }.get(report_result)
+            test_case.append(result_cls(message=report_message))
+
+        # Add suite to JunitXml
+        node_suite = TestSuite(node_name)
+        test_cases = []
+        n_tests = 0
+        for test_name, test_results in test_case_results.items():
+            test_case = TestCase(name=test_name, classname=f"{node_name}")
+            test_case.result = test_results
+            n_tests += len(test_results)
+            test_cases.append(test_case)
+        node_suite.add_testcases(test_cases)
+        xml.add_testsuite(node_suite)
+
+    xml.write("nrfu.xml", pretty=True)
+    ```
+
+    !!! tip "CI/CD Integration"
+        This report format can be used with CI/CD tools like Jenkins, GitLab, or GitHub Actions.
+
+## 🎉 Conclusion
+
+By implementing these scaling strategies, you can efficiently run ANTA tests on large network fabrics. The combination of parallel execution, optimized catalogs, and proper resource management ensures fast and reliable network validation.
+
+Remember to:
+
+- ✅ Choose the appropriate parallelization strategy
+- 📋 Optimize catalog organization
+- 📊 Monitor system resources
+- ⚡ Adjust parallel job limits based on your environment
+
+!!! tip "Need Help?"
+    Performance tuning can be complex and highly dependent on your specific deployment. Don't hesitate to:
+
+    - 📞 Reach out to your Arista SE for guidance
+    - 📝 Document your specific use case on [GitHub](https://github.com/aristanetworks/anta)
+    - 🔍 Share your findings with the community
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: repeat from the beginning - probably should be the same (or just a button that goes back to the beginning?:)
created_at: 2024-11-14T14:20:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,635 @@
+<!--
+  ~ Copyright (c) 2023-2024 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 🚀 Scaling ANTA: A Comprehensive Guide
+
+**Table of Contents:**
+
+- [🚀 Scaling ANTA: A Comprehensive Guide](#-scaling-anta-a-comprehensive-guide)
+  - [📖 Introduction](#-introduction)
+  - [🔄 Single Process Optimization](#-single-process-optimization)
+    - [Understanding ANTA's Asynchronous Nature](#understanding-antas-asynchronous-nature)
+    - [Resource Management](#resource-management)
+    - [Connection Management](#connection-management)
+    - [Timeouts Configuration](#timeouts-configuration)
+    - [Performance Tuning](#performance-tuning)
+  - [🔀 Multi-Process Scaling](#-multi-process-scaling)
+    - [Prerequisites](#prerequisites)
+    - [Installation Requirements](#installation-requirements)
+      - [Installing Required Tools](#installing-required-tools)
+    - [Scaling Strategies](#scaling-strategies)
+      - [Device-Based Parallelization](#device-based-parallelization)
+      - [Tag-Based Parallelization](#tag-based-parallelization)
+      - [Optimizing Catalog Distribution](#optimizing-catalog-distribution)
+    - [Implementation](#implementation)
+      - [Device-Based Implementation](#device-based-implementation)
+      - [Tag-Based Implementation](#tag-based-implementation)
+    - [Performance Considerations](#performance-considerations)
+      - [Parallel Job Control](#parallel-job-control)
+      - [JSON vs YAML](#json-vs-yaml)
+    - [Results Management](#results-management)
+  - [🎉 Conclusion](#-conclusion)
+  - [📚 References](#-references)
+
+## 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue on ANTA's [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+ANTA (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+We'll cover two main approaches:
+
+1. 🔄 Single Process Optimization: Leveraging ANTA's built-in asynchronous capabilities with resource management
+2. 🔀 Multi-Process Scaling: Running multiple ANTA instances in parallel (covered in [Part 2](#-multi-process-scaling))
+
+For large fabrics (500+ devices), you may need to combine both approaches for optimal performance.
+
+## 🔄 Single Process Optimization
+
+### Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python's `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- 🔄 Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+**Key Concept**: While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+### Resource Management
+
+ANTA now provides several environment variables to control resource usage:
+
+!!! note "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+1. **Test Concurrency**:
+
+    ```bash
+    export ANTA_MAX_CONCURRENCY=10000
+    ```
+
+    This limits the number of concurrent test coroutines to prevent event loop overload. ANTA will schedule coroutines up to this limit, then wait for some to complete before scheduling more.
+
+2. **File Descriptors**:
+
+    ```bash
+    export ANTA_NOFILE=16384
+    ```
+
+    Sets the maximum number of open file descriptors for the ANTA process, usually for handling device connections. At startup, ANTA sets its process’s soft limit to the maximum allowed (up to `16384`). This adjustment is necessary because the soft limit is typically set to `1024`, while the hard limit is often higher (system-dependent). If ANTA’s hard limit is lower than the number of selected tests in ANTA, the process may request more file descriptors than the operating system allows, causing an error. In such cases, a WARNING is displayed at startup.
+
+    To address this, consider increasing the hard limit for the user starting the ANTA process. You can check the current hard limit for a user by running the command `ulimit -n -H` in the terminal. To set a new limit, create the file `/etc/security/limits.d/10-anta.conf` with the following content:
+
+    ```bash
+    <user> hard nofile <value>
+    ```
+
+    Replace `<user>` with the username used to start the ANTA process, and `<value>` with the desired hard limit. The maximum value will depend on your system. After creating this file, log out of your current session and log back in for the changes to take effect.
+
+### Connection Management
+
+ANTA uses the `httpx` library as its underlying HTTP client for device connections. Each device has his own connection pool with a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Connection pooling can be tuned via:
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=100
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=20
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+**Best Practice**: For large fabrics, limiting connections (5-10 per device) has shown optimal performance in testing. `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+### Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts:
+
+```bash
+# Global timeout
+export ANTA_TIMEOUT=30.0
+
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=30.0
+```
+
+If not set, these values default to the global `timeout` of ANTA, which is `30.0` seconds by default. The global timeout can also be set via the `--timeout` command-line option. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+### Performance Tuning
+
+For optimal single-process performance, consider the following tuning parameters:
+
+1. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. You can increase or decrease this value gradually to find the best setting for your environment.
+
+2. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=5
+    ```
+
+    These values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+3. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_TIMEOUT=3600.0
+    ```
+
+    This is **very** important for large fabrics. Increase the global timeout to prevent test failures due to timeouts. You shouldn't need to adjust the other timeout values unless you have specific requirements.
+
+## 🔀 Multi-Process Scaling
+
+When single-process optimization isn't enough, you can scale ANTA horizontally by running multiple instances in parallel. This approach is particularly useful for large fabrics with 500+ devices. This section will provide a few examples of how to achieve this using the common tool [`GNU Parallel`](https://www.gnu.org/software/parallel/).
+
+By leveraging `GNU Parallel` and proper test organization, we can significantly reduce execution time and improve overall performance while maintaining accurate test results.
+
+### Prerequisites
+
+Before implementing multi-process scaling:
+
+- 🏗️ Understand your fabric topology
+- 🎯 Identify natural test boundaries (e.g., devices, PODs, roles, etc.)
+- 📋 Plan your test catalog(s) accordingly
+- 💻 Understand basic Linux command-line operations
+
+### Installation Requirements
+
+!!! info "System Requirements"
+    The following procedures and examples were tested on **Ubuntu 22.04**, using `GNU Parallel` version `20241022` and `yq` version `v4.44.3`. Please refer the tool's documentation for specific installation instructions.
+
+#### Installing Required Tools
+
+1. Install `GNU Parallel` using the following commands:
+
+    ```bash
+    apt-get install parallel
+    ```
+
+2. Install `yq` using the following commands:
+
+    ```bash
+    wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
+        chmod +x /usr/bin/yq
+    ```
+
+3. Confirm the installation by running:
+
+    ```bash
+    parallel --version
+    yq --version
+    ```
+
+    You should see the version information for both tools if the installation was successful:
+
+    ```bash
+    (.dev) ~ parallel --version
+    GNU parallel 20241022
+    Copyright (C) 2007-2024 Ole Tange, http://ole.tange.dk and Free Software
+    Foundation, Inc.
+    License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
+    This is free software: you are free to change and redistribute it.
+    GNU parallel comes with no warranty.
+
+    Web site: https://www.gnu.org/software/parallel
+
+    When using programs that use GNU Parallel to process data for publication
+    please cite as described in 'parallel --citation'.
+
+    (.dev) ~ yq --version
+    yq (https://github.com/mikefarah/yq/) version v4.44.3
+    ```
+
+### Scaling Strategies
+
+#### Device-Based Parallelization
+
+In this approach, we run ANTA instances in parallel, with each instance handling a specific device. This strategy is effective when:
+
+- Each device has unique test requirements, i.e., one catalog per device
+- You want to maintain separate results per device
+- You need fine-grained control over test execution
+
+Example inventory structure:
+
+```yaml
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+  # ... more devices
+```
+
+Example test catalog structure (one catalog per device):
+
+```yaml
+# DC1-SPINE2.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet1
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet1'
+
+# DC1-SPINE1.yml
+anta.tests.connectivity:
+- VerifyLLDPNeighbors:
+    neighbors:
+    - neighbor_device: DC1-LEAF1A
+      neighbor_port: Ethernet2
+      port: Ethernet1
+    result_overwrite:
+      custom_field: 'Local: Ethernet1 - Remote: DC1-LEAF1A Ethernet2'
+```
+
+#### Tag-Based Parallelization
+
+This approach runs parallel instances based on device tags, useful when:
+
+- Devices in the same role share similar test requirements
+- You want to test entire PODs or device groups simultaneously
+- You need to organize tests by network function or location
+
+Example inventory structure:
+
+```yaml
+---
+anta_inventory:
+  hosts:
+  - host: 172.20.20.101
+    name: DC1-SPINE1
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.102
+    name: DC1-SPINE2
+    tags: ["SPINE", "DC1"]
+
+  - host: 172.20.20.111
+    name: DC1-LEAF1A
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.112
+    name: DC1-LEAF1B
+    tags: ["LEAF", "DC1"]
+
+  - host: 172.20.20.201
+    name: DC2-SPINE1
+    tags: ["SPINE", "DC2"]
+
+  - host: 172.20.20.202
+    name: DC2-SPINE2
+    tags: ["SPINE", "DC2"]
+  # ... more devices
+```
+
+Example test catalog structure (one catalog for all devices):
+
+```yaml
+anta.tests.vxlan:
+  - VerifyVxlan1Interface:
+      filters:
+        tags: ["BL", "LEAF"]
+  - VerifyVxlanConfigSanity:
+      filters:
+        tags: ["BL", "LEAF"]
+
+anta.tests.routing:
+  generic:
+    - VerifyRoutingProtocolModel:
+        model: multi-agent
+        filters:
+          tags: ["SPINE"]
+```
+
+#### Optimizing Catalog Distribution
+
+For large fabrics, consider:
+
+1. Creating separate catalogs per device type
+2. Organizing catalogs by POD or datacenter
+3. Using tag-based filtering within catalogs
+
+Benefits:
+
+- Reduced memory usage per ANTA instance
+- Faster catalog processing
+- More efficient test execution
+
+### Implementation
+
+#### Device-Based Implementation
+
+1. Create the device-specific test runner BASH script (`nrfu.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    set -euo pipefail
+
+    exec 2>&1
+
+    usage() {
+        echo "Usage: $0 <node-name> [<nrfu-reports-path>]"
+        exit 1
+    }
+
+    ANTA_INVENTORY="anta_inventory.yml"
+    TEST_CATALOGS_PATH="intended/test_catalogs"
+    NODE_NAME=${1:-}
+    NRFU_REPORT_PATH=${2:-nrfu_reports}
+
+
+    if [[ -z "$NODE_NAME" ]]; then
+        usage
+    fi
+
+    anta nrfu \
+        -d "$NODE_NAME" \
+        -i "$ANTA_INVENTORY" \
+        -c "$TEST_CATALOGS_PATH/$NODE_NAME-catalog.yml" \
+        json -o "$NRFU_REPORT_PATH/$NODE_NAME.json"
+    ```
+
+    Change `ANTA_INVENTORY` and `TEST_CATALOGS_PATH` to match your environment.
+
+2. Create the parallel execution script (`run.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    mkdir -p nrfu_reports
+
+    NODE_NAMES=$(yq '.anta_inventory.hosts[].name' < "anta_inventory.yml" | tr -d '"')
+
+    parallel --tag './nrfu.sh {} nrfu_reports' ::: $NODE_NAMES || NRFU_STATUS=$?
+
+    exit $NRFU_STATUS
+    ```
+
+    Also change `anta_inventory.yml` to match your inventory file specified in `ANTA_INVENTORY` of `nrfu.sh`.
+
+3. Make both scripts executable:
+
+    ```bash
+    chmod +x nrfu.sh run.sh
+    ```
+
+4. Run the script:
+
+    ```bash
+    ./run.sh
+    ```
+
+5. The script will execute ANTA tests for each device in parallel, generating a JSON report for each device in the `nrfu_reports` directory.
+
+#### Tag-Based Implementation
+
+1. Create the tag-specific test runner (`nrfu-tag.sh`):
+
+    ```bash
+    #!/bin/bash
+    set -euo pipefail
+
+    exec 2>&1
+
+    usage() {
+        echo "Usage: $0 <tag-name> [<nrfu-reports-path>]"
+        exit 1
+    }
+
+    ANTA_INVENTORY="anta_inventory.yml"
+    ANTA_CATALOG="anta_catalog.yml"
+    TAG_NAME=${1:-}
+    NRFU_REPORT_PATH=${2:-nrfu_reports}
+
+    if [[ -z "$TAG_NAME" ]]; then
+        usage
+    fi
+
+    anta nrfu \
+        --tags "$TAG_NAME" \
+        -i "$ANTA_INVENTORY" \
+        -c "$ANTA_CATALOG" \
+        json -o "$NRFU_REPORT_PATH/${TAG_NAME}.json"
+    ```
+
+    Same as before, adjust `ANTA_INVENTORY`, `ANTA_CATALOG` to match your environment.
+
+2. Create the parallel execution script (`run-tag.sh`):
+
+    ```bash
+    #!/bin/bash
+
+    mkdir -p nrfu_reports
+
+    TAGS=$(yq -r '.. | select(.filters?) | .filters.tags[]' < "anta_catalog.yml" | sort -u)
+
+    parallel --tag './nrfu_tag.sh {} nrfu_reports' ::: $TAGS || NRFU_STATUS=$?
+
+    exit $NRFU_STATUS
+    ```
+
+    Change `anta_catalog.yml` to match your catalog file specified in `ANTA_CATALOG` of `nrfu-tag.sh`.
+
+3. Make both scripts executable:
+
+    ```bash
+    chmod +x nrfu-tag.sh run-tag.sh
+    ```
+
+4. Run the script:
+
+    ```bash
+    ./run-tag.sh
+    ```
+
+5. The script will execute ANTA tests for each tag in parallel, generating a JSON report for each tag in the `nrfu_reports` directory.
+
+### Performance Considerations
+
+#### Parallel Job Control
+
+The `-j` option in `GNU Parallel` controls the number of concurrent jobs. Consider these factors when setting the job limit:
+
+1. **Available CPU cores**:
+
+     - Recommend: jobs = number of cores - 1
+     - Example: For 8 cores, use `-j7`
+
+2. **Memory requirements**:
+
+     - Each ANTA instance requires memory
+     - Monitor system resources during execution using `htop` or similar tools
+
+#### JSON vs YAML
+
+When working with large catalogs and inventories, using JSON format instead of YAML can provide better performance as ANTA loads JSON files more efficiently. If you choose to use JSON:
+
+- Convert your YAML files to JSON (you can use `yq` for this)
+- Modify the scripts to use `jq` instead of `yq`:
+
+  ```bash
+  # For device-based implementation
+  NODE_NAMES=$(jq -r '.anta_inventory.hosts[].name' < "anta_inventory.json")
+
+  # For tag-based implementation
+  TAGS=$(jq -r '.. | select(.filters?) | .filters.tags[]' < "anta_catalog.json" | sort -u)
+  ```
+
+  Example of converting YAML to JSON:
+
+  ```bash
+  yq -o=json eval 'anta_inventory.yaml' > anta_inventory.json
+  yq -o=json eval 'anta_catalog.yaml' > anta_catalog.json
+  ```
+
+### Results Management
+
+ANTA can output results in various formats:
+
+1. JSON format (default in our scripts):
+
+    ```bash
+    anta nrfu -d <device> -i <inventory> -c <catalog> json -o <output-file>
+    ```
+
+2. Markdown format:
+
+    ```bash
+    anta nrfu -d <device> -i <inventory> -c <catalog> md-report --md-output <output-file>
+    ```
+
+    Please refer to the [ANTA CLI documentation](https://anta.arista.com/stable/cli/nrfu/) for more information on result output formats.
+
+    !!! note "Output Formats"
+        The scripts provided in this guide output JSON files for each device or tag. You can modify them to output in other formats as needed.
+
+3. Optional: You can merge the JSON results and generate a JUnit report using the following Python script (requires `junitparser`):
+
+    ```python
+    from pathlib import Path
+
+    from junitparser import TestCase, TestSuite, JUnitXml, Skipped, Error, Failure
+    import json
+
+    xml = JUnitXml()
+
+    for report_path in Path("nrfu_reports").glob("*.json"):
+        node_name = report_path.stem
+        test_case_results = {}
+
+        print(f"Processing {report_path}")
+        with open(report_path, mode="r", encoding="utf-8") as fd:
+            nrfu_report = json.load(fd)
+
+        for report_idx, report in enumerate(nrfu_report):
+            report_test = report["test"]
+            test_case = test_case_results.setdefault(report_test, [])
+
+            report_result = report["result"]
+            if report_result == "success":
+                continue
+
+            report_message = ",".join(report["messages"])
+
+            result_cls = {
+                "error": Error,
+                "skipped": Skipped,
+                "failure": Failure,
+            }.get(report_result)
+            test_case.append(result_cls(message=report_message))
+
+        # Add suite to JunitXml
+        node_suite = TestSuite(node_name)
+        test_cases = []
+        n_tests = 0
+        for test_name, test_results in test_case_results.items():
+            test_case = TestCase(name=test_name, classname=f"{node_name}")
+            test_case.result = test_results
+            n_tests += len(test_results)
+            test_cases.append(test_case)
+        node_suite.add_testcases(test_cases)
+        xml.add_testsuite(node_suite)
+
+    xml.write("nrfu.xml", pretty=True)
+    ```
+
+    !!! tip "CI/CD Integration"
+        This report format can be used with CI/CD tools like Jenkins, GitLab, or GitHub Actions.
+
+## 🎉 Conclusion
+
+By implementing these scaling strategies, you can efficiently run ANTA tests on large network fabrics. The combination of parallel execution, optimized catalogs, and proper resource management ensures fast and reliable network validation.
+
+Remember to:
+
+- ✅ Choose the appropriate parallelization strategy
+- 📋 Optimize catalog organization
+- 📊 Monitor system resources
+- ⚡ Adjust parallel job limits based on your environment
+
+!!! tip "Need Help?"
+    Performance tuning can be complex and highly dependent on your specific deployment. Don't hesitate to:
+
+    - 📞 Reach out to your Arista SE for guidance
+    - 📝 Document your specific use case on [GitHub](https://github.com/aristanetworks/anta)
+    - 🔍 Share your findings with the community
+
+## 📚 References
+
+- **Python AsyncIO**
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: need 4 spaces it does not render properly:

![Screenshot 2024-11-14 at 15 20 40](https://github.com/user-attachments/assets/ea8672ac-9d8a-4557-a685-b443607692d0)

created_at: 2024-11-14T14:21:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,8 +7,7 @@
 
 from typing import TYPE_CHECKING
 
-from anta.result_manager import ResultManager
-from anta.runner import get_coroutines, prepare_tests
+from anta.runner import prepare_tests
path: tests/benchmark/test_runner.py
user: login: gmuloc
body: we can add back get_coroutines for now
created_at: 2024-11-14T14:22:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,195 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+
+from httpx import Limits, Timeout
+from pydantic import Field, NonNegativeFloat, NonNegativeInt, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default values for HTTPX resource limits
+DEFAULT_HTTPX_MAX_CONNECTIONS = 100
+DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS = 20
+DEFAULT_HTTPX_KEEPALIVE_EXPIRY = 5.0
+
+# Default values for HTTPX timeouts
+DEFAULT_HTTPX_CONNECT_TIMEOUT = 5.0
+DEFAULT_HTTPX_READ_TIMEOUT = 5.0
+DEFAULT_HTTPX_WRITE_TIMEOUT = 5.0
+DEFAULT_HTTPX_POOL_TIMEOUT = 5.0
+
+# Default value for the maximum number of concurrent tests in the event loop
+DEFAULT_MAX_CONCURRENCY = 10000
+
+# Default value for the maximum number of open file descriptors for the ANTA process
+DEFAULT_NOFILE = 16384
+
+
+class FileDescriptorSettings(BaseSettings):
+    """Environment variable for configuring the maximum number of open file descriptors for the ANTA process.
+
+    On POSIX systems, this value is used to set the soft limit for the current process.
+    The value cannot exceed the system's hard limit.
+    """
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    nofile: PositiveInt = Field(default=DEFAULT_NOFILE)
+
+
+class MaxConcurrencySettings(BaseSettings):
+    """Environment variable for configuring the maximum number of concurrent tests in the event loop."""
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    max_concurrency: PositiveInt = Field(default=DEFAULT_MAX_CONCURRENCY)
+
+
+class HttpxResourceLimitsSettings(BaseSettings):
+    """Environment variables for configuring the underlying HTTPX client resource limits.
+
+    The limits are set using the following environment variables:
+        - ANTA_MAX_CONNECTIONS: Maximum number of allowable connections.
+        - ANTA_MAX_KEEPALIVE_CONNECTIONS: Number of allowable keep-alive connections.
+        - ANTA_KEEPALIVE_EXPIRY: Time limit on idle keep-alive connections in seconds.
+
+    If any environment variable is not set, the following HTTPX default limits are used:
+        - max_connections: 100
+        - max_keepalive_connections: 20
+        - keepalive_expiry: 5.0
+
+    These limits are set for all devices. `None` means no limit is set for the given operation.
+
+    For more information on HTTPX resource limits, see: https://www.python-httpx.org/advanced/resource-limits/
+    """
+
+    # The 'None' string is used to allow the environment variable to be set to `None`.
+    model_config = SettingsConfigDict(env_parse_none_str="None", env_prefix="ANTA_")
+
+    max_connections: NonNegativeInt | None = Field(default=DEFAULT_HTTPX_MAX_CONNECTIONS)
+    max_keepalive_connections: NonNegativeInt | None = Field(default=DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS)
+    keepalive_expiry: NonNegativeFloat | None = Field(default=DEFAULT_HTTPX_KEEPALIVE_EXPIRY)
+
+
+class HttpxTimeoutsSettings(BaseSettings):
+    """Environment variables for configuring the underlying HTTPX client timeouts.
+
+    The timeouts are set using the following environment variables:
+        - ANTA_CONNECT_TIMEOUT: Maximum amount of time to wait until a socket connection to the requested host is established.
+        - ANTA_READ_TIMEOUT: Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body).
+        - ANTA_WRITE_TIMEOUT: Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body).
+        - ANTA_POOL_TIMEOUT: Maximum duration to wait for acquiring a connection from the connection pool.
+
+    If any environment variable is not set, the default HTTPX timeout is used, 5 seconds.
+    `None` will disable the timeout for the given operation.
+
+    For more information on HTTPX timeouts, see: https://www.python-httpx.org/advanced/timeouts/
+    """
+
+    # The 'None' string is used to allow the environment variable to be set to `None`.
+    model_config = SettingsConfigDict(env_parse_none_str="None", env_prefix="ANTA_")
+
+    connect_timeout: NonNegativeFloat | None = Field(default=DEFAULT_HTTPX_CONNECT_TIMEOUT)
+    read_timeout: NonNegativeFloat | None = Field(default=DEFAULT_HTTPX_READ_TIMEOUT)
+    write_timeout: NonNegativeFloat | None = Field(default=DEFAULT_HTTPX_WRITE_TIMEOUT)
+    pool_timeout: NonNegativeFloat | None = Field(default=DEFAULT_HTTPX_POOL_TIMEOUT)
+
+    # The following properties are used to determine if a specific timeout was set by an environment variable
+    @property
+    def connect_set(self) -> bool:
+        """Return True if the connect timeout was set by an environment variable."""
+        return "connect_timeout" in self.model_fields_set
+
+    @property
+    def read_set(self) -> bool:
+        """Return True if the read timeout was set by an environment variable."""
+        return "read_timeout" in self.model_fields_set
+
+    @property
+    def write_set(self) -> bool:
+        """Return True if the write timeout was set by an environment variable."""
+        return "write_timeout" in self.model_fields_set
+
+    @property
+    def pool_set(self) -> bool:
+        """Return True if the pool timeout was set by an environment variable."""
+        return "pool_timeout" in self.model_fields_set
+
+
+def get_max_concurrency() -> int:
+    """Get the maximum number of concurrent tests that can run in the event loop."""
+    settings = MaxConcurrencySettings()
+    return settings.max_concurrency
+
+
+def get_httpx_limits() -> Limits:
+    """Get the HTTPX Limits object from environment variables."""
+    settings = HttpxResourceLimitsSettings()
+    return Limits(
+        max_connections=settings.max_connections,
+        max_keepalive_connections=settings.max_keepalive_connections,
+        keepalive_expiry=settings.keepalive_expiry,
+    )
+
+
+def get_httpx_timeout(default_timeout: float | None) -> Timeout:
+    """Get the HTTPX Timeout object from environment variables.
+
+    Parameters
+    ----------
+    default_timeout : float | None
path: anta/settings.py
user: login: mtache
body: Default values are defined twice here.
Once in this file, the other definition is in the timeout CLI option.
I would suggest to keep the default values in this file, remove the default value in Click and point to the documentation.
created_at: 2025-01-21T10:36:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -163,6 +190,7 @@ def core_options(f: Callable[..., Any]) -> Callable[..., Any]:
         show_envvar=True,
         envvar="ANTA_TIMEOUT",
         show_default=True,
+        type=FLOAT_OR_NONE,
path: anta/cli/utils.py
user: login: mtache
body: I would suggest to remove the default value here since it is defined in the anta.settings module already.
Then you can also get rid of the FLOAT_OR_NONE type :)
created_at: 2025-01-21T10:49:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,9 +181,10 @@ nav:
     - Debug commands: cli/debug.md
     - Tag Management: cli/tag-management.md
   - Advanced Usages:
-    - Caching in ANTA: advanced_usages/caching.md
     - Developing ANTA tests: advanced_usages/custom-tests.md
     - ANTA as a Python Library: advanced_usages/as-python-lib.md
+    - Caching in ANTA: advanced_usages/caching.md
+    - Scaling ANTA: advanced_usages/scaling.md
path: mkdocs.yml
user: login: mtache
body: Can we add a Settings section that render the BaseSettings models in anta.settings?
created_at: 2025-01-21T10:50:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,195 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+
+from httpx import Limits, Timeout
+from pydantic import Field, NonNegativeFloat, NonNegativeInt, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default values for HTTPX resource limits
+DEFAULT_HTTPX_MAX_CONNECTIONS = 100
+DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS = 20
+DEFAULT_HTTPX_KEEPALIVE_EXPIRY = 5.0
+
+# Default values for HTTPX timeouts
+DEFAULT_HTTPX_CONNECT_TIMEOUT = 5.0
+DEFAULT_HTTPX_READ_TIMEOUT = 5.0
+DEFAULT_HTTPX_WRITE_TIMEOUT = 5.0
+DEFAULT_HTTPX_POOL_TIMEOUT = 5.0
+
+# Default value for the maximum number of concurrent tests in the event loop
+DEFAULT_MAX_CONCURRENCY = 10000
+
+# Default value for the maximum number of open file descriptors for the ANTA process
+DEFAULT_NOFILE = 16384
+
+
+class FileDescriptorSettings(BaseSettings):
+    """Environment variable for configuring the maximum number of open file descriptors for the ANTA process.
+
+    On POSIX systems, this value is used to set the soft limit for the current process.
+    The value cannot exceed the system's hard limit.
+    """
path: anta/settings.py
user: login: mtache
body: Missing ANTA_NOFILE attribute in the doctsring.
created_at: 2025-01-21T11:14:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,195 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+
+from httpx import Limits, Timeout
+from pydantic import Field, NonNegativeFloat, NonNegativeInt, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default values for HTTPX resource limits
+DEFAULT_HTTPX_MAX_CONNECTIONS = 100
+DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS = 20
+DEFAULT_HTTPX_KEEPALIVE_EXPIRY = 5.0
+
+# Default values for HTTPX timeouts
+DEFAULT_HTTPX_CONNECT_TIMEOUT = 5.0
+DEFAULT_HTTPX_READ_TIMEOUT = 5.0
+DEFAULT_HTTPX_WRITE_TIMEOUT = 5.0
+DEFAULT_HTTPX_POOL_TIMEOUT = 5.0
+
+# Default value for the maximum number of concurrent tests in the event loop
+DEFAULT_MAX_CONCURRENCY = 10000
+
+# Default value for the maximum number of open file descriptors for the ANTA process
+DEFAULT_NOFILE = 16384
+
+
+class FileDescriptorSettings(BaseSettings):
+    """Environment variable for configuring the maximum number of open file descriptors for the ANTA process.
+
+    On POSIX systems, this value is used to set the soft limit for the current process.
+    The value cannot exceed the system's hard limit.
+    """
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    nofile: PositiveInt = Field(default=DEFAULT_NOFILE)
+
+
+class MaxConcurrencySettings(BaseSettings):
+    """Environment variable for configuring the maximum number of concurrent tests in the event loop."""
path: anta/settings.py
user: login: mtache
body: Missing ANTA_MAX_CONCURRENCY attribute in the docstring
created_at: 2025-01-21T11:14:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,195 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+
+from httpx import Limits, Timeout
+from pydantic import Field, NonNegativeFloat, NonNegativeInt, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default values for HTTPX resource limits
+DEFAULT_HTTPX_MAX_CONNECTIONS = 100
+DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS = 20
+DEFAULT_HTTPX_KEEPALIVE_EXPIRY = 5.0
+
+# Default values for HTTPX timeouts
+DEFAULT_HTTPX_CONNECT_TIMEOUT = 5.0
+DEFAULT_HTTPX_READ_TIMEOUT = 5.0
+DEFAULT_HTTPX_WRITE_TIMEOUT = 5.0
+DEFAULT_HTTPX_POOL_TIMEOUT = 5.0
+
+# Default value for the maximum number of concurrent tests in the event loop
+DEFAULT_MAX_CONCURRENCY = 10000
+
+# Default value for the maximum number of open file descriptors for the ANTA process
+DEFAULT_NOFILE = 16384
+
+
+class FileDescriptorSettings(BaseSettings):
+    """Environment variable for configuring the maximum number of open file descriptors for the ANTA process.
+
+    On POSIX systems, this value is used to set the soft limit for the current process.
+    The value cannot exceed the system's hard limit.
+    """
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    nofile: PositiveInt = Field(default=DEFAULT_NOFILE)
+
+
+class MaxConcurrencySettings(BaseSettings):
+    """Environment variable for configuring the maximum number of concurrent tests in the event loop."""
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    max_concurrency: PositiveInt = Field(default=DEFAULT_MAX_CONCURRENCY)
+
+
+class HttpxResourceLimitsSettings(BaseSettings):
+    """Environment variables for configuring the underlying HTTPX client resource limits.
+
+    The limits are set using the following environment variables:
path: anta/settings.py
user: login: mtache
body: Use attribute section to have a table in mkdocs?
created_at: 2025-01-21T11:15:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,508 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import get_file_descriptor_limit, get_max_concurrency
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from asyncio import Task
+    from collections.abc import AsyncGenerator, Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Track inventory filtering statistics during an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup and.
+
+    When initialized, the counters are set to zero.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int = 0
+    filtered_by_tags: int = 0
+    connection_failed: int = 0
+    established: int = 0
+
+
+class AntaRunnerScope(BaseModel):
+    """Define the scope of an ANTA test run.
+
+    The scope determines which devices and tests to include in a run, and how to
+    filter them. This class is used with the AntaRunner.run() method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If None, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If None, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When True, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If None, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    max_concurrency : int
+        Maximum number of tests that can run concurrently. Defaults from `get_max_concurrency()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    file_descriptor_limit : int
+        System file descriptor limit for connections. Defaults from `get_file_descriptor_limit()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        None if unknown, float('inf') if unlimited.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with _) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerScope
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerScope(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+    max_concurrency: int = Field(default_factory=get_max_concurrency)
+    file_descriptor_limit: int = Field(default_factory=get_file_descriptor_limit)
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = None
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+    _inventory_stats: AntaRunnerInventoryStats | None = None
+    _total_tests: int = 0
+    _potential_connections: float | None = None
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, scope: AntaRunnerScope | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        scope
+            Scope of the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        scope = scope or AntaRunnerScope()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(scope, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(scope):
+                    return manager
+
+            # Set up test coroutines
+            generator = self._setup_coroutines(manager=manager if dry_run else None)
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            async for test in generator:
+                test.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            async for result in self._run(generator):
+                manager.add(result)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, scope: AntaRunnerScope, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the CLI provided tags and devices if any
+        filtered_inventory = self.inventory.get_inventory(tags=scope.tags, devices=scope.devices) if scope.tags or scope.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
+        self._selected_inventory = filtered_inventory.get_inventory(established_only=scope.established_only)
+        connection_failed = len(filtered_inventory) - len(self._selected_inventory)
+
+        # If there are no devices in the inventory after filtering, exit
+        if not self._selected_inventory.devices:
+            # Build message parts
+            tag_msg = f"matching the tags {scope.tags} " if scope.tags else ""
+            device_msg = f" Selected devices: {scope.devices} " if scope.devices is not None else ""
+            logger.warning("No reachable device %swas found.%s", tag_msg, device_msg)
+            return False
+
+        self._inventory_stats = AntaRunnerInventoryStats(
+            total=total_devices, filtered_by_tags=filtered_by_tags, connection_failed=connection_failed, established=len(self._selected_inventory)
+        )
+        return True
+
+    def _setup_tests(self, scope: AntaRunnerScope) -> bool:
+        """Set up tests for the ANTA run."""
+        if self._selected_inventory is None:
+            msg = "The selected inventory is not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        # Build indexes for the catalog. If `tests` is set, filter the indexes based on these tests
+        self.catalog.build_indexes(filtered_tests=scope.tests)
+
+        # Using a set to avoid inserting duplicate tests
+        device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
+        total_tests = 0
+        total_connections = 0
+        unlimited_connections = False
+        all_have_connections = True
+
+        # Create the device to tests mapping from the tags and calculate connection stats
+        for device in self._selected_inventory.devices:
+            if scope.tags:
+                # If there are CLI tags, execute tests with matching tags for this device
+                if not (matching_tags := scope.tags.intersection(device.tags)):
+                    # The device does not have any selected tag, skipping
+                    continue
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(matching_tags))
+            else:
+                # If there is no CLI tags, execute all tests that do not have any tags
+                device_to_tests[device].update(self.catalog.tag_to_tests[None])
+
+                # Then add the tests with matching tags from device tags
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(device.tags))
+
+            total_tests += len(device_to_tests[device])
+
+            # Check device connections
+            if not hasattr(device, "max_connections"):
+                all_have_connections = False
+            elif device.max_connections is None:
+                unlimited_connections = True
+            else:
+                total_connections += device.max_connections
+
+        if total_tests == 0:
+            tag_msg = f" matching the tags {scope.tags} " if scope.tags else " "
+            logger.warning(
+                "There are no tests%sto run in the current test catalog and device inventory, please verify your inputs.",
+                tag_msg,
+            )
+            return False
+
+        self._selected_tests = device_to_tests
+        self._total_tests = total_tests
+        self._potential_connections = None if not all_have_connections else float("inf") if unlimited_connections else total_connections
+        return True
+
+    async def _setup_coroutines(self, *, manager: ResultManager | None = None) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """Set up test coroutines from selected tests for the ANTA run.
+
+        It creates an async generator of coroutines which are created by the `test` method of the AntaTest instances.
+        Each coroutine is a test to run.
+
+        Parameters
+        ----------
+        manager
+            An optional ResultManager instance to pre-populate with the test results. Used in `dry_run` mode.
+
+        Yields
+        ------
+            The coroutine (test) to run.
+        """
+        if self._selected_tests is None:
+            msg = "The selected tests are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        for device, test_definitions in self._selected_tests.items():
+            for test in test_definitions:
+                try:
+                    test_instance = test.test(device=device, inputs=test.inputs)
+                    if manager is not None:
+                        manager.add(test_instance.result)
+                    coroutine = test_instance.test()
+                except Exception as e:  # noqa: PERF203, BLE001
+                    # An AntaTest instance is potentially user-defined code.
+                    # We need to catch everything and exit gracefully with an error message.
+                    message = "\n".join(
+                        [
+                            f"There is an error when creating test {test.test.__module__}.{test.test.__name__}.",
+                            f"If this is not a custom test implementation: {GITHUB_SUGGESTION}",
+                        ],
+                    )
+                    anta_log_exception(e, message, logger)
+                else:
+                    yield coroutine
+
+    def _log_run_information(self, *, dry_run: bool = False) -> None:
+        """Log ANTA run information and potential resource limit warnings.
+
+        Parameters
+        ----------
+        dry_run
+            Dry-run mode flag.
+        """
+        if self._inventory_stats is None:
+            msg = "The inventory stats are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        width = min(int(console.width) - 34, len("  Total potential connections: Unlimited\n"))
+
+        # Build device information
+        device_lines = [
+            "Devices:",
+            f"  Total: {self._inventory_stats.total}",
+        ]
+        if self._inventory_stats.filtered_by_tags > 0:
+            device_lines.append(f"  Excluded by tags: {self._inventory_stats.filtered_by_tags}")
+        if self._inventory_stats.connection_failed > 0:
+            device_lines.append(f"  Failed to connect: {self._inventory_stats.connection_failed}")
+        device_lines.append(f"  Selected: {self._inventory_stats.established}{' (dry-run mode)' if dry_run else ''}")
+
+        # Build connection information
+        connections_line = (
+            ""
+            if self._potential_connections is None
+            else f"  Total potential connections: {'Unlimited' if self._potential_connections == float('inf') else self._potential_connections}\n"
+        )
+
+        run_info = (
+            f"{' ANTA NRFU Run Information ':-^{width}}\n"
+            f"{chr(10).join(device_lines)}\n"
+            f"Tests: {self._total_tests} total\n"
+            f"Limits:\n"
+            f"  Max concurrent tests: {self.max_concurrency}\n"
+            f"{connections_line}"
+            f"  Max file descriptors: {self.file_descriptor_limit}\n"
+            f"{'':-^{width}}"
+        )
+        logger.info(run_info)
+
+        # Log warnings for potential resource limits
+        if self._total_tests > self.max_concurrency:
+            logger.warning(
+                "Tests count (%s) exceeds concurrent limit (%s). Tests will be throttled. See Scaling ANTA documentation.", self._total_tests, self.max_concurrency
+            )
+        if self._potential_connections == float("inf"):
+            logger.warning(
+                "Running with unlimited connections. Connection errors may occur due to file descriptor limit (%s). See Scaling ANTA documentation.",
+                self.file_descriptor_limit,
+            )
+        elif self._potential_connections is not None and self._potential_connections > self.file_descriptor_limit:
+            logger.warning(
+                "Potential connections (%s) exceeds file descriptor limit (%s). Connection errors may occur. See Scaling ANTA documentation.",
+                self._potential_connections,
+                self.file_descriptor_limit,
+            )
+
+    async def _run(self, generator: AsyncGenerator[Coroutine[Any, Any, TestResult], None]) -> AsyncGenerator[TestResult, None]:
+        """Run tests with a concurrency limit.
+
+        This function takes an asynchronous generator of test coroutines and runs them
+        with a limit on the number of concurrent tests. It yields test results as each
+        test completes.
+
+        Inspired by: https://death.andgravity.com/limit-concurrency
+
+        Parameters
+        ----------
+        generator
+            An asynchronous generator that yields test coroutines.
+
+        Yields
+        ------
+            The result of each completed test.
+        """
+        if self.max_concurrency <= 0:
+            msg = "Concurrency limit must be greater than 0."
+            raise RuntimeError(msg)
+
+        # NOTE: The `aiter` built-in function is not available in Python 3.9
+        tests = generator.__aiter__()  # pylint: disable=unnecessary-dunder-call
+        tests_ended = False
+        tests_pending: set[Task[TestResult]] = set()
+
+        while tests_pending or not tests_ended:
+            # Add tests to the pending set until the limit is reached or no more tests are available
+            while len(tests_pending) < self.max_concurrency and not tests_ended:
+                try:
+                    # NOTE: The `anext` built-in function is not available in Python 3.9
+                    test = await tests.__anext__()  # pylint: disable=unnecessary-dunder-call
+                except StopAsyncIteration:  # noqa: PERF203
+                    tests_ended = True
+                    logger.debug("All tests have been added to the pending set.")
+                else:
+                    # Ensure the coroutine is scheduled to run and add it to the pending set
+                    tests_pending.add(asyncio.create_task(test))
+                    logger.debug("Added a test to the pending set: %s", test)
+
+            if len(tests_pending) >= self.max_concurrency:
+                logger.debug("Concurrency limit reached: %s tests running. Waiting for tests to complete.", self.max_concurrency)
+
+            if not tests_pending:
+                logger.debug("No pending tests and all tests have been processed. Exiting.")
+                return
path: anta/_runner.py
user: login: mtache
body: 
```suggestion
        if self.max_concurrency <= 0:
            msg = "Concurrency limit must be greater than 0."
            raise RuntimeError(msg)

        tests_ended = False
        tests_pending: set[Task[TestResult]] = set()

        while tests_pending or not tests_ended:
            async for test in generator:
                # Add tests to the pending set until the limit is reached or no more tests are available
                if len(tests_pending) < self.max_concurrency:
                    # Ensure the coroutine is scheduled to run and add it to the pending set
                    tests_pending.add(asyncio.create_task(test))
                    logger.debug("Added a test to the pending set: %s", test)
                else:
                    logger.debug("Concurrency limit reached: %s tests running. Waiting for tests to complete.", self.max_concurrency)
                    break
            if len(tests_pending) < self.max_concurrency and not tests_ended:
                tests_ended = True
                logger.debug("All tests have been added to the pending set.")

            if not tests_pending:
                logger.debug("No pending tests and all tests have been processed. Exiting.")
                return
```
created_at: 2025-01-21T11:28:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -378,15 +388,31 @@ def __init__(  # noqa: PLR0913
             message = f"'password' is required to instantiate device '{self.name}'"
             logger.error(message)
             raise ValueError(message)
+
         self.enable = enable
         self._enable_password = enable_password
-        self._session: asynceapi.Device = asynceapi.Device(host=host, port=port, username=username, password=password, proto=proto, timeout=timeout)
-        ssh_params: dict[str, Any] = {}
+
+        # Build the session settings for the `asynceapi` client
+        session_settings: dict[str, Any] = {
+            "host": host,
+            "port": port,
+            "username": username,
+            "password": password,
+            "proto": proto,
+            "timeout": httpx_timeout if httpx_timeout is not None else timeout,
+        }
+        if httpx_limits is not None:
+            session_settings["limits"] = httpx_limits
+            self.max_connections = httpx_limits.max_connections
+        else:
+            self.max_connections = DEFAULT_HTTPX_MAX_CONNECTIONS
path: anta/device.py
user: login: mtache
body: More robust logic to instantiate the `asynceapi.Device` object then retrieve `self._session.limits.max_connections`.
Because of https://github.com/encode/httpx/blob/10b7295922741b91a15751029e6ad3e8e5efb9f3/httpx/_client.py#L655, httpx default values will be used.
Use a read property?
created_at: 2025-01-21T15:06:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -354,14 +358,20 @@ def __init__(  # noqa: PLR0913
         tags
             Tags for this device.
         timeout
-            Timeout value in seconds for outgoing API calls.
-        insecure
-            Disable SSH Host Key validation.
+            Global timeout value in seconds for outgoing eAPI calls.
+        httpx_timeout
+            Optional HTTPX Timeout object for fine-grained timeout configuration.
+            If provided, it will override the global timeout.
path: anta/device.py
user: login: mtache
body: timeout defined twice.
Use `timeout: httpx.Timeout` object instead and remove `timeout: float`.
Use a read property to get the `Timeout` instance of this `AyncEOSDevice` instance.
created_at: 2025-01-21T15:10:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -378,15 +388,31 @@ def __init__(  # noqa: PLR0913
             message = f"'password' is required to instantiate device '{self.name}'"
             logger.error(message)
             raise ValueError(message)
+
         self.enable = enable
         self._enable_password = enable_password
-        self._session: asynceapi.Device = asynceapi.Device(host=host, port=port, username=username, password=password, proto=proto, timeout=timeout)
-        ssh_params: dict[str, Any] = {}
+
+        # Build the session settings for the `asynceapi` client
+        session_settings: dict[str, Any] = {
+            "host": host,
+            "port": port,
+            "username": username,
+            "password": password,
+            "proto": proto,
+            "timeout": httpx_timeout if httpx_timeout is not None else timeout,
+        }
+        if httpx_limits is not None:
+            session_settings["limits"] = httpx_limits
+            self.max_connections = httpx_limits.max_connections
+        else:
+            self.max_connections = DEFAULT_HTTPX_MAX_CONNECTIONS
+        self._session: asynceapi.Device = asynceapi.Device(**session_settings)
+
+        # Build the SSH connection options
+        ssh_settings = {"host": host, "port": ssh_port, "username": username, "password": password, "client_keys": CLIENT_KEYS}
         if insecure:
-            ssh_params["known_hosts"] = None
-        self._ssh_opts: SSHClientConnectionOptions = SSHClientConnectionOptions(
-            host=host, port=ssh_port, username=username, password=password, client_keys=CLIENT_KEYS, **ssh_params
-        )
+            ssh_settings["known_hosts"] = None
+        self._ssh_opts: SSHClientConnectionOptions = SSHClientConnectionOptions(**ssh_settings)
 
     def __rich_repr__(self) -> Iterator[tuple[str, Any]]:
path: anta/device.py
user: login: mtache
body: implement `timeout` and `limits` ?
created_at: 2025-01-21T15:10:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,195 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+
+from httpx import Limits, Timeout
+from pydantic import Field, NonNegativeFloat, NonNegativeInt, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default values for HTTPX resource limits
+DEFAULT_HTTPX_MAX_CONNECTIONS = 100
+DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS = 20
+DEFAULT_HTTPX_KEEPALIVE_EXPIRY = 5.0
+
+# Default values for HTTPX timeouts
+DEFAULT_HTTPX_CONNECT_TIMEOUT = 5.0
+DEFAULT_HTTPX_READ_TIMEOUT = 5.0
+DEFAULT_HTTPX_WRITE_TIMEOUT = 5.0
+DEFAULT_HTTPX_POOL_TIMEOUT = 5.0
path: anta/settings.py
user: login: mtache
body: Can we use https://github.com/encode/httpx/blob/10b7295922741b91a15751029e6ad3e8e5efb9f3/httpx/_config.py#L246 instead of copying the default value?
Not sure if the value will render in mkdocs.
created_at: 2025-01-21T15:13:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,6 +37,7 @@ def test_anta_dry_run(
 
     results = session_results[request.node.callspec.id]
 
+    # TODO: Use AntaRunner in ANTA v2.0.0
path: tests/benchmark/test_anta.py
user: login: mtache
body: Is this needed?
I think the `anta.runner.main` function uses the `AntaRunner` class already.
created_at: 2025-01-21T15:15:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,6 +70,7 @@ def test_anta(
 
     results = session_results[request.node.callspec.id]
 
+    # TODO: Use AntaRunner in ANTA v2.0.0
path: tests/benchmark/test_anta.py
user: login: mtache
body: Same here.
created_at: 2025-01-21T15:15:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,195 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+
+from httpx import Limits, Timeout
+from pydantic import Field, NonNegativeFloat, NonNegativeInt, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default values for HTTPX resource limits
+DEFAULT_HTTPX_MAX_CONNECTIONS = 100
+DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS = 20
+DEFAULT_HTTPX_KEEPALIVE_EXPIRY = 5.0
+
+# Default values for HTTPX timeouts
+DEFAULT_HTTPX_CONNECT_TIMEOUT = 5.0
+DEFAULT_HTTPX_READ_TIMEOUT = 5.0
+DEFAULT_HTTPX_WRITE_TIMEOUT = 5.0
+DEFAULT_HTTPX_POOL_TIMEOUT = 5.0
path: anta/settings.py
user: login: gmuloc
body: if it changes upstream it may make us sad one day
created_at: 2025-01-21T15:27:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,195 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+
+from httpx import Limits, Timeout
+from pydantic import Field, NonNegativeFloat, NonNegativeInt, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default values for HTTPX resource limits
+DEFAULT_HTTPX_MAX_CONNECTIONS = 100
+DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS = 20
+DEFAULT_HTTPX_KEEPALIVE_EXPIRY = 5.0
+
+# Default values for HTTPX timeouts
+DEFAULT_HTTPX_CONNECT_TIMEOUT = 5.0
+DEFAULT_HTTPX_READ_TIMEOUT = 5.0
+DEFAULT_HTTPX_WRITE_TIMEOUT = 5.0
+DEFAULT_HTTPX_POOL_TIMEOUT = 5.0
+
+# Default value for the maximum number of concurrent tests in the event loop
+DEFAULT_MAX_CONCURRENCY = 10000
+
+# Default value for the maximum number of open file descriptors for the ANTA process
+DEFAULT_NOFILE = 16384
+
+
+class FileDescriptorSettings(BaseSettings):
+    """Environment variable for configuring the maximum number of open file descriptors for the ANTA process.
+
+    On POSIX systems, this value is used to set the soft limit for the current process.
+    The value cannot exceed the system's hard limit.
+    """
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    nofile: PositiveInt = Field(default=DEFAULT_NOFILE)
+
+
+class MaxConcurrencySettings(BaseSettings):
+    """Environment variable for configuring the maximum number of concurrent tests in the event loop."""
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    max_concurrency: PositiveInt = Field(default=DEFAULT_MAX_CONCURRENCY)
+
+
+class HttpxResourceLimitsSettings(BaseSettings):
+    """Environment variables for configuring the underlying HTTPX client resource limits.
+
+    The limits are set using the following environment variables:
+        - ANTA_MAX_CONNECTIONS: Maximum number of allowable connections.
+        - ANTA_MAX_KEEPALIVE_CONNECTIONS: Number of allowable keep-alive connections.
+        - ANTA_KEEPALIVE_EXPIRY: Time limit on idle keep-alive connections in seconds.
+
+    If any environment variable is not set, the following HTTPX default limits are used:
+        - max_connections: 100
+        - max_keepalive_connections: 20
+        - keepalive_expiry: 5.0
+
+    These limits are set for all devices. `None` means no limit is set for the given operation.
+
+    For more information on HTTPX resource limits, see: https://www.python-httpx.org/advanced/resource-limits/
+    """
+
+    # The 'None' string is used to allow the environment variable to be set to `None`.
+    model_config = SettingsConfigDict(env_parse_none_str="None", env_prefix="ANTA_")
+
+    max_connections: NonNegativeInt | None = Field(default=DEFAULT_HTTPX_MAX_CONNECTIONS)
+    max_keepalive_connections: NonNegativeInt | None = Field(default=DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS)
+    keepalive_expiry: NonNegativeFloat | None = Field(default=DEFAULT_HTTPX_KEEPALIVE_EXPIRY)
+
+
+class HttpxTimeoutsSettings(BaseSettings):
+    """Environment variables for configuring the underlying HTTPX client timeouts.
+
+    The timeouts are set using the following environment variables:
+        - ANTA_CONNECT_TIMEOUT: Maximum amount of time to wait until a socket connection to the requested host is established.
+        - ANTA_READ_TIMEOUT: Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body).
+        - ANTA_WRITE_TIMEOUT: Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body).
+        - ANTA_POOL_TIMEOUT: Maximum duration to wait for acquiring a connection from the connection pool.
+
+    If any environment variable is not set, the default HTTPX timeout is used, 5 seconds.
+    `None` will disable the timeout for the given operation.
+
+    For more information on HTTPX timeouts, see: https://www.python-httpx.org/advanced/timeouts/
+    """
+
+    # The 'None' string is used to allow the environment variable to be set to `None`.
+    model_config = SettingsConfigDict(env_parse_none_str="None", env_prefix="ANTA_")
+
+    connect_timeout: NonNegativeFloat | None = Field(default=DEFAULT_HTTPX_CONNECT_TIMEOUT)
+    read_timeout: NonNegativeFloat | None = Field(default=DEFAULT_HTTPX_READ_TIMEOUT)
+    write_timeout: NonNegativeFloat | None = Field(default=DEFAULT_HTTPX_WRITE_TIMEOUT)
+    pool_timeout: NonNegativeFloat | None = Field(default=DEFAULT_HTTPX_POOL_TIMEOUT)
+
+    # The following properties are used to determine if a specific timeout was set by an environment variable
+    @property
+    def connect_set(self) -> bool:
+        """Return True if the connect timeout was set by an environment variable."""
+        return "connect_timeout" in self.model_fields_set
+
+    @property
+    def read_set(self) -> bool:
+        """Return True if the read timeout was set by an environment variable."""
+        return "read_timeout" in self.model_fields_set
+
+    @property
+    def write_set(self) -> bool:
+        """Return True if the write timeout was set by an environment variable."""
+        return "write_timeout" in self.model_fields_set
+
+    @property
+    def pool_set(self) -> bool:
+        """Return True if the pool timeout was set by an environment variable."""
+        return "pool_timeout" in self.model_fields_set
+
+
+def get_max_concurrency() -> int:
+    """Get the maximum number of concurrent tests that can run in the event loop."""
+    settings = MaxConcurrencySettings()
+    return settings.max_concurrency
+
+
+def get_httpx_limits() -> Limits:
+    """Get the HTTPX Limits object from environment variables."""
+    settings = HttpxResourceLimitsSettings()
+    return Limits(
+        max_connections=settings.max_connections,
+        max_keepalive_connections=settings.max_keepalive_connections,
+        keepalive_expiry=settings.keepalive_expiry,
+    )
+
+
+def get_httpx_timeout(default_timeout: float | None) -> Timeout:
+    """Get the HTTPX Timeout object from environment variables.
+
+    Parameters
+    ----------
+    default_timeout : float | None
path: anta/settings.py
user: login: carl-baillargeon
body: Agreed with removing the default value in the CLI.
created_at: 2025-01-22T02:58:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -163,6 +190,7 @@ def core_options(f: Callable[..., Any]) -> Callable[..., Any]:
         show_envvar=True,
         envvar="ANTA_TIMEOUT",
         show_default=True,
+        type=FLOAT_OR_NONE,
path: anta/cli/utils.py
user: login: carl-baillargeon
body: Same as above, agreed with removing the default value here but we do need to keep FLOAT_OR_NONE to support None (no timeout) at the CLI. We do not support None today:
```
(.venv) ~/git_projects/anta (main ✔) anta nrfu --timeout None table
Usage: anta nrfu [OPTIONS] COMMAND [ARGS]...
Try 'anta nrfu --help' for help.

Error: Invalid value for '--timeout': 'None' is not a valid float.
```
created_at: 2025-01-22T03:03:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,508 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import get_file_descriptor_limit, get_max_concurrency
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from asyncio import Task
+    from collections.abc import AsyncGenerator, Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Track inventory filtering statistics during an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup and.
+
+    When initialized, the counters are set to zero.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int = 0
+    filtered_by_tags: int = 0
+    connection_failed: int = 0
+    established: int = 0
+
+
+class AntaRunnerScope(BaseModel):
+    """Define the scope of an ANTA test run.
+
+    The scope determines which devices and tests to include in a run, and how to
+    filter them. This class is used with the AntaRunner.run() method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If None, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If None, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When True, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If None, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    max_concurrency : int
+        Maximum number of tests that can run concurrently. Defaults from `get_max_concurrency()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    file_descriptor_limit : int
+        System file descriptor limit for connections. Defaults from `get_file_descriptor_limit()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        None if unknown, float('inf') if unlimited.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with _) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerScope
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerScope(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+    max_concurrency: int = Field(default_factory=get_max_concurrency)
+    file_descriptor_limit: int = Field(default_factory=get_file_descriptor_limit)
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = None
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+    _inventory_stats: AntaRunnerInventoryStats | None = None
+    _total_tests: int = 0
+    _potential_connections: float | None = None
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, scope: AntaRunnerScope | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        scope
+            Scope of the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        scope = scope or AntaRunnerScope()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(scope, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(scope):
+                    return manager
+
+            # Set up test coroutines
+            generator = self._setup_coroutines(manager=manager if dry_run else None)
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            async for test in generator:
+                test.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            async for result in self._run(generator):
+                manager.add(result)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, scope: AntaRunnerScope, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the CLI provided tags and devices if any
+        filtered_inventory = self.inventory.get_inventory(tags=scope.tags, devices=scope.devices) if scope.tags or scope.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
+        self._selected_inventory = filtered_inventory.get_inventory(established_only=scope.established_only)
+        connection_failed = len(filtered_inventory) - len(self._selected_inventory)
+
+        # If there are no devices in the inventory after filtering, exit
+        if not self._selected_inventory.devices:
+            # Build message parts
+            tag_msg = f"matching the tags {scope.tags} " if scope.tags else ""
+            device_msg = f" Selected devices: {scope.devices} " if scope.devices is not None else ""
+            logger.warning("No reachable device %swas found.%s", tag_msg, device_msg)
+            return False
+
+        self._inventory_stats = AntaRunnerInventoryStats(
+            total=total_devices, filtered_by_tags=filtered_by_tags, connection_failed=connection_failed, established=len(self._selected_inventory)
+        )
+        return True
+
+    def _setup_tests(self, scope: AntaRunnerScope) -> bool:
+        """Set up tests for the ANTA run."""
+        if self._selected_inventory is None:
+            msg = "The selected inventory is not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        # Build indexes for the catalog. If `tests` is set, filter the indexes based on these tests
+        self.catalog.build_indexes(filtered_tests=scope.tests)
+
+        # Using a set to avoid inserting duplicate tests
+        device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
+        total_tests = 0
+        total_connections = 0
+        unlimited_connections = False
+        all_have_connections = True
+
+        # Create the device to tests mapping from the tags and calculate connection stats
+        for device in self._selected_inventory.devices:
+            if scope.tags:
+                # If there are CLI tags, execute tests with matching tags for this device
+                if not (matching_tags := scope.tags.intersection(device.tags)):
+                    # The device does not have any selected tag, skipping
+                    continue
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(matching_tags))
+            else:
+                # If there is no CLI tags, execute all tests that do not have any tags
+                device_to_tests[device].update(self.catalog.tag_to_tests[None])
+
+                # Then add the tests with matching tags from device tags
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(device.tags))
+
+            total_tests += len(device_to_tests[device])
+
+            # Check device connections
+            if not hasattr(device, "max_connections"):
+                all_have_connections = False
+            elif device.max_connections is None:
+                unlimited_connections = True
+            else:
+                total_connections += device.max_connections
+
+        if total_tests == 0:
+            tag_msg = f" matching the tags {scope.tags} " if scope.tags else " "
+            logger.warning(
+                "There are no tests%sto run in the current test catalog and device inventory, please verify your inputs.",
+                tag_msg,
+            )
+            return False
+
+        self._selected_tests = device_to_tests
+        self._total_tests = total_tests
+        self._potential_connections = None if not all_have_connections else float("inf") if unlimited_connections else total_connections
+        return True
+
+    async def _setup_coroutines(self, *, manager: ResultManager | None = None) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """Set up test coroutines from selected tests for the ANTA run.
+
+        It creates an async generator of coroutines which are created by the `test` method of the AntaTest instances.
+        Each coroutine is a test to run.
+
+        Parameters
+        ----------
+        manager
+            An optional ResultManager instance to pre-populate with the test results. Used in `dry_run` mode.
+
+        Yields
+        ------
+            The coroutine (test) to run.
+        """
+        if self._selected_tests is None:
+            msg = "The selected tests are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        for device, test_definitions in self._selected_tests.items():
+            for test in test_definitions:
+                try:
+                    test_instance = test.test(device=device, inputs=test.inputs)
+                    if manager is not None:
+                        manager.add(test_instance.result)
+                    coroutine = test_instance.test()
+                except Exception as e:  # noqa: PERF203, BLE001
+                    # An AntaTest instance is potentially user-defined code.
+                    # We need to catch everything and exit gracefully with an error message.
+                    message = "\n".join(
+                        [
+                            f"There is an error when creating test {test.test.__module__}.{test.test.__name__}.",
+                            f"If this is not a custom test implementation: {GITHUB_SUGGESTION}",
+                        ],
+                    )
+                    anta_log_exception(e, message, logger)
+                else:
+                    yield coroutine
+
+    def _log_run_information(self, *, dry_run: bool = False) -> None:
+        """Log ANTA run information and potential resource limit warnings.
+
+        Parameters
+        ----------
+        dry_run
+            Dry-run mode flag.
+        """
+        if self._inventory_stats is None:
+            msg = "The inventory stats are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        width = min(int(console.width) - 34, len("  Total potential connections: Unlimited\n"))
+
+        # Build device information
+        device_lines = [
+            "Devices:",
+            f"  Total: {self._inventory_stats.total}",
+        ]
+        if self._inventory_stats.filtered_by_tags > 0:
+            device_lines.append(f"  Excluded by tags: {self._inventory_stats.filtered_by_tags}")
+        if self._inventory_stats.connection_failed > 0:
+            device_lines.append(f"  Failed to connect: {self._inventory_stats.connection_failed}")
+        device_lines.append(f"  Selected: {self._inventory_stats.established}{' (dry-run mode)' if dry_run else ''}")
+
+        # Build connection information
+        connections_line = (
+            ""
+            if self._potential_connections is None
+            else f"  Total potential connections: {'Unlimited' if self._potential_connections == float('inf') else self._potential_connections}\n"
+        )
+
+        run_info = (
+            f"{' ANTA NRFU Run Information ':-^{width}}\n"
+            f"{chr(10).join(device_lines)}\n"
+            f"Tests: {self._total_tests} total\n"
+            f"Limits:\n"
+            f"  Max concurrent tests: {self.max_concurrency}\n"
+            f"{connections_line}"
+            f"  Max file descriptors: {self.file_descriptor_limit}\n"
+            f"{'':-^{width}}"
+        )
+        logger.info(run_info)
+
+        # Log warnings for potential resource limits
+        if self._total_tests > self.max_concurrency:
+            logger.warning(
+                "Tests count (%s) exceeds concurrent limit (%s). Tests will be throttled. See Scaling ANTA documentation.", self._total_tests, self.max_concurrency
+            )
+        if self._potential_connections == float("inf"):
+            logger.warning(
+                "Running with unlimited connections. Connection errors may occur due to file descriptor limit (%s). See Scaling ANTA documentation.",
+                self.file_descriptor_limit,
+            )
+        elif self._potential_connections is not None and self._potential_connections > self.file_descriptor_limit:
+            logger.warning(
+                "Potential connections (%s) exceeds file descriptor limit (%s). Connection errors may occur. See Scaling ANTA documentation.",
+                self._potential_connections,
+                self.file_descriptor_limit,
+            )
+
+    async def _run(self, generator: AsyncGenerator[Coroutine[Any, Any, TestResult], None]) -> AsyncGenerator[TestResult, None]:
+        """Run tests with a concurrency limit.
+
+        This function takes an asynchronous generator of test coroutines and runs them
+        with a limit on the number of concurrent tests. It yields test results as each
+        test completes.
+
+        Inspired by: https://death.andgravity.com/limit-concurrency
+
+        Parameters
+        ----------
+        generator
+            An asynchronous generator that yields test coroutines.
+
+        Yields
+        ------
+            The result of each completed test.
+        """
+        if self.max_concurrency <= 0:
+            msg = "Concurrency limit must be greater than 0."
+            raise RuntimeError(msg)
+
+        # NOTE: The `aiter` built-in function is not available in Python 3.9
+        tests = generator.__aiter__()  # pylint: disable=unnecessary-dunder-call
+        tests_ended = False
+        tests_pending: set[Task[TestResult]] = set()
+
+        while tests_pending or not tests_ended:
+            # Add tests to the pending set until the limit is reached or no more tests are available
+            while len(tests_pending) < self.max_concurrency and not tests_ended:
+                try:
+                    # NOTE: The `anext` built-in function is not available in Python 3.9
+                    test = await tests.__anext__()  # pylint: disable=unnecessary-dunder-call
+                except StopAsyncIteration:  # noqa: PERF203
+                    tests_ended = True
+                    logger.debug("All tests have been added to the pending set.")
+                else:
+                    # Ensure the coroutine is scheduled to run and add it to the pending set
+                    tests_pending.add(asyncio.create_task(test))
+                    logger.debug("Added a test to the pending set: %s", test)
+
+            if len(tests_pending) >= self.max_concurrency:
+                logger.debug("Concurrency limit reached: %s tests running. Waiting for tests to complete.", self.max_concurrency)
+
+            if not tests_pending:
+                logger.debug("No pending tests and all tests have been processed. Exiting.")
+                return
path: anta/_runner.py
user: login: carl-baillargeon
body: It looks equivalent right?
created_at: 2025-01-22T03:23:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -378,15 +388,31 @@ def __init__(  # noqa: PLR0913
             message = f"'password' is required to instantiate device '{self.name}'"
             logger.error(message)
             raise ValueError(message)
+
         self.enable = enable
         self._enable_password = enable_password
-        self._session: asynceapi.Device = asynceapi.Device(host=host, port=port, username=username, password=password, proto=proto, timeout=timeout)
-        ssh_params: dict[str, Any] = {}
+
+        # Build the session settings for the `asynceapi` client
+        session_settings: dict[str, Any] = {
+            "host": host,
+            "port": port,
+            "username": username,
+            "password": password,
+            "proto": proto,
+            "timeout": httpx_timeout if httpx_timeout is not None else timeout,
+        }
+        if httpx_limits is not None:
+            session_settings["limits"] = httpx_limits
+            self.max_connections = httpx_limits.max_connections
+        else:
+            self.max_connections = DEFAULT_HTTPX_MAX_CONNECTIONS
path: anta/device.py
user: login: carl-baillargeon
body: I believe you are suggesting to not store `max_connections` as a separate attribute, but rather create a read-only property that retrieves it directly from HTTPX? `Limits` is injected down the line to create the httpcore connection pool. We could potentially retrieve it by doing `self._session._transport._pool._max_connections` but since these are all privates it's too risky IMO.
created_at: 2025-01-22T03:39:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -354,14 +358,20 @@ def __init__(  # noqa: PLR0913
         tags
             Tags for this device.
         timeout
-            Timeout value in seconds for outgoing API calls.
-        insecure
-            Disable SSH Host Key validation.
+            Global timeout value in seconds for outgoing eAPI calls.
+        httpx_timeout
+            Optional HTTPX Timeout object for fine-grained timeout configuration.
+            If provided, it will override the global timeout.
path: anta/device.py
user: login: carl-baillargeon
body: Ah yes I didn't know HTTPX supported different types. Type would be `timeout: Timeout | float | None` to avoid breaking change. Should we also support the tuple type (below) like HTTPX?
```
TimeoutTypes = Union[
    Optional[float],
    Tuple[Optional[float], Optional[float], Optional[float], Optional[float]],
    "Timeout",
]
```
created_at: 2025-01-22T03:46:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -354,14 +358,20 @@ def __init__(  # noqa: PLR0913
         tags
             Tags for this device.
         timeout
-            Timeout value in seconds for outgoing API calls.
-        insecure
-            Disable SSH Host Key validation.
+            Global timeout value in seconds for outgoing eAPI calls.
+        httpx_timeout
+            Optional HTTPX Timeout object for fine-grained timeout configuration.
+            If provided, it will override the global timeout.
path: anta/device.py
user: login: carl-baillargeon
body: I think we should also replace `httpx_limits` with just `limits` to be consistent with `timeout` then.
created_at: 2025-01-22T03:47:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,6 +37,7 @@ def test_anta_dry_run(
 
     results = session_results[request.node.callspec.id]
 
+    # TODO: Use AntaRunner in ANTA v2.0.0
path: tests/benchmark/test_anta.py
user: login: carl-baillargeon
body: We can consider removing `main()` altogether and use `AntaRunner` everywhere in v2.0.0.
created_at: 2025-01-22T03:49:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,6 +70,7 @@ def test_anta(
 
     results = session_results[request.node.callspec.id]
 
+    # TODO: Use AntaRunner in ANTA v2.0.0
path: tests/benchmark/test_anta.py
user: login: carl-baillargeon
body: I put this TODO everywhere we are using `main()`.
created_at: 2025-01-22T03:50:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,195 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+
+from httpx import Limits, Timeout
+from pydantic import Field, NonNegativeFloat, NonNegativeInt, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default values for HTTPX resource limits
+DEFAULT_HTTPX_MAX_CONNECTIONS = 100
+DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS = 20
+DEFAULT_HTTPX_KEEPALIVE_EXPIRY = 5.0
+
+# Default values for HTTPX timeouts
+DEFAULT_HTTPX_CONNECT_TIMEOUT = 5.0
+DEFAULT_HTTPX_READ_TIMEOUT = 5.0
+DEFAULT_HTTPX_WRITE_TIMEOUT = 5.0
+DEFAULT_HTTPX_POOL_TIMEOUT = 5.0
path: anta/settings.py
user: login: carl-baillargeon
body: We could put our own default values here instead with the current 30s default we have. From our testing we have a pretty good idea what works best:
```python
# Default values for HTTPX resource limits
DEFAULT_HTTPX_MAX_CONNECTIONS = 10
DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS = 10
DEFAULT_HTTPX_KEEPALIVE_EXPIRY = 5.0

# Default values for HTTPX timeouts
DEFAULT_HTTPX_CONNECT_TIMEOUT = 30.0
DEFAULT_HTTPX_READ_TIMEOUT = 30.0
DEFAULT_HTTPX_WRITE_TIMEOUT = 30.0
DEFAULT_HTTPX_POOL_TIMEOUT = None
```
created_at: 2025-01-22T03:59:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,508 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import get_file_descriptor_limit, get_max_concurrency
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from asyncio import Task
+    from collections.abc import AsyncGenerator, Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Track inventory filtering statistics during an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup and.
+
+    When initialized, the counters are set to zero.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int = 0
+    filtered_by_tags: int = 0
+    connection_failed: int = 0
+    established: int = 0
+
+
+class AntaRunnerScope(BaseModel):
+    """Define the scope of an ANTA test run.
+
+    The scope determines which devices and tests to include in a run, and how to
+    filter them. This class is used with the AntaRunner.run() method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If None, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If None, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When True, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If None, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    max_concurrency : int
+        Maximum number of tests that can run concurrently. Defaults from `get_max_concurrency()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    file_descriptor_limit : int
+        System file descriptor limit for connections. Defaults from `get_file_descriptor_limit()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        None if unknown, float('inf') if unlimited.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with _) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerScope
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerScope(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+    max_concurrency: int = Field(default_factory=get_max_concurrency)
+    file_descriptor_limit: int = Field(default_factory=get_file_descriptor_limit)
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = None
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+    _inventory_stats: AntaRunnerInventoryStats | None = None
+    _total_tests: int = 0
+    _potential_connections: float | None = None
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, scope: AntaRunnerScope | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        scope
+            Scope of the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        scope = scope or AntaRunnerScope()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(scope, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(scope):
+                    return manager
+
+            # Set up test coroutines
+            generator = self._setup_coroutines(manager=manager if dry_run else None)
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            async for test in generator:
+                test.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            async for result in self._run(generator):
+                manager.add(result)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, scope: AntaRunnerScope, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the CLI provided tags and devices if any
+        filtered_inventory = self.inventory.get_inventory(tags=scope.tags, devices=scope.devices) if scope.tags or scope.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
+        self._selected_inventory = filtered_inventory.get_inventory(established_only=scope.established_only)
+        connection_failed = len(filtered_inventory) - len(self._selected_inventory)
+
+        # If there are no devices in the inventory after filtering, exit
+        if not self._selected_inventory.devices:
+            # Build message parts
+            tag_msg = f"matching the tags {scope.tags} " if scope.tags else ""
+            device_msg = f" Selected devices: {scope.devices} " if scope.devices is not None else ""
+            logger.warning("No reachable device %swas found.%s", tag_msg, device_msg)
+            return False
+
+        self._inventory_stats = AntaRunnerInventoryStats(
+            total=total_devices, filtered_by_tags=filtered_by_tags, connection_failed=connection_failed, established=len(self._selected_inventory)
+        )
+        return True
+
+    def _setup_tests(self, scope: AntaRunnerScope) -> bool:
+        """Set up tests for the ANTA run."""
+        if self._selected_inventory is None:
+            msg = "The selected inventory is not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        # Build indexes for the catalog. If `tests` is set, filter the indexes based on these tests
+        self.catalog.build_indexes(filtered_tests=scope.tests)
+
+        # Using a set to avoid inserting duplicate tests
+        device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
+        total_tests = 0
+        total_connections = 0
+        unlimited_connections = False
+        all_have_connections = True
+
+        # Create the device to tests mapping from the tags and calculate connection stats
+        for device in self._selected_inventory.devices:
+            if scope.tags:
+                # If there are CLI tags, execute tests with matching tags for this device
+                if not (matching_tags := scope.tags.intersection(device.tags)):
+                    # The device does not have any selected tag, skipping
+                    continue
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(matching_tags))
+            else:
+                # If there is no CLI tags, execute all tests that do not have any tags
+                device_to_tests[device].update(self.catalog.tag_to_tests[None])
+
+                # Then add the tests with matching tags from device tags
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(device.tags))
+
+            total_tests += len(device_to_tests[device])
+
+            # Check device connections
+            if not hasattr(device, "max_connections"):
+                all_have_connections = False
+            elif device.max_connections is None:
+                unlimited_connections = True
+            else:
+                total_connections += device.max_connections
+
+        if total_tests == 0:
+            tag_msg = f" matching the tags {scope.tags} " if scope.tags else " "
+            logger.warning(
+                "There are no tests%sto run in the current test catalog and device inventory, please verify your inputs.",
+                tag_msg,
+            )
+            return False
+
+        self._selected_tests = device_to_tests
+        self._total_tests = total_tests
+        self._potential_connections = None if not all_have_connections else float("inf") if unlimited_connections else total_connections
+        return True
+
+    async def _setup_coroutines(self, *, manager: ResultManager | None = None) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """Set up test coroutines from selected tests for the ANTA run.
+
+        It creates an async generator of coroutines which are created by the `test` method of the AntaTest instances.
+        Each coroutine is a test to run.
+
+        Parameters
+        ----------
+        manager
+            An optional ResultManager instance to pre-populate with the test results. Used in `dry_run` mode.
+
+        Yields
+        ------
+            The coroutine (test) to run.
+        """
+        if self._selected_tests is None:
+            msg = "The selected tests are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        for device, test_definitions in self._selected_tests.items():
+            for test in test_definitions:
+                try:
+                    test_instance = test.test(device=device, inputs=test.inputs)
+                    if manager is not None:
+                        manager.add(test_instance.result)
+                    coroutine = test_instance.test()
+                except Exception as e:  # noqa: PERF203, BLE001
+                    # An AntaTest instance is potentially user-defined code.
+                    # We need to catch everything and exit gracefully with an error message.
+                    message = "\n".join(
+                        [
+                            f"There is an error when creating test {test.test.__module__}.{test.test.__name__}.",
+                            f"If this is not a custom test implementation: {GITHUB_SUGGESTION}",
+                        ],
+                    )
+                    anta_log_exception(e, message, logger)
+                else:
+                    yield coroutine
+
+    def _log_run_information(self, *, dry_run: bool = False) -> None:
+        """Log ANTA run information and potential resource limit warnings.
+
+        Parameters
+        ----------
+        dry_run
+            Dry-run mode flag.
+        """
+        if self._inventory_stats is None:
+            msg = "The inventory stats are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        width = min(int(console.width) - 34, len("  Total potential connections: Unlimited\n"))
+
+        # Build device information
+        device_lines = [
+            "Devices:",
+            f"  Total: {self._inventory_stats.total}",
+        ]
+        if self._inventory_stats.filtered_by_tags > 0:
+            device_lines.append(f"  Excluded by tags: {self._inventory_stats.filtered_by_tags}")
+        if self._inventory_stats.connection_failed > 0:
+            device_lines.append(f"  Failed to connect: {self._inventory_stats.connection_failed}")
+        device_lines.append(f"  Selected: {self._inventory_stats.established}{' (dry-run mode)' if dry_run else ''}")
+
+        # Build connection information
+        connections_line = (
+            ""
+            if self._potential_connections is None
+            else f"  Total potential connections: {'Unlimited' if self._potential_connections == float('inf') else self._potential_connections}\n"
+        )
+
+        run_info = (
+            f"{' ANTA NRFU Run Information ':-^{width}}\n"
+            f"{chr(10).join(device_lines)}\n"
+            f"Tests: {self._total_tests} total\n"
+            f"Limits:\n"
+            f"  Max concurrent tests: {self.max_concurrency}\n"
+            f"{connections_line}"
+            f"  Max file descriptors: {self.file_descriptor_limit}\n"
+            f"{'':-^{width}}"
+        )
+        logger.info(run_info)
+
+        # Log warnings for potential resource limits
+        if self._total_tests > self.max_concurrency:
+            logger.warning(
+                "Tests count (%s) exceeds concurrent limit (%s). Tests will be throttled. See Scaling ANTA documentation.", self._total_tests, self.max_concurrency
+            )
+        if self._potential_connections == float("inf"):
+            logger.warning(
+                "Running with unlimited connections. Connection errors may occur due to file descriptor limit (%s). See Scaling ANTA documentation.",
+                self.file_descriptor_limit,
+            )
+        elif self._potential_connections is not None and self._potential_connections > self.file_descriptor_limit:
+            logger.warning(
+                "Potential connections (%s) exceeds file descriptor limit (%s). Connection errors may occur. See Scaling ANTA documentation.",
+                self._potential_connections,
+                self.file_descriptor_limit,
+            )
+
+    async def _run(self, generator: AsyncGenerator[Coroutine[Any, Any, TestResult], None]) -> AsyncGenerator[TestResult, None]:
+        """Run tests with a concurrency limit.
+
+        This function takes an asynchronous generator of test coroutines and runs them
+        with a limit on the number of concurrent tests. It yields test results as each
+        test completes.
+
+        Inspired by: https://death.andgravity.com/limit-concurrency
+
+        Parameters
+        ----------
+        generator
+            An asynchronous generator that yields test coroutines.
+
+        Yields
+        ------
+            The result of each completed test.
+        """
+        if self.max_concurrency <= 0:
+            msg = "Concurrency limit must be greater than 0."
+            raise RuntimeError(msg)
+
+        # NOTE: The `aiter` built-in function is not available in Python 3.9
+        tests = generator.__aiter__()  # pylint: disable=unnecessary-dunder-call
+        tests_ended = False
+        tests_pending: set[Task[TestResult]] = set()
+
+        while tests_pending or not tests_ended:
+            # Add tests to the pending set until the limit is reached or no more tests are available
+            while len(tests_pending) < self.max_concurrency and not tests_ended:
+                try:
+                    # NOTE: The `anext` built-in function is not available in Python 3.9
+                    test = await tests.__anext__()  # pylint: disable=unnecessary-dunder-call
+                except StopAsyncIteration:  # noqa: PERF203
+                    tests_ended = True
+                    logger.debug("All tests have been added to the pending set.")
+                else:
+                    # Ensure the coroutine is scheduled to run and add it to the pending set
+                    tests_pending.add(asyncio.create_task(test))
+                    logger.debug("Added a test to the pending set: %s", test)
+
+            if len(tests_pending) >= self.max_concurrency:
+                logger.debug("Concurrency limit reached: %s tests running. Waiting for tests to complete.", self.max_concurrency)
+
+            if not tests_pending:
+                logger.debug("No pending tests and all tests have been processed. Exiting.")
+                return
path: anta/_runner.py
user: login: mtache
body: It is :)
created_at: 2025-01-29T14:32:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,195 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+
+from httpx import Limits, Timeout
+from pydantic import Field, NonNegativeFloat, NonNegativeInt, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default values for HTTPX resource limits
+DEFAULT_HTTPX_MAX_CONNECTIONS = 100
+DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS = 20
+DEFAULT_HTTPX_KEEPALIVE_EXPIRY = 5.0
+
+# Default values for HTTPX timeouts
+DEFAULT_HTTPX_CONNECT_TIMEOUT = 5.0
+DEFAULT_HTTPX_READ_TIMEOUT = 5.0
+DEFAULT_HTTPX_WRITE_TIMEOUT = 5.0
+DEFAULT_HTTPX_POOL_TIMEOUT = 5.0
path: anta/settings.py
user: login: carl-baillargeon
body: I decided to put our own default values since they were tested. d8ca4acfe433b8a73a82a53cca7f2cf51cd177ad
created_at: 2025-02-07T19:57:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -378,15 +388,31 @@ def __init__(  # noqa: PLR0913
             message = f"'password' is required to instantiate device '{self.name}'"
             logger.error(message)
             raise ValueError(message)
+
         self.enable = enable
         self._enable_password = enable_password
-        self._session: asynceapi.Device = asynceapi.Device(host=host, port=port, username=username, password=password, proto=proto, timeout=timeout)
-        ssh_params: dict[str, Any] = {}
+
+        # Build the session settings for the `asynceapi` client
+        session_settings: dict[str, Any] = {
+            "host": host,
+            "port": port,
+            "username": username,
+            "password": password,
+            "proto": proto,
+            "timeout": httpx_timeout if httpx_timeout is not None else timeout,
+        }
+        if httpx_limits is not None:
+            session_settings["limits"] = httpx_limits
+            self.max_connections = httpx_limits.max_connections
+        else:
+            self.max_connections = DEFAULT_HTTPX_MAX_CONNECTIONS
+        self._session: asynceapi.Device = asynceapi.Device(**session_settings)
+
+        # Build the SSH connection options
+        ssh_settings = {"host": host, "port": ssh_port, "username": username, "password": password, "client_keys": CLIENT_KEYS}
         if insecure:
-            ssh_params["known_hosts"] = None
-        self._ssh_opts: SSHClientConnectionOptions = SSHClientConnectionOptions(
-            host=host, port=ssh_port, username=username, password=password, client_keys=CLIENT_KEYS, **ssh_params
-        )
+            ssh_settings["known_hosts"] = None
+        self._ssh_opts: SSHClientConnectionOptions = SSHClientConnectionOptions(**ssh_settings)
 
     def __rich_repr__(self) -> Iterator[tuple[str, Any]]:
path: anta/device.py
user: login: carl-baillargeon
body: `timeout` is already in `_session`. Added `max_connections` in d8ca4acfe433b8a73a82a53cca7f2cf51cd177ad.
created_at: 2025-02-07T19:58:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -354,14 +358,20 @@ def __init__(  # noqa: PLR0913
         tags
             Tags for this device.
         timeout
-            Timeout value in seconds for outgoing API calls.
-        insecure
-            Disable SSH Host Key validation.
+            Global timeout value in seconds for outgoing eAPI calls.
+        httpx_timeout
+            Optional HTTPX Timeout object for fine-grained timeout configuration.
+            If provided, it will override the global timeout.
path: anta/device.py
user: login: carl-baillargeon
body: Fixed in d8ca4acfe433b8a73a82a53cca7f2cf51cd177ad.
created_at: 2025-02-07T19:58:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -378,15 +388,31 @@ def __init__(  # noqa: PLR0913
             message = f"'password' is required to instantiate device '{self.name}'"
             logger.error(message)
             raise ValueError(message)
+
         self.enable = enable
         self._enable_password = enable_password
-        self._session: asynceapi.Device = asynceapi.Device(host=host, port=port, username=username, password=password, proto=proto, timeout=timeout)
-        ssh_params: dict[str, Any] = {}
+
+        # Build the session settings for the `asynceapi` client
+        session_settings: dict[str, Any] = {
+            "host": host,
+            "port": port,
+            "username": username,
+            "password": password,
+            "proto": proto,
+            "timeout": httpx_timeout if httpx_timeout is not None else timeout,
+        }
+        if httpx_limits is not None:
+            session_settings["limits"] = httpx_limits
+            self.max_connections = httpx_limits.max_connections
+        else:
+            self.max_connections = DEFAULT_HTTPX_MAX_CONNECTIONS
path: anta/device.py
user: login: carl-baillargeon
body: Added in d8ca4acfe433b8a73a82a53cca7f2cf51cd177ad around a try-expect block if not available.
created_at: 2025-02-07T19:59:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -163,6 +190,7 @@ def core_options(f: Callable[..., Any]) -> Callable[..., Any]:
         show_envvar=True,
         envvar="ANTA_TIMEOUT",
         show_default=True,
+        type=FLOAT_OR_NONE,
path: anta/cli/utils.py
user: login: carl-baillargeon
body: Added `float` type to support `inf` for no timeout in d8ca4acfe433b8a73a82a53cca7f2cf51cd177ad.
created_at: 2025-02-07T19:59:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,195 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+
+from httpx import Limits, Timeout
+from pydantic import Field, NonNegativeFloat, NonNegativeInt, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default values for HTTPX resource limits
+DEFAULT_HTTPX_MAX_CONNECTIONS = 100
+DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS = 20
+DEFAULT_HTTPX_KEEPALIVE_EXPIRY = 5.0
+
+# Default values for HTTPX timeouts
+DEFAULT_HTTPX_CONNECT_TIMEOUT = 5.0
+DEFAULT_HTTPX_READ_TIMEOUT = 5.0
+DEFAULT_HTTPX_WRITE_TIMEOUT = 5.0
+DEFAULT_HTTPX_POOL_TIMEOUT = 5.0
+
+# Default value for the maximum number of concurrent tests in the event loop
+DEFAULT_MAX_CONCURRENCY = 10000
+
+# Default value for the maximum number of open file descriptors for the ANTA process
+DEFAULT_NOFILE = 16384
+
+
+class FileDescriptorSettings(BaseSettings):
+    """Environment variable for configuring the maximum number of open file descriptors for the ANTA process.
+
+    On POSIX systems, this value is used to set the soft limit for the current process.
+    The value cannot exceed the system's hard limit.
+    """
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    nofile: PositiveInt = Field(default=DEFAULT_NOFILE)
+
+
+class MaxConcurrencySettings(BaseSettings):
+    """Environment variable for configuring the maximum number of concurrent tests in the event loop."""
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    max_concurrency: PositiveInt = Field(default=DEFAULT_MAX_CONCURRENCY)
+
+
+class HttpxResourceLimitsSettings(BaseSettings):
+    """Environment variables for configuring the underlying HTTPX client resource limits.
+
+    The limits are set using the following environment variables:
+        - ANTA_MAX_CONNECTIONS: Maximum number of allowable connections.
+        - ANTA_MAX_KEEPALIVE_CONNECTIONS: Number of allowable keep-alive connections.
+        - ANTA_KEEPALIVE_EXPIRY: Time limit on idle keep-alive connections in seconds.
+
+    If any environment variable is not set, the following HTTPX default limits are used:
+        - max_connections: 100
+        - max_keepalive_connections: 20
+        - keepalive_expiry: 5.0
+
+    These limits are set for all devices. `None` means no limit is set for the given operation.
+
+    For more information on HTTPX resource limits, see: https://www.python-httpx.org/advanced/resource-limits/
+    """
+
+    # The 'None' string is used to allow the environment variable to be set to `None`.
+    model_config = SettingsConfigDict(env_parse_none_str="None", env_prefix="ANTA_")
+
+    max_connections: NonNegativeInt | None = Field(default=DEFAULT_HTTPX_MAX_CONNECTIONS)
+    max_keepalive_connections: NonNegativeInt | None = Field(default=DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS)
+    keepalive_expiry: NonNegativeFloat | None = Field(default=DEFAULT_HTTPX_KEEPALIVE_EXPIRY)
+
+
+class HttpxTimeoutsSettings(BaseSettings):
+    """Environment variables for configuring the underlying HTTPX client timeouts.
+
+    The timeouts are set using the following environment variables:
+        - ANTA_CONNECT_TIMEOUT: Maximum amount of time to wait until a socket connection to the requested host is established.
+        - ANTA_READ_TIMEOUT: Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body).
+        - ANTA_WRITE_TIMEOUT: Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body).
+        - ANTA_POOL_TIMEOUT: Maximum duration to wait for acquiring a connection from the connection pool.
+
+    If any environment variable is not set, the default HTTPX timeout is used, 5 seconds.
+    `None` will disable the timeout for the given operation.
+
+    For more information on HTTPX timeouts, see: https://www.python-httpx.org/advanced/timeouts/
+    """
+
+    # The 'None' string is used to allow the environment variable to be set to `None`.
+    model_config = SettingsConfigDict(env_parse_none_str="None", env_prefix="ANTA_")
+
+    connect_timeout: NonNegativeFloat | None = Field(default=DEFAULT_HTTPX_CONNECT_TIMEOUT)
+    read_timeout: NonNegativeFloat | None = Field(default=DEFAULT_HTTPX_READ_TIMEOUT)
+    write_timeout: NonNegativeFloat | None = Field(default=DEFAULT_HTTPX_WRITE_TIMEOUT)
+    pool_timeout: NonNegativeFloat | None = Field(default=DEFAULT_HTTPX_POOL_TIMEOUT)
+
+    # The following properties are used to determine if a specific timeout was set by an environment variable
+    @property
+    def connect_set(self) -> bool:
+        """Return True if the connect timeout was set by an environment variable."""
+        return "connect_timeout" in self.model_fields_set
+
+    @property
+    def read_set(self) -> bool:
+        """Return True if the read timeout was set by an environment variable."""
+        return "read_timeout" in self.model_fields_set
+
+    @property
+    def write_set(self) -> bool:
+        """Return True if the write timeout was set by an environment variable."""
+        return "write_timeout" in self.model_fields_set
+
+    @property
+    def pool_set(self) -> bool:
+        """Return True if the pool timeout was set by an environment variable."""
+        return "pool_timeout" in self.model_fields_set
+
+
+def get_max_concurrency() -> int:
+    """Get the maximum number of concurrent tests that can run in the event loop."""
+    settings = MaxConcurrencySettings()
+    return settings.max_concurrency
+
+
+def get_httpx_limits() -> Limits:
+    """Get the HTTPX Limits object from environment variables."""
+    settings = HttpxResourceLimitsSettings()
+    return Limits(
+        max_connections=settings.max_connections,
+        max_keepalive_connections=settings.max_keepalive_connections,
+        keepalive_expiry=settings.keepalive_expiry,
+    )
+
+
+def get_httpx_timeout(default_timeout: float | None) -> Timeout:
+    """Get the HTTPX Timeout object from environment variables.
+
+    Parameters
+    ----------
+    default_timeout : float | None
path: anta/settings.py
user: login: carl-baillargeon
body: Fixed in d8ca4acfe433b8a73a82a53cca7f2cf51cd177ad.
created_at: 2025-02-07T20:00:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,508 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import get_file_descriptor_limit, get_max_concurrency
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from asyncio import Task
+    from collections.abc import AsyncGenerator, Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Track inventory filtering statistics during an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup and.
+
+    When initialized, the counters are set to zero.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int = 0
+    filtered_by_tags: int = 0
+    connection_failed: int = 0
+    established: int = 0
+
+
+class AntaRunnerScope(BaseModel):
+    """Define the scope of an ANTA test run.
+
+    The scope determines which devices and tests to include in a run, and how to
+    filter them. This class is used with the AntaRunner.run() method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If None, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If None, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When True, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If None, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    max_concurrency : int
+        Maximum number of tests that can run concurrently. Defaults from `get_max_concurrency()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    file_descriptor_limit : int
+        System file descriptor limit for connections. Defaults from `get_file_descriptor_limit()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        None if unknown, float('inf') if unlimited.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with _) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerScope
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerScope(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+    max_concurrency: int = Field(default_factory=get_max_concurrency)
+    file_descriptor_limit: int = Field(default_factory=get_file_descriptor_limit)
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = None
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+    _inventory_stats: AntaRunnerInventoryStats | None = None
+    _total_tests: int = 0
+    _potential_connections: float | None = None
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, scope: AntaRunnerScope | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        scope
+            Scope of the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        scope = scope or AntaRunnerScope()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(scope, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(scope):
+                    return manager
+
+            # Set up test coroutines
+            generator = self._setup_coroutines(manager=manager if dry_run else None)
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            async for test in generator:
+                test.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            async for result in self._run(generator):
+                manager.add(result)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, scope: AntaRunnerScope, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the CLI provided tags and devices if any
+        filtered_inventory = self.inventory.get_inventory(tags=scope.tags, devices=scope.devices) if scope.tags or scope.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
+        self._selected_inventory = filtered_inventory.get_inventory(established_only=scope.established_only)
+        connection_failed = len(filtered_inventory) - len(self._selected_inventory)
+
+        # If there are no devices in the inventory after filtering, exit
+        if not self._selected_inventory.devices:
+            # Build message parts
+            tag_msg = f"matching the tags {scope.tags} " if scope.tags else ""
+            device_msg = f" Selected devices: {scope.devices} " if scope.devices is not None else ""
+            logger.warning("No reachable device %swas found.%s", tag_msg, device_msg)
+            return False
+
+        self._inventory_stats = AntaRunnerInventoryStats(
+            total=total_devices, filtered_by_tags=filtered_by_tags, connection_failed=connection_failed, established=len(self._selected_inventory)
+        )
+        return True
+
+    def _setup_tests(self, scope: AntaRunnerScope) -> bool:
+        """Set up tests for the ANTA run."""
+        if self._selected_inventory is None:
+            msg = "The selected inventory is not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        # Build indexes for the catalog. If `tests` is set, filter the indexes based on these tests
+        self.catalog.build_indexes(filtered_tests=scope.tests)
+
+        # Using a set to avoid inserting duplicate tests
+        device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
+        total_tests = 0
+        total_connections = 0
+        unlimited_connections = False
+        all_have_connections = True
+
+        # Create the device to tests mapping from the tags and calculate connection stats
+        for device in self._selected_inventory.devices:
+            if scope.tags:
+                # If there are CLI tags, execute tests with matching tags for this device
+                if not (matching_tags := scope.tags.intersection(device.tags)):
+                    # The device does not have any selected tag, skipping
+                    continue
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(matching_tags))
+            else:
+                # If there is no CLI tags, execute all tests that do not have any tags
+                device_to_tests[device].update(self.catalog.tag_to_tests[None])
+
+                # Then add the tests with matching tags from device tags
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(device.tags))
+
+            total_tests += len(device_to_tests[device])
+
+            # Check device connections
+            if not hasattr(device, "max_connections"):
+                all_have_connections = False
+            elif device.max_connections is None:
+                unlimited_connections = True
+            else:
+                total_connections += device.max_connections
+
+        if total_tests == 0:
+            tag_msg = f" matching the tags {scope.tags} " if scope.tags else " "
+            logger.warning(
+                "There are no tests%sto run in the current test catalog and device inventory, please verify your inputs.",
+                tag_msg,
+            )
+            return False
+
+        self._selected_tests = device_to_tests
+        self._total_tests = total_tests
+        self._potential_connections = None if not all_have_connections else float("inf") if unlimited_connections else total_connections
+        return True
+
+    async def _setup_coroutines(self, *, manager: ResultManager | None = None) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """Set up test coroutines from selected tests for the ANTA run.
+
+        It creates an async generator of coroutines which are created by the `test` method of the AntaTest instances.
+        Each coroutine is a test to run.
+
+        Parameters
+        ----------
+        manager
+            An optional ResultManager instance to pre-populate with the test results. Used in `dry_run` mode.
+
+        Yields
+        ------
+            The coroutine (test) to run.
+        """
+        if self._selected_tests is None:
+            msg = "The selected tests are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        for device, test_definitions in self._selected_tests.items():
+            for test in test_definitions:
+                try:
+                    test_instance = test.test(device=device, inputs=test.inputs)
+                    if manager is not None:
+                        manager.add(test_instance.result)
+                    coroutine = test_instance.test()
+                except Exception as e:  # noqa: PERF203, BLE001
+                    # An AntaTest instance is potentially user-defined code.
+                    # We need to catch everything and exit gracefully with an error message.
+                    message = "\n".join(
+                        [
+                            f"There is an error when creating test {test.test.__module__}.{test.test.__name__}.",
+                            f"If this is not a custom test implementation: {GITHUB_SUGGESTION}",
+                        ],
+                    )
+                    anta_log_exception(e, message, logger)
+                else:
+                    yield coroutine
+
+    def _log_run_information(self, *, dry_run: bool = False) -> None:
+        """Log ANTA run information and potential resource limit warnings.
+
+        Parameters
+        ----------
+        dry_run
+            Dry-run mode flag.
+        """
+        if self._inventory_stats is None:
+            msg = "The inventory stats are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        width = min(int(console.width) - 34, len("  Total potential connections: Unlimited\n"))
+
+        # Build device information
+        device_lines = [
+            "Devices:",
+            f"  Total: {self._inventory_stats.total}",
+        ]
+        if self._inventory_stats.filtered_by_tags > 0:
+            device_lines.append(f"  Excluded by tags: {self._inventory_stats.filtered_by_tags}")
+        if self._inventory_stats.connection_failed > 0:
+            device_lines.append(f"  Failed to connect: {self._inventory_stats.connection_failed}")
+        device_lines.append(f"  Selected: {self._inventory_stats.established}{' (dry-run mode)' if dry_run else ''}")
+
+        # Build connection information
+        connections_line = (
+            ""
+            if self._potential_connections is None
+            else f"  Total potential connections: {'Unlimited' if self._potential_connections == float('inf') else self._potential_connections}\n"
+        )
+
+        run_info = (
+            f"{' ANTA NRFU Run Information ':-^{width}}\n"
+            f"{chr(10).join(device_lines)}\n"
+            f"Tests: {self._total_tests} total\n"
+            f"Limits:\n"
+            f"  Max concurrent tests: {self.max_concurrency}\n"
+            f"{connections_line}"
+            f"  Max file descriptors: {self.file_descriptor_limit}\n"
+            f"{'':-^{width}}"
+        )
+        logger.info(run_info)
+
+        # Log warnings for potential resource limits
+        if self._total_tests > self.max_concurrency:
+            logger.warning(
+                "Tests count (%s) exceeds concurrent limit (%s). Tests will be throttled. See Scaling ANTA documentation.", self._total_tests, self.max_concurrency
+            )
+        if self._potential_connections == float("inf"):
+            logger.warning(
+                "Running with unlimited connections. Connection errors may occur due to file descriptor limit (%s). See Scaling ANTA documentation.",
+                self.file_descriptor_limit,
+            )
+        elif self._potential_connections is not None and self._potential_connections > self.file_descriptor_limit:
+            logger.warning(
+                "Potential connections (%s) exceeds file descriptor limit (%s). Connection errors may occur. See Scaling ANTA documentation.",
+                self._potential_connections,
+                self.file_descriptor_limit,
+            )
+
+    async def _run(self, generator: AsyncGenerator[Coroutine[Any, Any, TestResult], None]) -> AsyncGenerator[TestResult, None]:
+        """Run tests with a concurrency limit.
+
+        This function takes an asynchronous generator of test coroutines and runs them
+        with a limit on the number of concurrent tests. It yields test results as each
+        test completes.
+
+        Inspired by: https://death.andgravity.com/limit-concurrency
+
+        Parameters
+        ----------
+        generator
+            An asynchronous generator that yields test coroutines.
+
+        Yields
+        ------
+            The result of each completed test.
+        """
+        if self.max_concurrency <= 0:
+            msg = "Concurrency limit must be greater than 0."
+            raise RuntimeError(msg)
+
+        # NOTE: The `aiter` built-in function is not available in Python 3.9
+        tests = generator.__aiter__()  # pylint: disable=unnecessary-dunder-call
+        tests_ended = False
+        tests_pending: set[Task[TestResult]] = set()
+
+        while tests_pending or not tests_ended:
+            # Add tests to the pending set until the limit is reached or no more tests are available
+            while len(tests_pending) < self.max_concurrency and not tests_ended:
+                try:
+                    # NOTE: The `anext` built-in function is not available in Python 3.9
+                    test = await tests.__anext__()  # pylint: disable=unnecessary-dunder-call
+                except StopAsyncIteration:  # noqa: PERF203
+                    tests_ended = True
+                    logger.debug("All tests have been added to the pending set.")
+                else:
+                    # Ensure the coroutine is scheduled to run and add it to the pending set
+                    tests_pending.add(asyncio.create_task(test))
+                    logger.debug("Added a test to the pending set: %s", test)
+
+            if len(tests_pending) >= self.max_concurrency:
+                logger.debug("Concurrency limit reached: %s tests running. Waiting for tests to complete.", self.max_concurrency)
+
+            if not tests_pending:
+                logger.debug("No pending tests and all tests have been processed. Exiting.")
+                return
path: anta/_runner.py
user: login: carl-baillargeon
body: I didn't troubleshoot much but coroutines are not awaited properly with this code:
```
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
/home/cbaillar/git_projects/anta/anta/_runner.py:449: RuntimeWarning: coroutine 'VerifySSHStatus.test' was never awaited
  async for test in generator:
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
/home/cbaillar/git_projects/anta/anta/_runner.py:449: RuntimeWarning: coroutine 'VerifyRoutingProtocolModel.test' was never awaited
  async for test in generator:
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
/home/cbaillar/git_projects/anta/anta/_runner.py:449: RuntimeWarning: coroutine 'VerifyAcctConsoleMethods.test' was never awaited
  async for test in generator:
  ```
created_at: 2025-02-07T20:13:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,195 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+
+from httpx import Limits, Timeout
+from pydantic import Field, NonNegativeFloat, NonNegativeInt, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default values for HTTPX resource limits
+DEFAULT_HTTPX_MAX_CONNECTIONS = 100
+DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS = 20
+DEFAULT_HTTPX_KEEPALIVE_EXPIRY = 5.0
+
+# Default values for HTTPX timeouts
+DEFAULT_HTTPX_CONNECT_TIMEOUT = 5.0
+DEFAULT_HTTPX_READ_TIMEOUT = 5.0
+DEFAULT_HTTPX_WRITE_TIMEOUT = 5.0
+DEFAULT_HTTPX_POOL_TIMEOUT = 5.0
+
+# Default value for the maximum number of concurrent tests in the event loop
+DEFAULT_MAX_CONCURRENCY = 10000
+
+# Default value for the maximum number of open file descriptors for the ANTA process
+DEFAULT_NOFILE = 16384
+
+
+class FileDescriptorSettings(BaseSettings):
+    """Environment variable for configuring the maximum number of open file descriptors for the ANTA process.
+
+    On POSIX systems, this value is used to set the soft limit for the current process.
+    The value cannot exceed the system's hard limit.
+    """
path: anta/settings.py
user: login: carl-baillargeon
body: Fixed in dee3bc9c479496fb5f9dac84d9e1e751b5dede0c
created_at: 2025-02-10T16:05:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,9 +181,10 @@ nav:
     - Debug commands: cli/debug.md
     - Tag Management: cli/tag-management.md
   - Advanced Usages:
-    - Caching in ANTA: advanced_usages/caching.md
     - Developing ANTA tests: advanced_usages/custom-tests.md
     - ANTA as a Python Library: advanced_usages/as-python-lib.md
+    - Caching in ANTA: advanced_usages/caching.md
+    - Scaling ANTA: advanced_usages/scaling.md
path: mkdocs.yml
user: login: carl-baillargeon
body: I added it to the API doc in dee3bc9c479496fb5f9dac84d9e1e751b5dede0c.
created_at: 2025-02-10T16:05:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,195 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+
+from httpx import Limits, Timeout
+from pydantic import Field, NonNegativeFloat, NonNegativeInt, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default values for HTTPX resource limits
+DEFAULT_HTTPX_MAX_CONNECTIONS = 100
+DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS = 20
+DEFAULT_HTTPX_KEEPALIVE_EXPIRY = 5.0
+
+# Default values for HTTPX timeouts
+DEFAULT_HTTPX_CONNECT_TIMEOUT = 5.0
+DEFAULT_HTTPX_READ_TIMEOUT = 5.0
+DEFAULT_HTTPX_WRITE_TIMEOUT = 5.0
+DEFAULT_HTTPX_POOL_TIMEOUT = 5.0
+
+# Default value for the maximum number of concurrent tests in the event loop
+DEFAULT_MAX_CONCURRENCY = 10000
+
+# Default value for the maximum number of open file descriptors for the ANTA process
+DEFAULT_NOFILE = 16384
+
+
+class FileDescriptorSettings(BaseSettings):
+    """Environment variable for configuring the maximum number of open file descriptors for the ANTA process.
+
+    On POSIX systems, this value is used to set the soft limit for the current process.
+    The value cannot exceed the system's hard limit.
+    """
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    nofile: PositiveInt = Field(default=DEFAULT_NOFILE)
+
+
+class MaxConcurrencySettings(BaseSettings):
+    """Environment variable for configuring the maximum number of concurrent tests in the event loop."""
path: anta/settings.py
user: login: carl-baillargeon
body: Fixed in dee3bc9c479496fb5f9dac84d9e1e751b5dede0c.
created_at: 2025-02-10T16:22:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,195 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+
+from httpx import Limits, Timeout
+from pydantic import Field, NonNegativeFloat, NonNegativeInt, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default values for HTTPX resource limits
+DEFAULT_HTTPX_MAX_CONNECTIONS = 100
+DEFAULT_HTTPX_MAX_KEEPALIVE_CONNECTIONS = 20
+DEFAULT_HTTPX_KEEPALIVE_EXPIRY = 5.0
+
+# Default values for HTTPX timeouts
+DEFAULT_HTTPX_CONNECT_TIMEOUT = 5.0
+DEFAULT_HTTPX_READ_TIMEOUT = 5.0
+DEFAULT_HTTPX_WRITE_TIMEOUT = 5.0
+DEFAULT_HTTPX_POOL_TIMEOUT = 5.0
+
+# Default value for the maximum number of concurrent tests in the event loop
+DEFAULT_MAX_CONCURRENCY = 10000
+
+# Default value for the maximum number of open file descriptors for the ANTA process
+DEFAULT_NOFILE = 16384
+
+
+class FileDescriptorSettings(BaseSettings):
+    """Environment variable for configuring the maximum number of open file descriptors for the ANTA process.
+
+    On POSIX systems, this value is used to set the soft limit for the current process.
+    The value cannot exceed the system's hard limit.
+    """
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    nofile: PositiveInt = Field(default=DEFAULT_NOFILE)
+
+
+class MaxConcurrencySettings(BaseSettings):
+    """Environment variable for configuring the maximum number of concurrent tests in the event loop."""
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    max_concurrency: PositiveInt = Field(default=DEFAULT_MAX_CONCURRENCY)
+
+
+class HttpxResourceLimitsSettings(BaseSettings):
+    """Environment variables for configuring the underlying HTTPX client resource limits.
+
+    The limits are set using the following environment variables:
path: anta/settings.py
user: login: carl-baillargeon
body: Fixed in dee3bc9c479496fb5f9dac84d9e1e751b5dede0c.
created_at: 2025-02-10T16:22:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
    This guide assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you are new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
```
created_at: 2025-02-10T16:40:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: still true (last part with strategie**s** plural)
created_at: 2025-02-10T16:41:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
## Resource Management

This section describes the environment variables used by ANTA to control resource usage.

!!! tip "Default Values"
    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
```
created_at: 2025-02-10T16:44:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: maybe lets add something about not putting it too high because of prevous paragraph
created_at: 2025-02-10T16:45:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system hard limit is often higher. If the system hard limit is lower than the number of file descriptors ANTA needs, a warning is emitted by ANTA at startup. The following environment variable controls this setting:
```
created_at: 2025-02-10T16:47:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: hmm maybe we try not to put this here as we open the door to "this does not work on my system"
created_at: 2025-02-10T16:52:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: don't include the full exact warning message as this is easy to forget to update :p

created_at: 2025-02-10T16:53:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has its own connection pool, and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
```
created_at: 2025-02-10T16:54:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: ```suggestion
!!! warning
```
created_at: 2025-02-10T16:54:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
+    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: Should or must?
```suggestion
    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
```
created_at: 2025-02-10T16:54:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
+    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
+
+## Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts. A global timeout can be set via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+```bash
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=inf
+```
+
+!!! warning "Global timeout"
+    Setting a global timeout via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable will override **ALL** individual timeout settings.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: even if I have other env variables?
created_at: 2025-02-10T16:56:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
+    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
+
+## Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts. A global timeout can be set via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+```bash
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=inf
+```
+
+!!! warning "Global timeout"
+    Setting a global timeout via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable will override **ALL** individual timeout settings.
+
+!!! tip "Disabling timeouts"
+    Setting a timeout to the float value `inf` will disable the timeout for that operation. ANTA will convert this value to `None` internally.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: do we need to mention the conversion? (I guess because the httpx documentation says None?)
created_at: 2025-02-10T16:56:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
+    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
+
+## Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts. A global timeout can be set via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+```bash
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=inf
+```
+
+!!! warning "Global timeout"
+    Setting a global timeout via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable will override **ALL** individual timeout settings.
+
+!!! tip "Disabling timeouts"
+    Setting a timeout to the float value `inf` will disable the timeout for that operation. ANTA will convert this value to `None` internally.
+
+## Performance Tuning
+
+For optimal performance on large fabrics, consider the following tuning parameters:
+
+1. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_CONNECT_TIMEOUT=60.0
+    export ANTA_READ_TIMEOUT=300.0
+    export ANTA_WRITE_TIMEOUT=60.0
+    export ANTA_POOL_TIMEOUT=inf
+    ```
+
+    This is **very** important for large fabrics. Setting appropriate timeouts for each operation type prevents test failures while maintaining proper error detection. The `ANTA_POOL_TIMEOUT=inf` default setting is crucial for large-scale deployments because:
+
+      - ANTA launches multiple test coroutines simultaneously (up to `ANTA_MAX_CONCURRENCY`)
+      - With thousands of tests, not all can acquire a connection immediately
+      - The connection pool queues requests when all connections are in use
+      - Without `ANTA_POOL_TIMEOUT=inf`, requests might fail if they can't get a connection within the specified timeout
+      - Setting it to `None` allows the connection pool to manage request queuing naturally
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: inf or None :) ?
created_at: 2025-02-10T16:58:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
+    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
+
+## Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts. A global timeout can be set via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+```bash
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=inf
+```
+
+!!! warning "Global timeout"
+    Setting a global timeout via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable will override **ALL** individual timeout settings.
+
+!!! tip "Disabling timeouts"
+    Setting a timeout to the float value `inf` will disable the timeout for that operation. ANTA will convert this value to `None` internally.
+
+## Performance Tuning
+
+For optimal performance on large fabrics, consider the following tuning parameters:
+
+1. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_CONNECT_TIMEOUT=60.0
+    export ANTA_READ_TIMEOUT=300.0
+    export ANTA_WRITE_TIMEOUT=60.0
+    export ANTA_POOL_TIMEOUT=inf
+    ```
+
+    This is **very** important for large fabrics. Setting appropriate timeouts for each operation type prevents test failures while maintaining proper error detection. The `ANTA_POOL_TIMEOUT=inf` default setting is crucial for large-scale deployments because:
+
+      - ANTA launches multiple test coroutines simultaneously (up to `ANTA_MAX_CONCURRENCY`)
+      - With thousands of tests, not all can acquire a connection immediately
+      - The connection pool queues requests when all connections are in use
+      - Without `ANTA_POOL_TIMEOUT=inf`, requests might fail if they can't get a connection within the specified timeout
+      - Setting it to `None` allows the connection pool to manage request queuing naturally
+
+2. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: it feels like this comment invite you to set the value to `n x t` (250 * 100) but for arger scale it is a bad idea to set it to say 1000000
created_at: 2025-02-10T16:58:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
+    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
+
+## Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts. A global timeout can be set via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+```bash
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=inf
+```
+
+!!! warning "Global timeout"
+    Setting a global timeout via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable will override **ALL** individual timeout settings.
+
+!!! tip "Disabling timeouts"
+    Setting a timeout to the float value `inf` will disable the timeout for that operation. ANTA will convert this value to `None` internally.
+
+## Performance Tuning
+
+For optimal performance on large fabrics, consider the following tuning parameters:
+
+1. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_CONNECT_TIMEOUT=60.0
+    export ANTA_READ_TIMEOUT=300.0
+    export ANTA_WRITE_TIMEOUT=60.0
+    export ANTA_POOL_TIMEOUT=inf
+    ```
+
+    This is **very** important for large fabrics. Setting appropriate timeouts for each operation type prevents test failures while maintaining proper error detection. The `ANTA_POOL_TIMEOUT=inf` default setting is crucial for large-scale deployments because:
+
+      - ANTA launches multiple test coroutines simultaneously (up to `ANTA_MAX_CONCURRENCY`)
+      - With thousands of tests, not all can acquire a connection immediately
+      - The connection pool queues requests when all connections are in use
+      - Without `ANTA_POOL_TIMEOUT=inf`, requests might fail if they can't get a connection within the specified timeout
+      - Setting it to `None` allows the connection pool to manage request queuing naturally
+
+2. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. The optimal value depends on your CPU usage, memory consumption, and total execution time. Start with the default and adjust in increments while monitoring these metrics to find the best balance for your environment.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: Should we recommend a max?
created_at: 2025-02-10T16:59:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
+    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
+
+## Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts. A global timeout can be set via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+```bash
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=inf
+```
+
+!!! warning "Global timeout"
+    Setting a global timeout via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable will override **ALL** individual timeout settings.
+
+!!! tip "Disabling timeouts"
+    Setting a timeout to the float value `inf` will disable the timeout for that operation. ANTA will convert this value to `None` internally.
+
+## Performance Tuning
+
+For optimal performance on large fabrics, consider the following tuning parameters:
+
+1. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_CONNECT_TIMEOUT=60.0
+    export ANTA_READ_TIMEOUT=300.0
+    export ANTA_WRITE_TIMEOUT=60.0
+    export ANTA_POOL_TIMEOUT=inf
+    ```
+
+    This is **very** important for large fabrics. Setting appropriate timeouts for each operation type prevents test failures while maintaining proper error detection. The `ANTA_POOL_TIMEOUT=inf` default setting is crucial for large-scale deployments because:
+
+      - ANTA launches multiple test coroutines simultaneously (up to `ANTA_MAX_CONCURRENCY`)
+      - With thousands of tests, not all can acquire a connection immediately
+      - The connection pool queues requests when all connections are in use
+      - Without `ANTA_POOL_TIMEOUT=inf`, requests might fail if they can't get a connection within the specified timeout
+      - Setting it to `None` allows the connection pool to manage request queuing naturally
+
+2. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. The optimal value depends on your CPU usage, memory consumption, and total execution time. Start with the default and adjust in increments while monitoring these metrics to find the best balance for your environment.
+
+3. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+    ```
+
+    The default values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+4. Consider JSON over YAML for better performance:
+
+    - JSON files load more efficiently than YAML in Python
+    - Convert large YAML files to JSON (you can use tools like `jq` or `yq`)
+
+        ```bash
+        yq -o=json eval 'inventory.yaml' > inventory.json
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: hmm we don't support this today right?
created_at: 2025-02-10T16:59:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
+    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
+
+## Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts. A global timeout can be set via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+```bash
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=inf
+```
+
+!!! warning "Global timeout"
+    Setting a global timeout via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable will override **ALL** individual timeout settings.
+
+!!! tip "Disabling timeouts"
+    Setting a timeout to the float value `inf` will disable the timeout for that operation. ANTA will convert this value to `None` internally.
+
+## Performance Tuning
+
+For optimal performance on large fabrics, consider the following tuning parameters:
+
+1. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_CONNECT_TIMEOUT=60.0
+    export ANTA_READ_TIMEOUT=300.0
+    export ANTA_WRITE_TIMEOUT=60.0
+    export ANTA_POOL_TIMEOUT=inf
+    ```
+
+    This is **very** important for large fabrics. Setting appropriate timeouts for each operation type prevents test failures while maintaining proper error detection. The `ANTA_POOL_TIMEOUT=inf` default setting is crucial for large-scale deployments because:
+
+      - ANTA launches multiple test coroutines simultaneously (up to `ANTA_MAX_CONCURRENCY`)
+      - With thousands of tests, not all can acquire a connection immediately
+      - The connection pool queues requests when all connections are in use
+      - Without `ANTA_POOL_TIMEOUT=inf`, requests might fail if they can't get a connection within the specified timeout
+      - Setting it to `None` allows the connection pool to manage request queuing naturally
+
+2. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. The optimal value depends on your CPU usage, memory consumption, and total execution time. Start with the default and adjust in increments while monitoring these metrics to find the best balance for your environment.
+
+3. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+    ```
+
+    The default values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+4. Consider JSON over YAML for better performance:
+
+    - JSON files load more efficiently than YAML in Python
+    - Convert large YAML files to JSON (you can use tools like `jq` or `yq`)
+
+        ```bash
+        yq -o=json eval 'inventory.yaml' > inventory.json
+        yq -o=json eval 'catalog.yaml' > catalog.json
+        ```
+
+    - Particularly beneficial for large inventories and test catalogs
+
+# 🔀 Distributed Testing
+
+While ANTA is designed to work efficiently as a single process, there may be scnarios where running multiple instances of ANTA is beneficial.
+
+## Possible Use Cases
+
+- **Distributed Testing**: Running ANTA instances on different machines to test separate parts of your network (e.g., data centers, regions)
+- **Parallel Execution**: Using tools like `GNU Parallel` to run multiple ANTA instances targeting different device groups (e.g., spines, leafs)
+- **Independent Testing**: Running separate ANTA instances for different testing purposes (e.g., connectivity tests vs. routing tests)
+
+# 🎉 Conclusion
+
+By implementing these scaling strategies, you can efficiently run ANTA tests on large network fabrics. The combination of optimized resource management, proper tuning, and thoughtful organization ensures fast and reliable network validation.
+
+Remember to:
+
+- ✅ Optimize resource usage through environment variables
+- 📋 Consider file format performance impacts
+- 📊 Choose appropriate output formats for your needs
+- ⚡ Monitor and adjust based on your specific environment
+
+!!! tip "Need Help?"
+    Performance tuning can be complex and highly dependent on your specific deployment. See the warning "Advanced Guide" at the beginning of this document for assistance options.
path: docs/advanced_usages/scaling.md
user: login: gmuloc
body: can we put an anchor?
created_at: 2025-02-10T17:01:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,494 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import get_file_descriptor_limit, get_max_concurrency
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from asyncio import Task
+    from collections.abc import AsyncGenerator, Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics for an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerScope(BaseModel):
+    """Define the scope of an ANTA test run.
+
+    The scope determines which devices and tests to include in a run, and how to
+    filter them. This class is used with the AntaRunner.run() method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If None, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If None, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When True, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If None, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    max_concurrency : int
+        Maximum number of tests that can run concurrently. Defaults from `get_max_concurrency()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    file_descriptor_limit : int
+        System file descriptor limit for connections. Defaults from `get_file_descriptor_limit()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        None if unknown, float('inf') if unlimited.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with _) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerScope
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerScope(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+    max_concurrency: int = Field(default_factory=get_max_concurrency)
+    file_descriptor_limit: int = Field(default_factory=get_file_descriptor_limit)
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = None
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+    _inventory_stats: AntaRunnerInventoryStats | None = None
+    _total_tests: int = 0
+    _potential_connections: float | None = None
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, scope: AntaRunnerScope | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        scope
+            Scope of the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        scope = scope or AntaRunnerScope()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(scope, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(scope):
+                    return manager
+
+            # Set up test coroutines
+            generator = self._setup_coroutines(manager=manager if dry_run else None)
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            async for test in generator:
+                test.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            async for result in self._run(generator):
+                manager.add(result)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, scope: AntaRunnerScope, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices, filtered_by_tags=0, connection_failed=0, established=0)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the CLI provided tags and devices if any
+        filtered_inventory = self.inventory.get_inventory(tags=scope.tags, devices=scope.devices) if scope.tags or scope.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
+        self._selected_inventory = filtered_inventory.get_inventory(established_only=scope.established_only)
+        connection_failed = len(filtered_inventory) - len(self._selected_inventory)
+
+        # If there are no devices in the inventory after filtering, exit
+        if not self._selected_inventory.devices:
+            # Build message parts
+            tag_msg = f"matching the tags {scope.tags} " if scope.tags else ""
+            device_msg = f" Selected devices: {scope.devices} " if scope.devices is not None else ""
+            logger.warning("No reachable device %swas found.%s", tag_msg, device_msg)
+            return False
+
+        self._inventory_stats = AntaRunnerInventoryStats(
+            total=total_devices, filtered_by_tags=filtered_by_tags, connection_failed=connection_failed, established=len(self._selected_inventory)
+        )
+        return True
+
+    def _setup_tests(self, scope: AntaRunnerScope) -> bool:
+        """Set up tests for the ANTA run."""
+        if self._selected_inventory is None:
+            msg = "The selected inventory is not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        # Build indexes for the catalog. If `tests` is set, filter the indexes based on these tests
+        self.catalog.build_indexes(filtered_tests=scope.tests)
+
+        # Using a set to avoid inserting duplicate tests
+        device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
+        total_tests = 0
+        total_connections = 0
+        all_have_connections = True
+
+        # Create the device to tests mapping from the tags and calculate connection stats
+        for device in self._selected_inventory.devices:
+            if scope.tags:
+                # If there are CLI tags, execute tests with matching tags for this device
+                if not (matching_tags := scope.tags.intersection(device.tags)):
+                    # The device does not have any selected tag, skipping
+                    continue
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(matching_tags))
+            else:
+                # If there is no CLI tags, execute all tests that do not have any tags
+                device_to_tests[device].update(self.catalog.tag_to_tests[None])
+
+                # Then add the tests with matching tags from device tags
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(device.tags))
+
+            total_tests += len(device_to_tests[device])
+
+            # Check device connections
+            if not hasattr(device, "max_connections") or device.max_connections is None:
+                all_have_connections = False
+            else:
+                total_connections += device.max_connections
+
+        if total_tests == 0:
+            tag_msg = f" matching the tags {scope.tags} " if scope.tags else " "
+            logger.warning(
+                "There are no tests%sto run in the current test catalog and device inventory, please verify your inputs.",
+                tag_msg,
+            )
+            return False
+
+        self._selected_tests = device_to_tests
+        self._total_tests = total_tests
+        self._potential_connections = None if not all_have_connections else total_connections
+        return True
+
+    async def _setup_coroutines(self, *, manager: ResultManager | None = None) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """Set up test coroutines from selected tests for the ANTA run.
+
+        It creates an async generator of coroutines which are created by the `test` method of the AntaTest instances.
+        Each coroutine is a test to run.
+
+        Parameters
+        ----------
+        manager
+            An optional ResultManager instance to pre-populate with the test results. Used in `dry_run` mode.
path: anta/_runner.py
user: login: gmuloc
body: maybe it can be done when we call close on all the coroutines instead
created_at: 2025-02-12T13:30:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:53:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:53:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:54:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:54:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:54:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:54:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:54:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:54:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
+    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:54:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
+    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
+
+## Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts. A global timeout can be set via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+```bash
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=inf
+```
+
+!!! warning "Global timeout"
+    Setting a global timeout via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable will override **ALL** individual timeout settings.
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:54:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
+    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
+
+## Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts. A global timeout can be set via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+```bash
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=inf
+```
+
+!!! warning "Global timeout"
+    Setting a global timeout via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable will override **ALL** individual timeout settings.
+
+!!! tip "Disabling timeouts"
+    Setting a timeout to the float value `inf` will disable the timeout for that operation. ANTA will convert this value to `None` internally.
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:54:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
+    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
+
+## Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts. A global timeout can be set via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+```bash
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=inf
+```
+
+!!! warning "Global timeout"
+    Setting a global timeout via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable will override **ALL** individual timeout settings.
+
+!!! tip "Disabling timeouts"
+    Setting a timeout to the float value `inf` will disable the timeout for that operation. ANTA will convert this value to `None` internally.
+
+## Performance Tuning
+
+For optimal performance on large fabrics, consider the following tuning parameters:
+
+1. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_CONNECT_TIMEOUT=60.0
+    export ANTA_READ_TIMEOUT=300.0
+    export ANTA_WRITE_TIMEOUT=60.0
+    export ANTA_POOL_TIMEOUT=inf
+    ```
+
+    This is **very** important for large fabrics. Setting appropriate timeouts for each operation type prevents test failures while maintaining proper error detection. The `ANTA_POOL_TIMEOUT=inf` default setting is crucial for large-scale deployments because:
+
+      - ANTA launches multiple test coroutines simultaneously (up to `ANTA_MAX_CONCURRENCY`)
+      - With thousands of tests, not all can acquire a connection immediately
+      - The connection pool queues requests when all connections are in use
+      - Without `ANTA_POOL_TIMEOUT=inf`, requests might fail if they can't get a connection within the specified timeout
+      - Setting it to `None` allows the connection pool to manage request queuing naturally
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:54:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
+    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
+
+## Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts. A global timeout can be set via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+```bash
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=inf
+```
+
+!!! warning "Global timeout"
+    Setting a global timeout via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable will override **ALL** individual timeout settings.
+
+!!! tip "Disabling timeouts"
+    Setting a timeout to the float value `inf` will disable the timeout for that operation. ANTA will convert this value to `None` internally.
+
+## Performance Tuning
+
+For optimal performance on large fabrics, consider the following tuning parameters:
+
+1. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_CONNECT_TIMEOUT=60.0
+    export ANTA_READ_TIMEOUT=300.0
+    export ANTA_WRITE_TIMEOUT=60.0
+    export ANTA_POOL_TIMEOUT=inf
+    ```
+
+    This is **very** important for large fabrics. Setting appropriate timeouts for each operation type prevents test failures while maintaining proper error detection. The `ANTA_POOL_TIMEOUT=inf` default setting is crucial for large-scale deployments because:
+
+      - ANTA launches multiple test coroutines simultaneously (up to `ANTA_MAX_CONCURRENCY`)
+      - With thousands of tests, not all can acquire a connection immediately
+      - The connection pool queues requests when all connections are in use
+      - Without `ANTA_POOL_TIMEOUT=inf`, requests might fail if they can't get a connection within the specified timeout
+      - Setting it to `None` allows the connection pool to manage request queuing naturally
+
+2. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. The optimal value depends on your CPU usage, memory consumption, and total execution time. Start with the default and adjust in increments while monitoring these metrics to find the best balance for your environment.
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:55:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
+    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
+
+## Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts. A global timeout can be set via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+```bash
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=inf
+```
+
+!!! warning "Global timeout"
+    Setting a global timeout via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable will override **ALL** individual timeout settings.
+
+!!! tip "Disabling timeouts"
+    Setting a timeout to the float value `inf` will disable the timeout for that operation. ANTA will convert this value to `None` internally.
+
+## Performance Tuning
+
+For optimal performance on large fabrics, consider the following tuning parameters:
+
+1. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_CONNECT_TIMEOUT=60.0
+    export ANTA_READ_TIMEOUT=300.0
+    export ANTA_WRITE_TIMEOUT=60.0
+    export ANTA_POOL_TIMEOUT=inf
+    ```
+
+    This is **very** important for large fabrics. Setting appropriate timeouts for each operation type prevents test failures while maintaining proper error detection. The `ANTA_POOL_TIMEOUT=inf` default setting is crucial for large-scale deployments because:
+
+      - ANTA launches multiple test coroutines simultaneously (up to `ANTA_MAX_CONCURRENCY`)
+      - With thousands of tests, not all can acquire a connection immediately
+      - The connection pool queues requests when all connections are in use
+      - Without `ANTA_POOL_TIMEOUT=inf`, requests might fail if they can't get a connection within the specified timeout
+      - Setting it to `None` allows the connection pool to manage request queuing naturally
+
+2. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. The optimal value depends on your CPU usage, memory consumption, and total execution time. Start with the default and adjust in increments while monitoring these metrics to find the best balance for your environment.
+
+3. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+    ```
+
+    The default values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+4. Consider JSON over YAML for better performance:
+
+    - JSON files load more efficiently than YAML in Python
+    - Convert large YAML files to JSON (you can use tools like `jq` or `yq`)
+
+        ```bash
+        yq -o=json eval 'inventory.yaml' > inventory.json
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:55:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
+
+# 🔄 Core Optimizations
+
+## Understanding ANTA's Asynchronous Nature
+
+ANTA uses Python `asyncio` library for concurrent execution:
+
+- 📦 Each test is a coroutine in the main event loop
+- ⚡ Tests run concurrently, not in parallel
+- ⚙️ The event loop manages all coroutines and network I/O
+
+!!! note "Key Concept"
+    While `asyncio` provides excellent concurrency, there are practical limits to how many coroutines can be efficiently managed in a single event loop running on one CPU core. High coroutine counts increase event loop overhead, lead to frequent context switching, consume more memory, and may degrade performance.
+
+## Resource Management
+
+This section describes the environment variables to control resource usage provided by ANTA.
+
+!!! tip "Default Values"
+    The values specified in the following examples are the actual default values used by ANTA. See the [Performance Tuning](#performance-tuning) section for guidance on adjusting these values.
+
+### **Test Concurrency**
+
+ANTA limits the number of concurrent test coroutines to prevent event loop overload. The runner schedules coroutines up to this limit, then waits for some to complete before scheduling more. The following variable can be used to tune this limit:
+
+```bash
+export ANTA_MAX_CONCURRENCY=10000
+```
+
+### **File Descriptors**
+
+ANTA manages the number of open file descriptors, which are primarily used for handling device connections. At startup, on POSIX systems (Linux/macOS), ANTA attempts to set its process's soft limit to the maximum allowed value (up to 16384). This adjustment is necessary because the default soft limit (typically 1024) may be too low for large-scale testing, while the system's hard limit is often higher. If the system's hard limit is lower than the number of file descriptors ANTA needs, you'll see a warning at startup. The following environment variable controls this setting:
+
+```bash
+export ANTA_NOFILE=16384
+```
+
+On Windows (non-POSIX systems), ANTA cannot adjust file descriptor limits. In these cases, ANTA will:
+
+- Use Python `sys.maxsize` as the limit
+- Display a warning message: "Running on a non-POSIX system, cannot adjust the maximum number of file descriptors"
+- Continue operation with system defaults
+
+For POSIX systems, if you need to increase the hard limit:
+
+- Check your current hard limit:
+
+```bash
+ulimit -n -H
+```
+
+- Set a new limit by creating `/etc/security/limits.d/10-anta.conf`:
+
+```bash
+<user> hard nofile <value>
+```
+
+Replace `<user>` with the username running ANTA, and `<value>` with your desired limit (system-dependent).
+
+- Log out and back in for changes to take effect.
+
+## Connection Management
+
+For [`AsyncEosDevice`](../api/device.md#anta.device.AsyncEOSDevice) implementations (default in ANTA), ANTA uses the [`httpx`](https://www.python-httpx.org/) library as its underlying HTTP client for device connections. Each device has his own connection pool and HTTPX uses a default maximum of `100` connections. On large fabrics, this can lead to a very high number of connections (n x 100), which may overwhelm the ANTA process. Note that custom `AntaDevice` subclasses might implement different HTTP settings. For `AsyncEosDevice`, connection pooling can be tuned via the following environment variables. See [HTTPX documentation](https://www.python-httpx.org/advanced/resource-limits/) for more details.
+
+```bash
+# Maximum number of allowable connections
+export ANTA_MAX_CONNECTIONS=10
+
+# Number of allowable keep-alive connections
+export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+
+# Time limit on idle keep-alive connections in seconds
+export ANTA_KEEPALIVE_EXPIRY=5.0
+```
+
+!!! warning "Variable relationships"
+    The value of `ANTA_MAX_KEEPALIVE_CONNECTIONS` should be lower than `ANTA_MAX_CONNECTIONS`.
+
+## Timeouts Configuration
+
+ANTA provides several environment variables to control `httpx` timeouts. A global timeout can be set via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable. See [HTTPX documentation](https://www.python-httpx.org/advanced/timeouts/) for more details.
+
+```bash
+# Maximum amount of time to wait until a socket connection to the requested host is established
+export ANTA_CONNECT_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be received (for example, a chunk of the response body)
+export ANTA_READ_TIMEOUT=30.0
+
+# Maximum duration to wait for a chunk of data to be sent (for example, a chunk of the request body)
+export ANTA_WRITE_TIMEOUT=30.0
+
+# Maximum duration to wait for acquiring a connection from the connection pool
+export ANTA_POOL_TIMEOUT=inf
+```
+
+!!! warning "Global timeout"
+    Setting a global timeout via the `--timeout` command-line option or the `ANTA_TIMEOUT` environment variable will override **ALL** individual timeout settings.
+
+!!! tip "Disabling timeouts"
+    Setting a timeout to the float value `inf` will disable the timeout for that operation. ANTA will convert this value to `None` internally.
+
+## Performance Tuning
+
+For optimal performance on large fabrics, consider the following tuning parameters:
+
+1. Increase timeouts for stability:
+
+    ```bash
+    export ANTA_CONNECT_TIMEOUT=60.0
+    export ANTA_READ_TIMEOUT=300.0
+    export ANTA_WRITE_TIMEOUT=60.0
+    export ANTA_POOL_TIMEOUT=inf
+    ```
+
+    This is **very** important for large fabrics. Setting appropriate timeouts for each operation type prevents test failures while maintaining proper error detection. The `ANTA_POOL_TIMEOUT=inf` default setting is crucial for large-scale deployments because:
+
+      - ANTA launches multiple test coroutines simultaneously (up to `ANTA_MAX_CONCURRENCY`)
+      - With thousands of tests, not all can acquire a connection immediately
+      - The connection pool queues requests when all connections are in use
+      - Without `ANTA_POOL_TIMEOUT=inf`, requests might fail if they can't get a connection within the specified timeout
+      - Setting it to `None` allows the connection pool to manage request queuing naturally
+
+2. Adjust concurrency based on test count and system resources:
+
+    ```bash
+    # For 250 devices with 100 tests each (25,000 total tests)
+    export ANTA_MAX_CONCURRENCY=25000
+    ```
+
+    Even though the default value is `10000`, it may not be optimal for your system. The optimal value depends on your CPU usage, memory consumption, and total execution time. Start with the default and adjust in increments while monitoring these metrics to find the best balance for your environment.
+
+3. Optimize connection pooling:
+
+    ```bash
+    export ANTA_MAX_CONNECTIONS=10
+    export ANTA_MAX_KEEPALIVE_CONNECTIONS=10
+    ```
+
+    The default values are a good starting point for large fabrics. Adjust them based on your fabric size and performance testing.
+
+4. Consider JSON over YAML for better performance:
+
+    - JSON files load more efficiently than YAML in Python
+    - Convert large YAML files to JSON (you can use tools like `jq` or `yq`)
+
+        ```bash
+        yq -o=json eval 'inventory.yaml' > inventory.json
+        yq -o=json eval 'catalog.yaml' > catalog.json
+        ```
+
+    - Particularly beneficial for large inventories and test catalogs
+
+# 🔀 Distributed Testing
+
+While ANTA is designed to work efficiently as a single process, there may be scnarios where running multiple instances of ANTA is beneficial.
+
+## Possible Use Cases
+
+- **Distributed Testing**: Running ANTA instances on different machines to test separate parts of your network (e.g., data centers, regions)
+- **Parallel Execution**: Using tools like `GNU Parallel` to run multiple ANTA instances targeting different device groups (e.g., spines, leafs)
+- **Independent Testing**: Running separate ANTA instances for different testing purposes (e.g., connectivity tests vs. routing tests)
+
+# 🎉 Conclusion
+
+By implementing these scaling strategies, you can efficiently run ANTA tests on large network fabrics. The combination of optimized resource management, proper tuning, and thoughtful organization ensures fast and reliable network validation.
+
+Remember to:
+
+- ✅ Optimize resource usage through environment variables
+- 📋 Consider file format performance impacts
+- 📊 Choose appropriate output formats for your needs
+- ⚡ Monitor and adjust based on your specific environment
+
+!!! tip "Need Help?"
+    Performance tuning can be complex and highly dependent on your specific deployment. See the warning "Advanced Guide" at the beginning of this document for assistance options.
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:55:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,224 @@
+---
+anta_title: "🚀 Scaling ANTA: A Comprehensive Guide"
+---
+<!--
+  ~ Copyright (c) 2023-2025 Arista Networks, Inc.
+  ~ Use of this source code is governed by the Apache License 2.0
+  ~ that can be found in the LICENSE file.
+  -->
+
+# 📖 Introduction
+
+!!! warning "Advanced Guide"
+    This document covers advanced ANTA configuration and scaling strategies. If you need assistance with specific deployments or optimizations:
+
+    - 📧 Contact your favorite Arista Systems Engineer
+    - 💻 Open an issue or a discussion on ANTA [GitHub repository](https://github.com/aristanetworks/anta)
+    - 🤝 Join the discussion in our community channels
+
+    This guide also assumes you have a basic understanding of ANTA and are familiar with its core concepts. If you're new to ANTA, we recommend starting with the [ANTA Getting Started](../getting-started.md) before proceeding.
+
+**ANTA** (Arista Network Test Automation) is continually evolving to meet the demands of large-scale network validation. As networks grow in size and complexity, the need for efficient, scalable testing becomes increasingly important. This guide explores comprehensive strategies for scaling ANTA to handle large network fabrics effectively.
path: docs/advanced_usages/scaling.md
user: login: carl-baillargeon
body: Removed the guide. Check FAQ instead.
created_at: 2025-02-24T12:55:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,473 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime, limit_concurrency
+
+if TYPE_CHECKING:
+    from collections.abc import AsyncGenerator, Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
path: anta/_runner.py
user: login: gmuloc
body: do we need the type hints here? we tend to not put them for parameters in funcitons
created_at: 2025-03-05T09:33:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -385,15 +392,26 @@ def __init__(  # noqa: PLR0913
             message = f"'password' is required to instantiate device '{self.name}'"
             logger.error(message)
             raise ValueError(message)
+
         self.enable = enable
         self._enable_password = enable_password
-        self._session: asynceapi.Device = asynceapi.Device(host=host, port=port, username=username, password=password, proto=proto, timeout=timeout)
-        ssh_params: dict[str, Any] = {}
+
+        # Build the session settings for the `asynceapi` client
+        session_settings: dict[str, Any] = {
+            "host": host,
+            "port": port,
+            "username": username,
+            "password": password,
+            "proto": proto,
+            "timeout": timeout,
+        }
+        self._session: asynceapi.Device = asynceapi.Device(**session_settings)
path: anta/device.py
user: login: gmuloc
body: do you need the type hint?
created_at: 2025-03-05T09:36:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -385,15 +392,26 @@ def __init__(  # noqa: PLR0913
             message = f"'password' is required to instantiate device '{self.name}'"
             logger.error(message)
             raise ValueError(message)
+
         self.enable = enable
         self._enable_password = enable_password
-        self._session: asynceapi.Device = asynceapi.Device(host=host, port=port, username=username, password=password, proto=proto, timeout=timeout)
-        ssh_params: dict[str, Any] = {}
+
+        # Build the session settings for the `asynceapi` client
+        session_settings: dict[str, Any] = {
path: anta/device.py
user: login: gmuloc
body: any reason to move this to a dict?
created_at: 2025-03-05T09:36:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -385,15 +392,26 @@ def __init__(  # noqa: PLR0913
             message = f"'password' is required to instantiate device '{self.name}'"
             logger.error(message)
             raise ValueError(message)
+
         self.enable = enable
         self._enable_password = enable_password
-        self._session: asynceapi.Device = asynceapi.Device(host=host, port=port, username=username, password=password, proto=proto, timeout=timeout)
-        ssh_params: dict[str, Any] = {}
+
+        # Build the session settings for the `asynceapi` client
+        session_settings: dict[str, Any] = {
+            "host": host,
+            "port": port,
+            "username": username,
+            "password": password,
+            "proto": proto,
+            "timeout": timeout,
+        }
+        self._session: asynceapi.Device = asynceapi.Device(**session_settings)
+
+        # Build the SSH connection options
path: anta/device.py
user: login: gmuloc
body: same why create an extra dict?
created_at: 2025-03-05T09:37:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -442,6 +461,14 @@ def _keys(self) -> tuple[Any, ...]:
         """
         return (self._session.host, self._session.port)
 
+    @property
+    def max_connections(self) -> int | None:
+        """Maximum number of concurrent connections allowed by the device. Returns None if not available."""
+        try:
+            return attrgetter("_transport._pool._max_connections")(self._session)
path: anta/device.py
user: login: gmuloc
body: do you need `attrgetter` why not just
`self._session._transport._pool._max_connections` ? (it will also raise an Attribute Error on any missing subattribute?
created_at: 2025-03-05T09:38:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -329,9 +337,9 @@ def __init__(  # noqa: PLR0913
         name: str | None = None,
         enable_password: str | None = None,
         port: int | None = None,
-        ssh_port: int | None = 22,
+        ssh_port: int = 22,
         tags: set[str] | None = None,
-        timeout: float | None = None,
+        timeout: float | None = 30.0,
path: anta/device.py
user: login: gmuloc
body: This is breaking for people using the Python class in a script and relying on the default.

Do we need it in this PR? Can we make it a 2.0 thing?
created_at: 2025-03-05T09:42:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,6 +50,7 @@ def run_tests(ctx: click.Context) -> None:
 
     print_settings(inventory, catalog)
     with anta_progress_bar() as AntaTest.progress:
+        # TODO: Use AntaRunner in ANTA v2.0.0
path: anta/cli/nrfu/utils.py
user: login: gmuloc
body: ```suggestion
        # TODO: Use AntaRunner directly in ANTA v2.0.0
```
created_at: 2025-03-05T09:45:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +43,39 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **10000** tests concurrently. This default is a balance between performance and stability, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA schedules the first 10000 tests and waits for some tests to complete before scheduling more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system's CPU usage, memory consumption, and file descriptor limits.
path: docs/faq.md
user: login: gmuloc
body: ```suggestion
    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system CPU usage, memory consumption, and file descriptor limits.
```
created_at: 2025-03-05T09:49:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +43,39 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **10000** tests concurrently. This default is a balance between performance and stability, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA schedules the first 10000 tests and waits for some tests to complete before scheduling more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system's CPU usage, memory consumption, and file descriptor limits.
+
+    !!! warning
+
+        Increasing the maximum concurrency limit can lead to system instability if the system is not able to handle the increased load. Monitor system resources and adjust the limit accordingly.
+
+    !!! info "Device Connection Limits"
+
+        Each device is limited to a maximum of **100** concurrent connections. This means that, even if ANTA schedules a high number of tests, each device will only attempt to open up to 100 connections at a time. Furthermore, Arista EOS eAPI is inherently protected against overload and is designed to handle high connection volumes safely.
path: docs/faq.md
user: login: gmuloc
body: the last sentence is unclear to me 
created_at: 2025-03-05T09:49:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +43,39 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **10000** tests concurrently. This default is a balance between performance and stability, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA schedules the first 10000 tests and waits for some tests to complete before scheduling more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system's CPU usage, memory consumption, and file descriptor limits.
+
+    !!! warning
+
+        Increasing the maximum concurrency limit can lead to system instability if the system is not able to handle the increased load. Monitor system resources and adjust the limit accordingly.
+
+    !!! info "Device Connection Limits"
+
+        Each device is limited to a maximum of **100** concurrent connections. This means that, even if ANTA schedules a high number of tests, each device will only attempt to open up to 100 connections at a time. Furthermore, Arista EOS eAPI is inherently protected against overload and is designed to handle high connection volumes safely.
+
+    ANTA also offers several test scheduling strategies to optimize test execution, particularly relevant when the total number of tests exceeds the maximum concurrency limit. It is configurable via the `ANTA_SCHEDULING_STRATEGY` environment variable (default is `round-robin`), along with `ANTA_SCHEDULING_TESTS_PER_DEVICE` (default is **100**) for the `device-by-count` strategy:
path: docs/faq.md
user: login: gmuloc
body: Feel weird that this doc is in an FAQ
created_at: 2025-03-05T09:50:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +43,39 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **10000** tests concurrently. This default is a balance between performance and stability, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA schedules the first 10000 tests and waits for some tests to complete before scheduling more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system's CPU usage, memory consumption, and file descriptor limits.
+
+    !!! warning
+
+        Increasing the maximum concurrency limit can lead to system instability if the system is not able to handle the increased load. Monitor system resources and adjust the limit accordingly.
+
+    !!! info "Device Connection Limits"
+
+        Each device is limited to a maximum of **100** concurrent connections. This means that, even if ANTA schedules a high number of tests, each device will only attempt to open up to 100 connections at a time. Furthermore, Arista EOS eAPI is inherently protected against overload and is designed to handle high connection volumes safely.
+
+    ANTA also offers several test scheduling strategies to optimize test execution, particularly relevant when the total number of tests exceeds the maximum concurrency limit. It is configurable via the `ANTA_SCHEDULING_STRATEGY` environment variable (default is `round-robin`), along with `ANTA_SCHEDULING_TESTS_PER_DEVICE` (default is **100**) for the `device-by-count` strategy:
path: docs/faq.md
user: login: gmuloc
body: ```suggestion
    ANTA offers several test scheduling strategies to optimize test execution, which is particularly relevant when the total number of tests exceeds the maximum concurrency limit. It is configurable via the `ANTA_SCHEDULING_STRATEGY` environment variable (default is `round-robin`), along with `ANTA_SCHEDULING_TESTS_PER_DEVICE` (default is **100**) for the `device-by-count` strategy:
```
created_at: 2025-03-05T09:51:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +43,39 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **10000** tests concurrently. This default is a balance between performance and stability, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA schedules the first 10000 tests and waits for some tests to complete before scheduling more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system's CPU usage, memory consumption, and file descriptor limits.
+
+    !!! warning
+
+        Increasing the maximum concurrency limit can lead to system instability if the system is not able to handle the increased load. Monitor system resources and adjust the limit accordingly.
+
+    !!! info "Device Connection Limits"
+
+        Each device is limited to a maximum of **100** concurrent connections. This means that, even if ANTA schedules a high number of tests, each device will only attempt to open up to 100 connections at a time. Furthermore, Arista EOS eAPI is inherently protected against overload and is designed to handle high connection volumes safely.
+
+    ANTA also offers several test scheduling strategies to optimize test execution, particularly relevant when the total number of tests exceeds the maximum concurrency limit. It is configurable via the `ANTA_SCHEDULING_STRATEGY` environment variable (default is `round-robin`), along with `ANTA_SCHEDULING_TESTS_PER_DEVICE` (default is **100**) for the `device-by-count` strategy:
+
+    - **Round-robin (`round-robin`)**: Distributes tests evenly across devices. This is generally suitable for small to medium-sized (around 200 devices) fabrics but can open many simultaneous connections.
+    - **Device-by-Device (`device-by-device`)**: Executes all tests for one device before moving on to the next, which may help reduce peak concurrent connections.
+    - **Device-by-Count (`device-by-count`)**: Limits the number of tests scheduled per device in each round. This provides finer control in larger environments where opening too many connections simultaneously might exceed system limits.
path: docs/faq.md
user: login: gmuloc
body: can we test these on a benchmark (not necessarily in CI) and check the performance and time to get some ideas? (I guess you have done this already but asking here)
created_at: 2025-03-05T09:52:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +43,39 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **10000** tests concurrently. This default is a balance between performance and stability, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA schedules the first 10000 tests and waits for some tests to complete before scheduling more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system's CPU usage, memory consumption, and file descriptor limits.
+
+    !!! warning
+
+        Increasing the maximum concurrency limit can lead to system instability if the system is not able to handle the increased load. Monitor system resources and adjust the limit accordingly.
+
+    !!! info "Device Connection Limits"
+
+        Each device is limited to a maximum of **100** concurrent connections. This means that, even if ANTA schedules a high number of tests, each device will only attempt to open up to 100 connections at a time. Furthermore, Arista EOS eAPI is inherently protected against overload and is designed to handle high connection volumes safely.
+
+    ANTA also offers several test scheduling strategies to optimize test execution, particularly relevant when the total number of tests exceeds the maximum concurrency limit. It is configurable via the `ANTA_SCHEDULING_STRATEGY` environment variable (default is `round-robin`), along with `ANTA_SCHEDULING_TESTS_PER_DEVICE` (default is **100**) for the `device-by-count` strategy:
+
+    - **Round-robin (`round-robin`)**: Distributes tests evenly across devices. This is generally suitable for small to medium-sized (around 200 devices) fabrics but can open many simultaneous connections.
+    - **Device-by-Device (`device-by-device`)**: Executes all tests for one device before moving on to the next, which may help reduce peak concurrent connections.
+    - **Device-by-Count (`device-by-count`)**: Limits the number of tests scheduled per device in each round. This provides finer control in larger environments where opening too many connections simultaneously might exceed system limits.
+
+    **Recommendation:** If you're running ANTA on a large fabric or encounter issues related to resource limits, considering tuning these settings. Test different configurations to find the optimal balance for your system.
path: docs/faq.md
user: login: gmuloc
body: make this a `tip`

```suggestion
    **Recommendation:** If you urn ANTA on a large fabric or encounter issues related to resource limits, consider tuning these settings. Test different configurations to find the optimal balance for your system.
```
created_at: 2025-03-05T09:53:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,6 +37,7 @@ def test_anta_dry_run(
 
     results = session_results[request.node.callspec.id]
 
+    # TODO: Use AntaRunner in ANTA v2.0.0
path: tests/benchmark/test_anta.py
user: login: gmuloc
body: ```suggestion
    # TODO: Use AntaRunner directly in ANTA v2.0.0
```
created_at: 2025-03-05T09:54:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,6 +70,7 @@ def test_anta(
 
     results = session_results[request.node.callspec.id]
 
+    # TODO: Use AntaRunner in ANTA v2.0.0
path: tests/benchmark/test_anta.py
user: login: gmuloc
body: ```suggestion
    # TODO: Use AntaRunner directly in ANTA v2.0.0
```
created_at: 2025-03-05T09:54:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,3 +59,19 @@ def bench() -> list[Coroutine[Any, Any, TestResult]]:
 
     count = sum(len(tests) for tests in selected_tests.values())
     assert count == len(coroutines)
+
+
+def test_setup_tests(benchmark: BenchmarkFixture, catalog: AntaCatalog, inventory: AntaInventory) -> None:
+    """Benchmark `anta._runner.AntaRunner._setup_tests`."""
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+    runner._selected_inventory = inventory
+
+    def bench() -> bool:
path: tests/benchmark/test_runner.py
user: login: gmuloc
body: don't think you need to return `bool`
created_at: 2025-03-05T09:57:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,3 +89,64 @@ def yaml_file(request: pytest.FixtureRequest, tmp_path: Path) -> Path:
     content: dict[str, Any] = request.param
     file.write_text(yaml.dump(content, allow_unicode=True))
     return file
+
+
+@pytest.fixture
+def anta_runner(request: pytest.FixtureRequest) -> AntaRunner:
+    """AntaRunner fixture.
+
+    Must be parametrized with a dictionary containing the following keys:
+    - inventory: Inventory file name from the data directory
+    - catalog: Catalog file name from the data directory
+
+    Optional keys:
+    - manager: ResultManager instance
+    - max_concurrency: Maximum concurrency limit
+    - nofile: File descriptor limit
+    """
+    # Import must be inside fixture to prevent circular dependency from breaking CLI tests:
+    # anta.runner -> anta.cli.console -> anta.cli/* (not yet loaded) -> anta.cli.anta
path: tests/units/conftest.py
user: login: gmuloc
body: More like this?
```suggestion
    # anta._runner -> anta.cli.console -> anta.cli/* (not yet loaded) -> anta.cli.anta
```
created_at: 2025-03-05T09:59:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,473 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime, limit_concurrency
+
+if TYPE_CHECKING:
+    from collections.abc import AsyncGenerator, Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If `None`, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        `None` if unknown.
+    _settings : AntaRunnerSettings
+        Internal settings loaded from environment variables. See the class definition
+        in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with `_`) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerFilter
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerFilter(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = PrivateAttr(default=None)
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = PrivateAttr(default=None)
+    _inventory_stats: AntaRunnerInventoryStats | None = PrivateAttr(default=None)
+    _total_tests: int = PrivateAttr(default=0)
+    _potential_connections: float | None = PrivateAttr(default=None)
+
+    # Internal settings loaded from environment variables
+    _settings: AntaRunnerSettings = PrivateAttr(default_factory=AntaRunnerSettings)
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, filters: AntaRunnerFilter | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        filters
+            Filters for the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        filters = filters or AntaRunnerFilter()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(filters, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(filters):
+                    return manager
+
+            # Build the test generator
+            test_generator = self._test_generator(manager if dry_run else None)
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            async for test in test_generator:
+                test.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            async for result in limit_concurrency(test_generator, limit=self._settings.max_concurrency):
+                manager.add(await result)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, filters: AntaRunnerFilter, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices, filtered_by_tags=0, connection_failed=0, established=0)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the provided filters any
+        filtered_inventory = self.inventory.get_inventory(tags=filters.tags, devices=filters.devices) if filters.tags or filters.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
+        self._selected_inventory = filtered_inventory.get_inventory(established_only=filters.established_only)
+        connection_failed = len(filtered_inventory) - len(self._selected_inventory)
+
+        # If there are no devices in the inventory after filtering, exit
+        if not self._selected_inventory.devices:
+            # Build message parts
+            tag_msg = f"matching the tags {filters.tags} " if filters.tags else ""
+            device_msg = f" Selected devices: {filters.devices} " if filters.devices is not None else ""
+            logger.warning("No reachable device %swas found.%s", tag_msg, device_msg)
+            return False
+
+        self._inventory_stats = AntaRunnerInventoryStats(
+            total=total_devices, filtered_by_tags=filtered_by_tags, connection_failed=connection_failed, established=len(self._selected_inventory)
+        )
+        return True
+
+    def _setup_tests(self, filters: AntaRunnerFilter) -> bool:
+        """Set up tests for the ANTA run."""
+        if self._selected_inventory is None:
+            msg = "The selected inventory is not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        # Build indexes for the catalog. If `filters.tests` is set, filter the indexes based on these tests
+        self.catalog.build_indexes(filtered_tests=filters.tests)
+
+        # Using a set to avoid inserting duplicate tests
+        device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
+        total_tests = 0
+        total_connections = 0
+        all_have_connections = True
+
+        # Create the device to tests mapping from the tags and calculate connection stats
+        for device in self._selected_inventory.devices:
+            if filters.tags:
+                # If there are CLI tags, execute tests with matching tags for this device
+                if not (matching_tags := filters.tags.intersection(device.tags)):
+                    # The device does not have any selected tag, skipping
+                    continue
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(matching_tags))
+            else:
+                # If there is no CLI tags, execute all tests that do not have any tags
+                device_to_tests[device].update(self.catalog.tag_to_tests[None])
+
+                # Then add the tests with matching tags from device tags
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(device.tags))
+
+            total_tests += len(device_to_tests[device])
+
+            # Check device connections
+            if not hasattr(device, "max_connections") or device.max_connections is None:
+                all_have_connections = False
+            else:
+                total_connections += device.max_connections
+
+        if total_tests == 0:
+            tag_msg = f" matching the tags {filters.tags} " if filters.tags else " "
+            logger.warning(
+                "There are no tests%sto run in the current test catalog and device inventory, please verify your inputs.",
+                tag_msg,
+            )
+            return False
+
+        self._selected_tests = device_to_tests
+        self._total_tests = total_tests
+        self._potential_connections = None if not all_have_connections else total_connections
+        return True
+
+    async def _test_generator(self, manager: ResultManager | None = None) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """Generate test coroutines for the ANTA run."""
+        if self._selected_tests is None:
+            msg = "The selected tests are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        logger.debug("ANTA run scheduling strategy: %s", self._settings.scheduling_strategy)
+
+        device_to_tests: dict[AntaDevice, list[AntaTestDefinition]] = {
+            device: sorted(tests, key=lambda td: td.test.__name__) for device, tests in self._selected_tests.items()
+        }
+
+        if self._settings.scheduling_strategy == "device-by-device":
+            async for coro in self._generate_device_by_device(device_to_tests, manager):
+                yield coro
+        elif self._settings.scheduling_strategy == "device-by-count":
+            async for coro in self._generate_device_by_count(device_to_tests, self._settings.scheduling_tests_per_device, manager):
+                yield coro
+        else:
+            # Default to round-robin
+            async for coro in self._generate_round_robin(device_to_tests, manager):
+                yield coro
+
+    async def _generate_round_robin(
+        self, device_to_tests: dict[AntaDevice, list[AntaTestDefinition]], manager: ResultManager | None = None
+    ) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """Yield one test per device in each round."""
+        while any(device_to_tests.values()):
+            for device, tests in device_to_tests.items():
+                if tests:
+                    test_def = tests.pop(0)
+                    coro = self._create_test_coroutine(test_def, device, manager)
+                    if coro is not None:
+                        yield coro
+
+    async def _generate_device_by_device(
+        self, device_to_tests: dict[AntaDevice, list[AntaTestDefinition]], manager: ResultManager | None = None
+    ) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """Yield all tests for one device before moving to the next."""
+        for device, tests in device_to_tests.items():
+            while tests:
+                test_def = tests.pop(0)
+                coro = self._create_test_coroutine(test_def, device, manager)
+                if coro is not None:
+                    yield coro
+
+    async def _generate_device_by_count(
+        self,
+        device_to_tests: dict[AntaDevice, list[AntaTestDefinition]],
+        tests_per_device: int,
+        manager: ResultManager | None = None,
+    ) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """In each round, yield up to `tests_per_device` tests for each device."""
+        while any(device_to_tests.values()):
+            for device, tests in device_to_tests.items():
+                count = min(tests_per_device, len(tests))
+                for _ in range(count):
+                    test_def = tests.pop(0)
+                    coro = self._create_test_coroutine(test_def, device, manager)
+                    if coro is not None:
+                        yield coro
+
+    def _create_test_coroutine(
+        self, test_def: AntaTestDefinition, device: AntaDevice, manager: ResultManager | None = None
+    ) -> Coroutine[Any, Any, TestResult] | None:
+        """Create a test coroutine from a test definition."""
+        try:
+            test_instance = test_def.test(device=device, inputs=test_def.inputs)
+            if manager is not None:
+                manager.add(test_instance.result)
+            coroutine = test_instance.test()
+        except Exception as e:  # noqa: BLE001
+            # An AntaTest instance is potentially user-defined code.
+            # We need to catch everything and exit gracefully with an error message.
+            message = "\n".join(
+                [
+                    f"There is an error when creating test {test_def.test.__module__}.{test_def.test.__name__}.",
+                    f"If this is not a custom test implementation: {GITHUB_SUGGESTION}",
+                ],
+            )
+            anta_log_exception(e, message, logger)
+            return None
+        return coroutine
+
+    def _log_run_information(self, *, dry_run: bool = False) -> None:
+        """Log ANTA run information and potential resource limit warnings."""
+        if self._inventory_stats is None:
+            msg = "The inventory stats are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        width = min(int(console.width) - 34, len("  Total potential connections: 100000000\n"))
path: anta/_runner.py
user: login: gmuloc
body: Hmmm if this is the only place where we need this to break the circular import, we should think about making this better maybe? Like giving console_width as a param to _log_run_information?
created_at: 2025-03-05T10:01:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,3 +89,64 @@ def yaml_file(request: pytest.FixtureRequest, tmp_path: Path) -> Path:
     content: dict[str, Any] = request.param
     file.write_text(yaml.dump(content, allow_unicode=True))
     return file
+
+
+@pytest.fixture
+def anta_runner(request: pytest.FixtureRequest) -> AntaRunner:
+    """AntaRunner fixture.
+
+    Must be parametrized with a dictionary containing the following keys:
+    - inventory: Inventory file name from the data directory
+    - catalog: Catalog file name from the data directory
+
+    Optional keys:
+    - manager: ResultManager instance
+    - max_concurrency: Maximum concurrency limit
+    - nofile: File descriptor limit
+    """
+    # Import must be inside fixture to prevent circular dependency from breaking CLI tests:
+    # anta.runner -> anta.cli.console -> anta.cli/* (not yet loaded) -> anta.cli.anta
path: tests/units/conftest.py
user: login: gmuloc
body: let's see if we can prevent this..
created_at: 2025-03-05T10:02:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,3 +89,64 @@ def yaml_file(request: pytest.FixtureRequest, tmp_path: Path) -> Path:
     content: dict[str, Any] = request.param
     file.write_text(yaml.dump(content, allow_unicode=True))
     return file
+
+
+@pytest.fixture
+def anta_runner(request: pytest.FixtureRequest) -> AntaRunner:
+    """AntaRunner fixture.
+
+    Must be parametrized with a dictionary containing the following keys:
+    - inventory: Inventory file name from the data directory
+    - catalog: Catalog file name from the data directory
+
+    Optional keys:
+    - manager: ResultManager instance
+    - max_concurrency: Maximum concurrency limit
+    - nofile: File descriptor limit
+    """
+    # Import must be inside fixture to prevent circular dependency from breaking CLI tests:
+    # anta.runner -> anta.cli.console -> anta.cli/* (not yet loaded) -> anta.cli.anta
+    from anta._runner import AntaRunner
+    from anta.settings import AntaRunnerSettings
+
+    if not hasattr(request, "param"):
+        msg = "anta_runner fixture requires a parameter dictionary"
+        raise ValueError(msg)
+
+    params = request.param
+
+    # Check required parameters
+    required_params = {"inventory", "catalog"}
+    missing_params = required_params - params.keys()
+    if missing_params:
+        msg = f"runner_context fixture missing required parameters: {missing_params}"
+        raise ValueError(msg)
+
+    # Build AntaRunner fields
+    runner_fields = {
+        "inventory": AntaInventory.parse(
+            filename=DATA_DIR / params["inventory"],
+            username="arista",
+            password="arista",
+        ),
+        "catalog": AntaCatalog.parse(DATA_DIR / params["catalog"]),
+        "manager": params.get("manager", None),
+    }
+
+    # Build AntaRunnerSettings fields
+    settings_fields = {}
+    if "max_concurrency" in params:
+        settings_fields["max_concurrency"] = params["max_concurrency"]
+    if "nofile" in params:
+        settings_fields["nofile"] = params["nofile"]
+
+    runner = AntaRunner(**runner_fields)
+    runner._settings = AntaRunnerSettings(**settings_fields)
+    return runner
+
+
+@pytest.fixture
+def setenvvar(monkeypatch: pytest.MonkeyPatch) -> Generator[pytest.MonkeyPatch, None, None]:
+    """Fixture to set environment variables for testing."""
+    with mock.patch.dict(os.environ, clear=True):
+        yield monkeypatch
path: tests/units/conftest.py
user: login: gmuloc
body: I feel like we are doing this in many different places in different ways - maybe let's take a look after we merge this PR
created_at: 2025-03-05T10:04:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,310 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Test anta._runner.py."""
+
+from __future__ import annotations
+
+import logging
+import os
+
+import pytest
+from pydantic import ValidationError
+
+from anta._runner import AntaRunner, AntaRunnerFilter
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSchedulingStrategy
+
+
+class TestAntaRunnerBasic:
+    """Test AntaRunner basic functionality."""
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    def test_init(self, anta_runner: AntaRunner) -> None:
+        """Test basic initialization."""
+        assert anta_runner.manager is not None
+        assert len(anta_runner.inventory.devices) == 3
+        assert len(anta_runner.catalog.tests) == 11
+        assert len(anta_runner.manager.results) == 0
+
+        # Check private attributes are initialized
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+        # Check default settings
+        assert anta_runner._settings.max_concurrency == 10000
+        assert anta_runner._settings.scheduling_strategy == "round-robin"
+        assert anta_runner._settings.scheduling_tests_per_device == 100
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_reset(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.reset method."""
+        await anta_runner.run(dry_run=True)
+
+        # After a run, the following attributes should be set
+        assert anta_runner._selected_inventory is not None
+        assert anta_runner._selected_tests is not None
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._total_tests != 0
+        assert anta_runner._potential_connections is not None
+
+        anta_runner.reset()
+
+        # After reset, the following attributes should be None
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+
+class TestAntaRunnerRun:
+    """Test AntaRunner.run method."""
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_dry_run(self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method in dry-run mode."""
+        caplog.set_level(logging.INFO)
+
+        await anta_runner.run(dry_run=True)
+
+        # In dry-run mode, the selected inventory is the original inventory
+        assert anta_runner._selected_inventory is not None
+        assert len(anta_runner._selected_inventory) == len(anta_runner.inventory)
+
+        # In dry-run mode, the inventory stats total should match the original inventory length
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._inventory_stats.total == len(anta_runner.inventory)
+
+        assert "Dry-run mode, exiting before running the tests." in caplog.records[-1].message
path: tests/units/test__runner.py
user: login: gmuloc
body: maybe lets add some assert or the manager ?
created_at: 2025-03-05T10:06:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,310 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Test anta._runner.py."""
+
+from __future__ import annotations
+
+import logging
+import os
+
+import pytest
+from pydantic import ValidationError
+
+from anta._runner import AntaRunner, AntaRunnerFilter
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSchedulingStrategy
+
+
+class TestAntaRunnerBasic:
+    """Test AntaRunner basic functionality."""
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    def test_init(self, anta_runner: AntaRunner) -> None:
+        """Test basic initialization."""
+        assert anta_runner.manager is not None
+        assert len(anta_runner.inventory.devices) == 3
+        assert len(anta_runner.catalog.tests) == 11
+        assert len(anta_runner.manager.results) == 0
+
+        # Check private attributes are initialized
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+        # Check default settings
+        assert anta_runner._settings.max_concurrency == 10000
+        assert anta_runner._settings.scheduling_strategy == "round-robin"
+        assert anta_runner._settings.scheduling_tests_per_device == 100
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_reset(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.reset method."""
+        await anta_runner.run(dry_run=True)
+
+        # After a run, the following attributes should be set
+        assert anta_runner._selected_inventory is not None
+        assert anta_runner._selected_tests is not None
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._total_tests != 0
+        assert anta_runner._potential_connections is not None
+
+        anta_runner.reset()
+
+        # After reset, the following attributes should be None
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+
+class TestAntaRunnerRun:
+    """Test AntaRunner.run method."""
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_dry_run(self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method in dry-run mode."""
+        caplog.set_level(logging.INFO)
+
+        await anta_runner.run(dry_run=True)
+
+        # In dry-run mode, the selected inventory is the original inventory
+        assert anta_runner._selected_inventory is not None
+        assert len(anta_runner._selected_inventory) == len(anta_runner.inventory)
+
+        # In dry-run mode, the inventory stats total should match the original inventory length
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._inventory_stats.total == len(anta_runner.inventory)
+
+        assert "Dry-run mode, exiting before running the tests." in caplog.records[-1].message
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_invalid_filters(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with invalid scope."""
+        with pytest.raises(ValidationError, match="1 validation error for AntaRunnerFilter"):
+            await anta_runner.run(filters=AntaRunnerFilter(devices="invalid", tests=None, tags=None), dry_run=True)  # type: ignore[arg-type]
+
+    @pytest.mark.parametrize(
+        ("anta_runner", "filters", "expected_devices", "expected_tests"),
+        [
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags=None),
+                3,
+                27,
+                id="all-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"leaf"}),
+                2,
+                6,
+                id="1-tag",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"leaf", "spine"}),
+                3,
+                9,
+                id="2-tags",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags=None),
+                3,
+                5,
+                id="filtered-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags={"leaf"}),
+                2,
+                4,
+                id="1-tag-filtered-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"invalid"}),
+                0,
+                0,
+                id="invalid-tag",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"dc1"}),
+                0,
+                0,
+                id="device-tag-no-tests",
+            ),
+        ],
+        indirect=["anta_runner"],
+    )
+    async def test_run_filters(
+        self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner, filters: AntaRunnerFilter, expected_devices: int, expected_tests: int
+    ) -> None:
+        """Test AntaRunner.run method with different filters."""
+        caplog.set_level(logging.WARNING)
+
+        await anta_runner.run(filters, dry_run=True)
+
+        # Check when all tests are filtered out
+        if expected_devices == 0 and expected_tests == 0:
+            assert anta_runner._total_tests == 0
+            assert anta_runner._selected_tests is None
+            msg = f"There are no tests matching the tags {filters.tags} to run in the current test catalog and device inventory, please verify your inputs."
+            assert msg in caplog.messages
+            return
+
+        assert anta_runner._selected_tests is not None
+        assert len(anta_runner._selected_tests) == expected_devices
+        assert sum(len(tests) for tests in anta_runner._selected_tests.values()) == expected_tests
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_multiple_runs_no_manager(self, anta_runner: AntaRunner) -> None:
+        """Test multiple runs without a ResultManager instance."""
+        assert anta_runner.manager is None
+
+        first_run_manager = await anta_runner.run(dry_run=True)
+        assert isinstance(first_run_manager, ResultManager)
+        assert len(first_run_manager.results) == 27
+
+        second_run_manager = await anta_runner.run(dry_run=True)
+        assert isinstance(second_run_manager, ResultManager)
+        assert len(second_run_manager.results) == 27
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    async def test_multiple_runs_with_manager(self, anta_runner: AntaRunner) -> None:
+        """Test multiple runs with a provided ResultManager instance."""
+        assert anta_runner.manager is not None
+
+        first_run_manager = await anta_runner.run(dry_run=True)
+        assert len(first_run_manager.results) == 27
+        assert first_run_manager.results == anta_runner.manager.results
+
+        # When a manager is provided, results from subsequent runs are appended to the manager
path: tests/units/test__runner.py
user: login: gmuloc
body: thats cool - maybe we need a way to merge managers at some point :p
created_at: 2025-03-05T10:07:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,310 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Test anta._runner.py."""
+
+from __future__ import annotations
+
+import logging
+import os
+
+import pytest
+from pydantic import ValidationError
+
+from anta._runner import AntaRunner, AntaRunnerFilter
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSchedulingStrategy
+
+
+class TestAntaRunnerBasic:
+    """Test AntaRunner basic functionality."""
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    def test_init(self, anta_runner: AntaRunner) -> None:
+        """Test basic initialization."""
+        assert anta_runner.manager is not None
+        assert len(anta_runner.inventory.devices) == 3
+        assert len(anta_runner.catalog.tests) == 11
+        assert len(anta_runner.manager.results) == 0
+
+        # Check private attributes are initialized
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+        # Check default settings
+        assert anta_runner._settings.max_concurrency == 10000
+        assert anta_runner._settings.scheduling_strategy == "round-robin"
+        assert anta_runner._settings.scheduling_tests_per_device == 100
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_reset(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.reset method."""
+        await anta_runner.run(dry_run=True)
+
+        # After a run, the following attributes should be set
+        assert anta_runner._selected_inventory is not None
+        assert anta_runner._selected_tests is not None
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._total_tests != 0
+        assert anta_runner._potential_connections is not None
+
+        anta_runner.reset()
+
+        # After reset, the following attributes should be None
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+
+class TestAntaRunnerRun:
+    """Test AntaRunner.run method."""
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_dry_run(self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method in dry-run mode."""
+        caplog.set_level(logging.INFO)
+
+        await anta_runner.run(dry_run=True)
+
+        # In dry-run mode, the selected inventory is the original inventory
+        assert anta_runner._selected_inventory is not None
+        assert len(anta_runner._selected_inventory) == len(anta_runner.inventory)
+
+        # In dry-run mode, the inventory stats total should match the original inventory length
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._inventory_stats.total == len(anta_runner.inventory)
+
+        assert "Dry-run mode, exiting before running the tests." in caplog.records[-1].message
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_invalid_filters(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with invalid scope."""
+        with pytest.raises(ValidationError, match="1 validation error for AntaRunnerFilter"):
+            await anta_runner.run(filters=AntaRunnerFilter(devices="invalid", tests=None, tags=None), dry_run=True)  # type: ignore[arg-type]
+
+    @pytest.mark.parametrize(
+        ("anta_runner", "filters", "expected_devices", "expected_tests"),
+        [
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags=None),
+                3,
+                27,
+                id="all-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"leaf"}),
+                2,
+                6,
+                id="1-tag",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"leaf", "spine"}),
+                3,
+                9,
+                id="2-tags",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags=None),
+                3,
+                5,
+                id="filtered-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags={"leaf"}),
+                2,
+                4,
+                id="1-tag-filtered-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"invalid"}),
+                0,
+                0,
+                id="invalid-tag",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"dc1"}),
+                0,
+                0,
+                id="device-tag-no-tests",
+            ),
+        ],
+        indirect=["anta_runner"],
+    )
+    async def test_run_filters(
+        self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner, filters: AntaRunnerFilter, expected_devices: int, expected_tests: int
+    ) -> None:
+        """Test AntaRunner.run method with different filters."""
+        caplog.set_level(logging.WARNING)
+
+        await anta_runner.run(filters, dry_run=True)
+
+        # Check when all tests are filtered out
+        if expected_devices == 0 and expected_tests == 0:
+            assert anta_runner._total_tests == 0
+            assert anta_runner._selected_tests is None
+            msg = f"There are no tests matching the tags {filters.tags} to run in the current test catalog and device inventory, please verify your inputs."
+            assert msg in caplog.messages
+            return
+
+        assert anta_runner._selected_tests is not None
+        assert len(anta_runner._selected_tests) == expected_devices
+        assert sum(len(tests) for tests in anta_runner._selected_tests.values()) == expected_tests
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_multiple_runs_no_manager(self, anta_runner: AntaRunner) -> None:
+        """Test multiple runs without a ResultManager instance."""
+        assert anta_runner.manager is None
+
+        first_run_manager = await anta_runner.run(dry_run=True)
+        assert isinstance(first_run_manager, ResultManager)
+        assert len(first_run_manager.results) == 27
+
+        second_run_manager = await anta_runner.run(dry_run=True)
+        assert isinstance(second_run_manager, ResultManager)
+        assert len(second_run_manager.results) == 27
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    async def test_multiple_runs_with_manager(self, anta_runner: AntaRunner) -> None:
+        """Test multiple runs with a provided ResultManager instance."""
+        assert anta_runner.manager is not None
+
+        first_run_manager = await anta_runner.run(dry_run=True)
+        assert len(first_run_manager.results) == 27
+        assert first_run_manager.results == anta_runner.manager.results
+
+        # When a manager is provided, results from subsequent runs are appended to the manager
+        second_run_manager = await anta_runner.run(dry_run=True)
+        assert len(second_run_manager.results) == 54
+        assert first_run_manager.results == second_run_manager.results
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_device_by_device_strategy(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with device-by-device scheduling strategy."""
+        manager = ResultManager()
+        anta_runner._selected_inventory = anta_runner.inventory
+        anta_runner._setup_tests(filters=AntaRunnerFilter())
+        anta_runner._settings.scheduling_strategy = AntaRunnerSchedulingStrategy.DEVICE_BY_DEVICE
+
+        # Exhaust the generator and close the coroutines
+        async for coro in anta_runner._test_generator(manager):
+            coro.close()
+
+        # Check that indices 0-8 all have name "leaf1"
+        assert all(result.name == "leaf1" for result in manager.results[0:9])
+
+        # Check that indices 9-17 all have name "leaf2"
+        assert all(result.name == "leaf2" for result in manager.results[9:18])
+
+        # Check that indices 18-26 all have name "spine1"
+        assert all(result.name == "spine1" for result in manager.results[18:26])
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_device_by_count_strategy(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with device-by-count scheduling strategy."""
+        manager = ResultManager()
+        anta_runner._selected_inventory = anta_runner.inventory
+        anta_runner._setup_tests(filters=AntaRunnerFilter())
+        anta_runner._settings.scheduling_strategy = AntaRunnerSchedulingStrategy.DEVICE_BY_COUNT
+        anta_runner._settings.scheduling_tests_per_device = 2
+
+        # Exhaust the generator and close the coroutines
+        async for coro in anta_runner._test_generator(manager):
+            coro.close()
+
+        # Check that indices 0-1 all have name "leaf1"
+        assert all(result.name == "leaf1" for result in manager.results[0:2])
+
+        # Check that indices 2-3 all have name "leaf2"
+        assert all(result.name == "leaf2" for result in manager.results[2:4])
+
+        # Check that indices 4-5 all have name "spine1"
+        assert all(result.name == "spine1" for result in manager.results[4:6])
+
+        # The last 3 results should be "leaf1", "leaf2", "spine1" since there is no more tests to run
+        assert manager.results[-3].name == "leaf1"
+        assert manager.results[-2].name == "leaf2"
+        assert manager.results[-1].name == "spine1"
path: tests/units/test__runner.py
user: login: gmuloc
body: ```suggestion
        assert [result.name for result in manager.results[-3:]] == ["leaf1", "leaf2", "spine1"]
```
created_at: 2025-03-05T10:11:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,310 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Test anta._runner.py."""
+
+from __future__ import annotations
+
+import logging
+import os
+
+import pytest
+from pydantic import ValidationError
+
+from anta._runner import AntaRunner, AntaRunnerFilter
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSchedulingStrategy
+
+
+class TestAntaRunnerBasic:
+    """Test AntaRunner basic functionality."""
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    def test_init(self, anta_runner: AntaRunner) -> None:
+        """Test basic initialization."""
+        assert anta_runner.manager is not None
+        assert len(anta_runner.inventory.devices) == 3
+        assert len(anta_runner.catalog.tests) == 11
+        assert len(anta_runner.manager.results) == 0
+
+        # Check private attributes are initialized
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+        # Check default settings
+        assert anta_runner._settings.max_concurrency == 10000
+        assert anta_runner._settings.scheduling_strategy == "round-robin"
+        assert anta_runner._settings.scheduling_tests_per_device == 100
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_reset(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.reset method."""
+        await anta_runner.run(dry_run=True)
+
+        # After a run, the following attributes should be set
+        assert anta_runner._selected_inventory is not None
+        assert anta_runner._selected_tests is not None
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._total_tests != 0
+        assert anta_runner._potential_connections is not None
+
+        anta_runner.reset()
+
+        # After reset, the following attributes should be None
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+
+class TestAntaRunnerRun:
+    """Test AntaRunner.run method."""
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_dry_run(self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method in dry-run mode."""
+        caplog.set_level(logging.INFO)
+
+        await anta_runner.run(dry_run=True)
+
+        # In dry-run mode, the selected inventory is the original inventory
+        assert anta_runner._selected_inventory is not None
+        assert len(anta_runner._selected_inventory) == len(anta_runner.inventory)
+
+        # In dry-run mode, the inventory stats total should match the original inventory length
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._inventory_stats.total == len(anta_runner.inventory)
+
+        assert "Dry-run mode, exiting before running the tests." in caplog.records[-1].message
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_invalid_filters(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with invalid scope."""
+        with pytest.raises(ValidationError, match="1 validation error for AntaRunnerFilter"):
+            await anta_runner.run(filters=AntaRunnerFilter(devices="invalid", tests=None, tags=None), dry_run=True)  # type: ignore[arg-type]
+
+    @pytest.mark.parametrize(
+        ("anta_runner", "filters", "expected_devices", "expected_tests"),
+        [
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags=None),
+                3,
+                27,
+                id="all-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"leaf"}),
+                2,
+                6,
+                id="1-tag",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"leaf", "spine"}),
+                3,
+                9,
+                id="2-tags",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags=None),
+                3,
+                5,
+                id="filtered-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags={"leaf"}),
+                2,
+                4,
+                id="1-tag-filtered-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"invalid"}),
+                0,
+                0,
+                id="invalid-tag",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"dc1"}),
+                0,
+                0,
+                id="device-tag-no-tests",
+            ),
+        ],
+        indirect=["anta_runner"],
+    )
+    async def test_run_filters(
+        self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner, filters: AntaRunnerFilter, expected_devices: int, expected_tests: int
+    ) -> None:
+        """Test AntaRunner.run method with different filters."""
+        caplog.set_level(logging.WARNING)
+
+        await anta_runner.run(filters, dry_run=True)
+
+        # Check when all tests are filtered out
+        if expected_devices == 0 and expected_tests == 0:
+            assert anta_runner._total_tests == 0
+            assert anta_runner._selected_tests is None
+            msg = f"There are no tests matching the tags {filters.tags} to run in the current test catalog and device inventory, please verify your inputs."
+            assert msg in caplog.messages
+            return
+
+        assert anta_runner._selected_tests is not None
+        assert len(anta_runner._selected_tests) == expected_devices
+        assert sum(len(tests) for tests in anta_runner._selected_tests.values()) == expected_tests
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_multiple_runs_no_manager(self, anta_runner: AntaRunner) -> None:
+        """Test multiple runs without a ResultManager instance."""
+        assert anta_runner.manager is None
+
+        first_run_manager = await anta_runner.run(dry_run=True)
+        assert isinstance(first_run_manager, ResultManager)
+        assert len(first_run_manager.results) == 27
+
+        second_run_manager = await anta_runner.run(dry_run=True)
+        assert isinstance(second_run_manager, ResultManager)
+        assert len(second_run_manager.results) == 27
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    async def test_multiple_runs_with_manager(self, anta_runner: AntaRunner) -> None:
+        """Test multiple runs with a provided ResultManager instance."""
+        assert anta_runner.manager is not None
+
+        first_run_manager = await anta_runner.run(dry_run=True)
+        assert len(first_run_manager.results) == 27
+        assert first_run_manager.results == anta_runner.manager.results
+
+        # When a manager is provided, results from subsequent runs are appended to the manager
+        second_run_manager = await anta_runner.run(dry_run=True)
+        assert len(second_run_manager.results) == 54
+        assert first_run_manager.results == second_run_manager.results
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_device_by_device_strategy(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with device-by-device scheduling strategy."""
+        manager = ResultManager()
+        anta_runner._selected_inventory = anta_runner.inventory
+        anta_runner._setup_tests(filters=AntaRunnerFilter())
+        anta_runner._settings.scheduling_strategy = AntaRunnerSchedulingStrategy.DEVICE_BY_DEVICE
+
+        # Exhaust the generator and close the coroutines
+        async for coro in anta_runner._test_generator(manager):
+            coro.close()
+
+        # Check that indices 0-8 all have name "leaf1"
+        assert all(result.name == "leaf1" for result in manager.results[0:9])
+
+        # Check that indices 9-17 all have name "leaf2"
+        assert all(result.name == "leaf2" for result in manager.results[9:18])
+
+        # Check that indices 18-26 all have name "spine1"
+        assert all(result.name == "spine1" for result in manager.results[18:26])
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_device_by_count_strategy(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with device-by-count scheduling strategy."""
+        manager = ResultManager()
+        anta_runner._selected_inventory = anta_runner.inventory
+        anta_runner._setup_tests(filters=AntaRunnerFilter())
+        anta_runner._settings.scheduling_strategy = AntaRunnerSchedulingStrategy.DEVICE_BY_COUNT
+        anta_runner._settings.scheduling_tests_per_device = 2
+
+        # Exhaust the generator and close the coroutines
+        async for coro in anta_runner._test_generator(manager):
+            coro.close()
+
+        # Check that indices 0-1 all have name "leaf1"
+        assert all(result.name == "leaf1" for result in manager.results[0:2])
+
+        # Check that indices 2-3 all have name "leaf2"
+        assert all(result.name == "leaf2" for result in manager.results[2:4])
+
+        # Check that indices 4-5 all have name "spine1"
+        assert all(result.name == "spine1" for result in manager.results[4:6])
path: tests/units/test__runner.py
user: login: gmuloc
body: ```suggestion
        device_names = ["leaf1", "leaf2", "spine1"]
        for index, result in enumerate(manager.results[:-3]):
            assert result.name == devices_name[(index % 6) // 2]
```
created_at: 2025-03-05T10:15:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,310 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Test anta._runner.py."""
+
+from __future__ import annotations
+
+import logging
+import os
+
+import pytest
+from pydantic import ValidationError
+
+from anta._runner import AntaRunner, AntaRunnerFilter
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSchedulingStrategy
+
+
+class TestAntaRunnerBasic:
+    """Test AntaRunner basic functionality."""
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    def test_init(self, anta_runner: AntaRunner) -> None:
+        """Test basic initialization."""
+        assert anta_runner.manager is not None
+        assert len(anta_runner.inventory.devices) == 3
+        assert len(anta_runner.catalog.tests) == 11
+        assert len(anta_runner.manager.results) == 0
+
+        # Check private attributes are initialized
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+        # Check default settings
+        assert anta_runner._settings.max_concurrency == 10000
+        assert anta_runner._settings.scheduling_strategy == "round-robin"
+        assert anta_runner._settings.scheduling_tests_per_device == 100
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_reset(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.reset method."""
+        await anta_runner.run(dry_run=True)
+
+        # After a run, the following attributes should be set
+        assert anta_runner._selected_inventory is not None
+        assert anta_runner._selected_tests is not None
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._total_tests != 0
+        assert anta_runner._potential_connections is not None
+
+        anta_runner.reset()
+
+        # After reset, the following attributes should be None
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+
+class TestAntaRunnerRun:
+    """Test AntaRunner.run method."""
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_dry_run(self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method in dry-run mode."""
+        caplog.set_level(logging.INFO)
+
+        await anta_runner.run(dry_run=True)
+
+        # In dry-run mode, the selected inventory is the original inventory
+        assert anta_runner._selected_inventory is not None
+        assert len(anta_runner._selected_inventory) == len(anta_runner.inventory)
+
+        # In dry-run mode, the inventory stats total should match the original inventory length
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._inventory_stats.total == len(anta_runner.inventory)
+
+        assert "Dry-run mode, exiting before running the tests." in caplog.records[-1].message
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_invalid_filters(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with invalid scope."""
+        with pytest.raises(ValidationError, match="1 validation error for AntaRunnerFilter"):
+            await anta_runner.run(filters=AntaRunnerFilter(devices="invalid", tests=None, tags=None), dry_run=True)  # type: ignore[arg-type]
+
+    @pytest.mark.parametrize(
+        ("anta_runner", "filters", "expected_devices", "expected_tests"),
+        [
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags=None),
+                3,
+                27,
+                id="all-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"leaf"}),
+                2,
+                6,
+                id="1-tag",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"leaf", "spine"}),
+                3,
+                9,
+                id="2-tags",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags=None),
+                3,
+                5,
+                id="filtered-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags={"leaf"}),
+                2,
+                4,
+                id="1-tag-filtered-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"invalid"}),
+                0,
+                0,
+                id="invalid-tag",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"dc1"}),
+                0,
+                0,
+                id="device-tag-no-tests",
+            ),
+        ],
+        indirect=["anta_runner"],
+    )
+    async def test_run_filters(
+        self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner, filters: AntaRunnerFilter, expected_devices: int, expected_tests: int
+    ) -> None:
+        """Test AntaRunner.run method with different filters."""
+        caplog.set_level(logging.WARNING)
+
+        await anta_runner.run(filters, dry_run=True)
+
+        # Check when all tests are filtered out
+        if expected_devices == 0 and expected_tests == 0:
+            assert anta_runner._total_tests == 0
+            assert anta_runner._selected_tests is None
+            msg = f"There are no tests matching the tags {filters.tags} to run in the current test catalog and device inventory, please verify your inputs."
+            assert msg in caplog.messages
+            return
+
+        assert anta_runner._selected_tests is not None
+        assert len(anta_runner._selected_tests) == expected_devices
+        assert sum(len(tests) for tests in anta_runner._selected_tests.values()) == expected_tests
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_multiple_runs_no_manager(self, anta_runner: AntaRunner) -> None:
+        """Test multiple runs without a ResultManager instance."""
+        assert anta_runner.manager is None
+
+        first_run_manager = await anta_runner.run(dry_run=True)
+        assert isinstance(first_run_manager, ResultManager)
+        assert len(first_run_manager.results) == 27
+
+        second_run_manager = await anta_runner.run(dry_run=True)
+        assert isinstance(second_run_manager, ResultManager)
+        assert len(second_run_manager.results) == 27
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    async def test_multiple_runs_with_manager(self, anta_runner: AntaRunner) -> None:
+        """Test multiple runs with a provided ResultManager instance."""
+        assert anta_runner.manager is not None
+
+        first_run_manager = await anta_runner.run(dry_run=True)
+        assert len(first_run_manager.results) == 27
+        assert first_run_manager.results == anta_runner.manager.results
+
+        # When a manager is provided, results from subsequent runs are appended to the manager
+        second_run_manager = await anta_runner.run(dry_run=True)
+        assert len(second_run_manager.results) == 54
+        assert first_run_manager.results == second_run_manager.results
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_device_by_device_strategy(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with device-by-device scheduling strategy."""
+        manager = ResultManager()
+        anta_runner._selected_inventory = anta_runner.inventory
+        anta_runner._setup_tests(filters=AntaRunnerFilter())
+        anta_runner._settings.scheduling_strategy = AntaRunnerSchedulingStrategy.DEVICE_BY_DEVICE
+
+        # Exhaust the generator and close the coroutines
+        async for coro in anta_runner._test_generator(manager):
+            coro.close()
+
+        # Check that indices 0-8 all have name "leaf1"
+        assert all(result.name == "leaf1" for result in manager.results[0:9])
+
+        # Check that indices 9-17 all have name "leaf2"
+        assert all(result.name == "leaf2" for result in manager.results[9:18])
+
+        # Check that indices 18-26 all have name "spine1"
+        assert all(result.name == "spine1" for result in manager.results[18:26])
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_device_by_count_strategy(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with device-by-count scheduling strategy."""
+        manager = ResultManager()
+        anta_runner._selected_inventory = anta_runner.inventory
+        anta_runner._setup_tests(filters=AntaRunnerFilter())
+        anta_runner._settings.scheduling_strategy = AntaRunnerSchedulingStrategy.DEVICE_BY_COUNT
+        anta_runner._settings.scheduling_tests_per_device = 2
+
+        # Exhaust the generator and close the coroutines
+        async for coro in anta_runner._test_generator(manager):
+            coro.close()
+
+        # Check that indices 0-1 all have name "leaf1"
+        assert all(result.name == "leaf1" for result in manager.results[0:2])
+
+        # Check that indices 2-3 all have name "leaf2"
+        assert all(result.name == "leaf2" for result in manager.results[2:4])
+
+        # Check that indices 4-5 all have name "spine1"
+        assert all(result.name == "spine1" for result in manager.results[4:6])
+
+        # The last 3 results should be "leaf1", "leaf2", "spine1" since there is no more tests to run
+        assert manager.results[-3].name == "leaf1"
+        assert manager.results[-2].name == "leaf2"
+        assert manager.results[-1].name == "spine1"
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_round_robin_strategy(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with round-robin scheduling strategy."""
+        manager = ResultManager()
+        anta_runner._selected_inventory = anta_runner.inventory
+        anta_runner._setup_tests(filters=AntaRunnerFilter())
+        anta_runner._settings.scheduling_strategy = AntaRunnerSchedulingStrategy.ROUND_ROBIN
+
+        # Exhaust the generator and close the coroutines
+        async for coro in anta_runner._test_generator(manager):
+            coro.close()
+
+        # Round-robin between devices
+        assert manager.results[0].name == "leaf1"
+        assert manager.results[1].name == "leaf2"
+        assert manager.results[2].name == "spine1"
+        assert manager.results[3].name == "leaf1"
+        assert manager.results[4].name == "leaf2"
+        assert manager.results[5].name == "spine1"
path: tests/units/test__runner.py
user: login: gmuloc
body: ```suggestion
        # Round-robin between devices
        device_names = ["leaf1", "leaf2", "spine1"]
        for index, result in enumerate(manager.results):
            assert result.name == devices_name[index % 3]
```
created_at: 2025-03-05T10:16:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,119 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from enum import Enum
+from typing import Any
+
+from pydantic import Field, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default value for the maximum number of concurrent tests in the event loop
+DEFAULT_MAX_CONCURRENCY = 10000
+
+# Default value for the maximum number of open file descriptors for the ANTA process
+DEFAULT_NOFILE = 16384
+
+# Default value for the test scheduling strategy
+DEFAULT_SCHEDULING_STRATEGY = "round-robin"
+
+# Default value for the number of tests to schedule per device when using the DEVICE_BY_COUNT scheduling strategy
+DEFAULT_SCHEDULING_TESTS_PER_DEVICE = 100
+
+
+class AntaRunnerSchedulingStrategy(str, Enum):
+    """Enum for the test scheduling strategies available in the ANTA runner.
+
+    * ROUND_ROBIN: Distribute tests across devices in a round-robin fashion.
+    * DEVICE_BY_DEVICE: Run all tests on a single device before moving to the next.
+    * DEVICE_BY_COUNT: Run all tests on a single device for a specified count before moving to the next.
+
+    NOTE: This could be updated to StrEnum when Python 3.11 is the minimum supported version in ANTA.
+    """
+
+    ROUND_ROBIN = "round-robin"
+    DEVICE_BY_DEVICE = "device-by-device"
+    DEVICE_BY_COUNT = "device-by-count"
+
+    def __str__(self) -> str:
+        """Override the __str__ method to return the value of the Enum, mimicking the behavior of StrEnum."""
+        return self.value
+
+
+class AntaRunnerSettings(BaseSettings):
path: anta/settings.py
user: login: gmuloc
body: not for this PR but probably we should think about making a config file down the line (though I hate interaction between config file/ CLI / env variable :p) 
created_at: 2025-03-12T13:18:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -415,3 +418,45 @@ def format_data(data: dict[str, bool]) -> str:
     "Advertised: True, Received: True, Enabled: True"
     """
     return ", ".join(f"{k.capitalize()}: {v}" for k, v in data.items())
+
+
+async def limit_concurrency(coroutines: AsyncIterator[Coroutine[Any, Any, T]], limit: int) -> AsyncIterator[asyncio.Task[T]]:
+    """Schedule a limited number of coroutines concurrently.
+
+    Inspired by: https://death.andgravity.com/limit-concurrency
+
+    Parameters
+    ----------
+    coroutines
+        An async iterator of coroutines.
+    limit
+        The maximum number of coroutines to run concurrently.
+
+    Yields
+    ------
+        Each completed task.
+    """
+    if limit <= 0:
+        msg = "Concurrency limit must be greater than 0."
+        raise RuntimeError(msg)
+
+    coros_ended = False
+    pending: set[asyncio.Task[T]] = set()
+
+    while pending or not coros_ended:
+        while len(pending) < limit and not coros_ended:
+            try:
+                # NOTE: The `anext` built-in function is not available in Python 3.9
+                coro = await coroutines.__anext__()  # pylint: disable=unnecessary-dunder-call
+            except StopAsyncIteration:  # noqa: PERF203
path: anta/tools.py
user: login: gmuloc
body: lets add a comment that we don't want to stop the loop in case an exception is raised and thats why we ignore PERF203
```suggestion
            except StopAsyncIteration:  # noqa: PERF203
```
created_at: 2025-03-12T13:35:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -527,3 +534,78 @@ def test_convert_categories(test_input: list[str], expected_raise: AbstractConte
 def test_format_data(input_data: dict[str, bool], expected_output: str) -> None:
     """Test format_data."""
     assert format_data(input_data) == expected_output
+
+
+class TestLimitConcurrency:
+    """Test limit_concurrency function."""
+
+    # Helper classes and functions for testing limit_concurrency function
+    class _EmptyGenerator:
+        """Helper class to create an empty async generator."""
+
+        def __aiter__(self) -> AsyncIterator[Coroutine[Any, Any, Any]]:
+            """Make this class an async iterator."""
+            return self
+
+        async def __anext__(self) -> Coroutine[Any, Any, Any]:
+            """Raise StopAsyncIteration."""
+            raise StopAsyncIteration
+
+    async def _mock_test_coro(self, result: Result) -> Result:
+        """Mock coroutine simulating a test."""
+        # Simulate some work
+        await asyncio.sleep(0.1)
+        return result
+
+    async def _create_test_generator(self, results: Sequence[Result]) -> AsyncGenerator[Coroutine[Any, Any, Result], None]:
+        """Create a test generator yielding mock test coroutines."""
+        for result in results:
+            yield self._mock_test_coro(result)
path: tests/units/test_tools.py
user: login: gmuloc
body: should be a pytest fixture it seems
created_at: 2025-03-12T13:38:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,508 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import get_file_descriptor_limit, get_max_concurrency
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from asyncio import Task
+    from collections.abc import AsyncGenerator, Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Track inventory filtering statistics during an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup and.
+
+    When initialized, the counters are set to zero.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int = 0
+    filtered_by_tags: int = 0
+    connection_failed: int = 0
+    established: int = 0
+
+
+class AntaRunnerScope(BaseModel):
+    """Define the scope of an ANTA test run.
+
+    The scope determines which devices and tests to include in a run, and how to
+    filter them. This class is used with the AntaRunner.run() method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If None, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If None, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When True, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If None, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    max_concurrency : int
+        Maximum number of tests that can run concurrently. Defaults from `get_max_concurrency()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    file_descriptor_limit : int
+        System file descriptor limit for connections. Defaults from `get_file_descriptor_limit()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        None if unknown, float('inf') if unlimited.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with _) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerScope
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerScope(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+    max_concurrency: int = Field(default_factory=get_max_concurrency)
+    file_descriptor_limit: int = Field(default_factory=get_file_descriptor_limit)
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = None
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+    _inventory_stats: AntaRunnerInventoryStats | None = None
+    _total_tests: int = 0
+    _potential_connections: float | None = None
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, scope: AntaRunnerScope | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        scope
+            Scope of the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        scope = scope or AntaRunnerScope()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(scope, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(scope):
+                    return manager
+
+            # Set up test coroutines
+            generator = self._setup_coroutines(manager=manager if dry_run else None)
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            async for test in generator:
+                test.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            async for result in self._run(generator):
+                manager.add(result)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, scope: AntaRunnerScope, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the CLI provided tags and devices if any
+        filtered_inventory = self.inventory.get_inventory(tags=scope.tags, devices=scope.devices) if scope.tags or scope.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
+        self._selected_inventory = filtered_inventory.get_inventory(established_only=scope.established_only)
+        connection_failed = len(filtered_inventory) - len(self._selected_inventory)
+
+        # If there are no devices in the inventory after filtering, exit
+        if not self._selected_inventory.devices:
+            # Build message parts
+            tag_msg = f"matching the tags {scope.tags} " if scope.tags else ""
+            device_msg = f" Selected devices: {scope.devices} " if scope.devices is not None else ""
+            logger.warning("No reachable device %swas found.%s", tag_msg, device_msg)
+            return False
+
+        self._inventory_stats = AntaRunnerInventoryStats(
+            total=total_devices, filtered_by_tags=filtered_by_tags, connection_failed=connection_failed, established=len(self._selected_inventory)
+        )
+        return True
+
+    def _setup_tests(self, scope: AntaRunnerScope) -> bool:
+        """Set up tests for the ANTA run."""
+        if self._selected_inventory is None:
+            msg = "The selected inventory is not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        # Build indexes for the catalog. If `tests` is set, filter the indexes based on these tests
+        self.catalog.build_indexes(filtered_tests=scope.tests)
+
+        # Using a set to avoid inserting duplicate tests
+        device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
+        total_tests = 0
+        total_connections = 0
+        unlimited_connections = False
+        all_have_connections = True
+
+        # Create the device to tests mapping from the tags and calculate connection stats
+        for device in self._selected_inventory.devices:
+            if scope.tags:
+                # If there are CLI tags, execute tests with matching tags for this device
+                if not (matching_tags := scope.tags.intersection(device.tags)):
+                    # The device does not have any selected tag, skipping
+                    continue
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(matching_tags))
+            else:
+                # If there is no CLI tags, execute all tests that do not have any tags
+                device_to_tests[device].update(self.catalog.tag_to_tests[None])
+
+                # Then add the tests with matching tags from device tags
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(device.tags))
+
+            total_tests += len(device_to_tests[device])
+
+            # Check device connections
+            if not hasattr(device, "max_connections"):
+                all_have_connections = False
+            elif device.max_connections is None:
+                unlimited_connections = True
+            else:
+                total_connections += device.max_connections
+
+        if total_tests == 0:
+            tag_msg = f" matching the tags {scope.tags} " if scope.tags else " "
+            logger.warning(
+                "There are no tests%sto run in the current test catalog and device inventory, please verify your inputs.",
+                tag_msg,
+            )
+            return False
+
+        self._selected_tests = device_to_tests
+        self._total_tests = total_tests
+        self._potential_connections = None if not all_have_connections else float("inf") if unlimited_connections else total_connections
+        return True
+
+    async def _setup_coroutines(self, *, manager: ResultManager | None = None) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """Set up test coroutines from selected tests for the ANTA run.
+
+        It creates an async generator of coroutines which are created by the `test` method of the AntaTest instances.
+        Each coroutine is a test to run.
+
+        Parameters
+        ----------
+        manager
+            An optional ResultManager instance to pre-populate with the test results. Used in `dry_run` mode.
+
+        Yields
+        ------
+            The coroutine (test) to run.
+        """
+        if self._selected_tests is None:
+            msg = "The selected tests are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        for device, test_definitions in self._selected_tests.items():
+            for test in test_definitions:
+                try:
+                    test_instance = test.test(device=device, inputs=test.inputs)
+                    if manager is not None:
+                        manager.add(test_instance.result)
+                    coroutine = test_instance.test()
+                except Exception as e:  # noqa: PERF203, BLE001
+                    # An AntaTest instance is potentially user-defined code.
+                    # We need to catch everything and exit gracefully with an error message.
+                    message = "\n".join(
+                        [
+                            f"There is an error when creating test {test.test.__module__}.{test.test.__name__}.",
+                            f"If this is not a custom test implementation: {GITHUB_SUGGESTION}",
+                        ],
+                    )
+                    anta_log_exception(e, message, logger)
+                else:
+                    yield coroutine
+
+    def _log_run_information(self, *, dry_run: bool = False) -> None:
+        """Log ANTA run information and potential resource limit warnings.
+
+        Parameters
+        ----------
+        dry_run
+            Dry-run mode flag.
+        """
+        if self._inventory_stats is None:
+            msg = "The inventory stats are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        width = min(int(console.width) - 34, len("  Total potential connections: Unlimited\n"))
+
+        # Build device information
+        device_lines = [
+            "Devices:",
+            f"  Total: {self._inventory_stats.total}",
+        ]
+        if self._inventory_stats.filtered_by_tags > 0:
+            device_lines.append(f"  Excluded by tags: {self._inventory_stats.filtered_by_tags}")
+        if self._inventory_stats.connection_failed > 0:
+            device_lines.append(f"  Failed to connect: {self._inventory_stats.connection_failed}")
+        device_lines.append(f"  Selected: {self._inventory_stats.established}{' (dry-run mode)' if dry_run else ''}")
+
+        # Build connection information
+        connections_line = (
+            ""
+            if self._potential_connections is None
+            else f"  Total potential connections: {'Unlimited' if self._potential_connections == float('inf') else self._potential_connections}\n"
+        )
+
+        run_info = (
+            f"{' ANTA NRFU Run Information ':-^{width}}\n"
+            f"{chr(10).join(device_lines)}\n"
+            f"Tests: {self._total_tests} total\n"
+            f"Limits:\n"
+            f"  Max concurrent tests: {self.max_concurrency}\n"
+            f"{connections_line}"
+            f"  Max file descriptors: {self.file_descriptor_limit}\n"
+            f"{'':-^{width}}"
+        )
+        logger.info(run_info)
+
+        # Log warnings for potential resource limits
+        if self._total_tests > self.max_concurrency:
+            logger.warning(
+                "Tests count (%s) exceeds concurrent limit (%s). Tests will be throttled. See Scaling ANTA documentation.", self._total_tests, self.max_concurrency
+            )
+        if self._potential_connections == float("inf"):
+            logger.warning(
+                "Running with unlimited connections. Connection errors may occur due to file descriptor limit (%s). See Scaling ANTA documentation.",
+                self.file_descriptor_limit,
+            )
+        elif self._potential_connections is not None and self._potential_connections > self.file_descriptor_limit:
+            logger.warning(
+                "Potential connections (%s) exceeds file descriptor limit (%s). Connection errors may occur. See Scaling ANTA documentation.",
+                self._potential_connections,
+                self.file_descriptor_limit,
+            )
+
+    async def _run(self, generator: AsyncGenerator[Coroutine[Any, Any, TestResult], None]) -> AsyncGenerator[TestResult, None]:
+        """Run tests with a concurrency limit.
+
+        This function takes an asynchronous generator of test coroutines and runs them
+        with a limit on the number of concurrent tests. It yields test results as each
+        test completes.
+
+        Inspired by: https://death.andgravity.com/limit-concurrency
+
+        Parameters
+        ----------
+        generator
+            An asynchronous generator that yields test coroutines.
+
+        Yields
+        ------
+            The result of each completed test.
+        """
+        if self.max_concurrency <= 0:
+            msg = "Concurrency limit must be greater than 0."
+            raise RuntimeError(msg)
+
+        # NOTE: The `aiter` built-in function is not available in Python 3.9
+        tests = generator.__aiter__()  # pylint: disable=unnecessary-dunder-call
+        tests_ended = False
+        tests_pending: set[Task[TestResult]] = set()
+
+        while tests_pending or not tests_ended:
+            # Add tests to the pending set until the limit is reached or no more tests are available
+            while len(tests_pending) < self.max_concurrency and not tests_ended:
+                try:
+                    # NOTE: The `anext` built-in function is not available in Python 3.9
+                    test = await tests.__anext__()  # pylint: disable=unnecessary-dunder-call
+                except StopAsyncIteration:  # noqa: PERF203
+                    tests_ended = True
+                    logger.debug("All tests have been added to the pending set.")
+                else:
+                    # Ensure the coroutine is scheduled to run and add it to the pending set
+                    tests_pending.add(asyncio.create_task(test))
+                    logger.debug("Added a test to the pending set: %s", test)
+
+            if len(tests_pending) >= self.max_concurrency:
+                logger.debug("Concurrency limit reached: %s tests running. Waiting for tests to complete.", self.max_concurrency)
+
+            if not tests_pending:
+                logger.debug("No pending tests and all tests have been processed. Exiting.")
+                return
path: anta/_runner.py
user: login: gmuloc
body: TODO for ANTA v1.4.0 - need to understand why `async for` + `break` is _sometimes_ raising this kind of errors
```
 RuntimeWarning: coroutine 'VerifyAcctConsoleMethods.test' was never awaited
  async for test in generator:
```

we want to use `async for`
created_at: 2025-03-12T14:53:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,473 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime, limit_concurrency
+
+if TYPE_CHECKING:
+    from collections.abc import AsyncGenerator, Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
path: anta/_runner.py
user: login: carl-baillargeon
body: As discussed, keeping type hints for IDE's.
created_at: 2025-03-12T15:57:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -385,15 +392,26 @@ def __init__(  # noqa: PLR0913
             message = f"'password' is required to instantiate device '{self.name}'"
             logger.error(message)
             raise ValueError(message)
+
         self.enable = enable
         self._enable_password = enable_password
-        self._session: asynceapi.Device = asynceapi.Device(host=host, port=port, username=username, password=password, proto=proto, timeout=timeout)
-        ssh_params: dict[str, Any] = {}
+
+        # Build the session settings for the `asynceapi` client
+        session_settings: dict[str, Any] = {
+            "host": host,
+            "port": port,
+            "username": username,
+            "password": password,
+            "proto": proto,
+            "timeout": timeout,
+        }
+        self._session: asynceapi.Device = asynceapi.Device(**session_settings)
path: anta/device.py
user: login: carl-baillargeon
body: As discussed, keeping type hints for IDE's.
created_at: 2025-03-12T15:57:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -415,3 +418,45 @@ def format_data(data: dict[str, bool]) -> str:
     "Advertised: True, Received: True, Enabled: True"
     """
     return ", ".join(f"{k.capitalize()}: {v}" for k, v in data.items())
+
+
+async def limit_concurrency(coroutines: AsyncIterator[Coroutine[Any, Any, T]], limit: int) -> AsyncIterator[asyncio.Task[T]]:
+    """Schedule a limited number of coroutines concurrently.
+
+    Inspired by: https://death.andgravity.com/limit-concurrency
+
+    Parameters
+    ----------
+    coroutines
+        An async iterator of coroutines.
+    limit
+        The maximum number of coroutines to run concurrently.
+
+    Yields
+    ------
+        Each completed task.
+    """
+    if limit <= 0:
+        msg = "Concurrency limit must be greater than 0."
+        raise RuntimeError(msg)
+
+    coros_ended = False
+    pending: set[asyncio.Task[T]] = set()
+
+    while pending or not coros_ended:
+        while len(pending) < limit and not coros_ended:
+            try:
+                # NOTE: The `anext` built-in function is not available in Python 3.9
+                coro = await coroutines.__anext__()  # pylint: disable=unnecessary-dunder-call
+            except StopAsyncIteration:  # noqa: PERF203
+                coros_ended = True
+            else:
+                pending.add(asyncio.create_task(coro))
+
+        if not pending:
+            return
+
+        done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
+
+        while done:
+            yield done.pop()
path: anta/tools.py
user: login: gmuloc
body: ```suggestion
    while pending or not coros_ended:
        async for test in coroutines:
            # Add tests to the pending set until the limit is reached or no more tests are available
            if len(pending) < limit:
                # Ensure the coroutine is scheduled to run and add it to the pending set
                pending.add(asyncio.create_task(test))
                logger.debug("Added a test to the pending set: %s", test)
            else:
                logger.debug("Concurrency limit reached: %s tests running. Waiting for tests to complete.", limit)
                break
        if len(pending) < limit and not coros_ended:
            coros_ended = True
            logger.debug("All tests have been added to the pending set.")
        if not pending:
            logger.debug("No pending tests and all tests have been processed. Exiting.")
            return

        done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)

        while done:
            yield done.pop()
```
created_at: 2025-03-12T16:21:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,473 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime, limit_concurrency
+
+if TYPE_CHECKING:
+    from collections.abc import AsyncGenerator, Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If `None`, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        `None` if unknown.
+    _settings : AntaRunnerSettings
+        Internal settings loaded from environment variables. See the class definition
+        in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with `_`) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerFilter
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerFilter(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = PrivateAttr(default=None)
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = PrivateAttr(default=None)
+    _inventory_stats: AntaRunnerInventoryStats | None = PrivateAttr(default=None)
+    _total_tests: int = PrivateAttr(default=0)
+    _potential_connections: float | None = PrivateAttr(default=None)
+
+    # Internal settings loaded from environment variables
+    _settings: AntaRunnerSettings = PrivateAttr(default_factory=AntaRunnerSettings)
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, filters: AntaRunnerFilter | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        filters
+            Filters for the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        filters = filters or AntaRunnerFilter()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(filters, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(filters):
+                    return manager
+
+            # Build the test generator
+            test_generator = self._test_generator(manager if dry_run else None)
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            async for test in test_generator:
+                test.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            async for result in limit_concurrency(test_generator, limit=self._settings.max_concurrency):
+                manager.add(await result)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, filters: AntaRunnerFilter, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices, filtered_by_tags=0, connection_failed=0, established=0)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the provided filters any
+        filtered_inventory = self.inventory.get_inventory(tags=filters.tags, devices=filters.devices) if filters.tags or filters.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
+        self._selected_inventory = filtered_inventory.get_inventory(established_only=filters.established_only)
+        connection_failed = len(filtered_inventory) - len(self._selected_inventory)
+
+        # If there are no devices in the inventory after filtering, exit
+        if not self._selected_inventory.devices:
+            # Build message parts
+            tag_msg = f"matching the tags {filters.tags} " if filters.tags else ""
+            device_msg = f" Selected devices: {filters.devices} " if filters.devices is not None else ""
+            logger.warning("No reachable device %swas found.%s", tag_msg, device_msg)
+            return False
+
+        self._inventory_stats = AntaRunnerInventoryStats(
+            total=total_devices, filtered_by_tags=filtered_by_tags, connection_failed=connection_failed, established=len(self._selected_inventory)
+        )
+        return True
+
+    def _setup_tests(self, filters: AntaRunnerFilter) -> bool:
+        """Set up tests for the ANTA run."""
+        if self._selected_inventory is None:
+            msg = "The selected inventory is not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        # Build indexes for the catalog. If `filters.tests` is set, filter the indexes based on these tests
+        self.catalog.build_indexes(filtered_tests=filters.tests)
+
+        # Using a set to avoid inserting duplicate tests
+        device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
+        total_tests = 0
+        total_connections = 0
+        all_have_connections = True
+
+        # Create the device to tests mapping from the tags and calculate connection stats
+        for device in self._selected_inventory.devices:
+            if filters.tags:
+                # If there are CLI tags, execute tests with matching tags for this device
+                if not (matching_tags := filters.tags.intersection(device.tags)):
+                    # The device does not have any selected tag, skipping
+                    continue
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(matching_tags))
+            else:
+                # If there is no CLI tags, execute all tests that do not have any tags
+                device_to_tests[device].update(self.catalog.tag_to_tests[None])
+
+                # Then add the tests with matching tags from device tags
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(device.tags))
+
+            total_tests += len(device_to_tests[device])
+
+            # Check device connections
+            if not hasattr(device, "max_connections") or device.max_connections is None:
+                all_have_connections = False
+            else:
+                total_connections += device.max_connections
+
+        if total_tests == 0:
+            tag_msg = f" matching the tags {filters.tags} " if filters.tags else " "
+            logger.warning(
+                "There are no tests%sto run in the current test catalog and device inventory, please verify your inputs.",
+                tag_msg,
+            )
+            return False
+
+        self._selected_tests = device_to_tests
+        self._total_tests = total_tests
+        self._potential_connections = None if not all_have_connections else total_connections
+        return True
+
+    async def _test_generator(self, manager: ResultManager | None = None) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """Generate test coroutines for the ANTA run."""
+        if self._selected_tests is None:
+            msg = "The selected tests are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        logger.debug("ANTA run scheduling strategy: %s", self._settings.scheduling_strategy)
+
+        device_to_tests: dict[AntaDevice, list[AntaTestDefinition]] = {
+            device: sorted(tests, key=lambda td: td.test.__name__) for device, tests in self._selected_tests.items()
+        }
+
+        if self._settings.scheduling_strategy == "device-by-device":
+            async for coro in self._generate_device_by_device(device_to_tests, manager):
+                yield coro
+        elif self._settings.scheduling_strategy == "device-by-count":
+            async for coro in self._generate_device_by_count(device_to_tests, self._settings.scheduling_tests_per_device, manager):
+                yield coro
+        else:
+            # Default to round-robin
+            async for coro in self._generate_round_robin(device_to_tests, manager):
+                yield coro
+
+    async def _generate_round_robin(
+        self, device_to_tests: dict[AntaDevice, list[AntaTestDefinition]], manager: ResultManager | None = None
+    ) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """Yield one test per device in each round."""
+        while any(device_to_tests.values()):
+            for device, tests in device_to_tests.items():
+                if tests:
+                    test_def = tests.pop(0)
+                    coro = self._create_test_coroutine(test_def, device, manager)
+                    if coro is not None:
+                        yield coro
+
+    async def _generate_device_by_device(
+        self, device_to_tests: dict[AntaDevice, list[AntaTestDefinition]], manager: ResultManager | None = None
+    ) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """Yield all tests for one device before moving to the next."""
+        for device, tests in device_to_tests.items():
+            while tests:
+                test_def = tests.pop(0)
+                coro = self._create_test_coroutine(test_def, device, manager)
+                if coro is not None:
+                    yield coro
+
+    async def _generate_device_by_count(
+        self,
+        device_to_tests: dict[AntaDevice, list[AntaTestDefinition]],
+        tests_per_device: int,
+        manager: ResultManager | None = None,
+    ) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """In each round, yield up to `tests_per_device` tests for each device."""
+        while any(device_to_tests.values()):
+            for device, tests in device_to_tests.items():
+                count = min(tests_per_device, len(tests))
+                for _ in range(count):
+                    test_def = tests.pop(0)
+                    coro = self._create_test_coroutine(test_def, device, manager)
+                    if coro is not None:
+                        yield coro
+
+    def _create_test_coroutine(
+        self, test_def: AntaTestDefinition, device: AntaDevice, manager: ResultManager | None = None
+    ) -> Coroutine[Any, Any, TestResult] | None:
+        """Create a test coroutine from a test definition."""
+        try:
+            test_instance = test_def.test(device=device, inputs=test_def.inputs)
+            if manager is not None:
+                manager.add(test_instance.result)
+            coroutine = test_instance.test()
+        except Exception as e:  # noqa: BLE001
+            # An AntaTest instance is potentially user-defined code.
+            # We need to catch everything and exit gracefully with an error message.
+            message = "\n".join(
+                [
+                    f"There is an error when creating test {test_def.test.__module__}.{test_def.test.__name__}.",
+                    f"If this is not a custom test implementation: {GITHUB_SUGGESTION}",
+                ],
+            )
+            anta_log_exception(e, message, logger)
+            return None
+        return coroutine
+
+    def _log_run_information(self, *, dry_run: bool = False) -> None:
+        """Log ANTA run information and potential resource limit warnings."""
+        if self._inventory_stats is None:
+            msg = "The inventory stats are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        width = min(int(console.width) - 34, len("  Total potential connections: 100000000\n"))
path: anta/_runner.py
user: login: carl-baillargeon
body: Agreed, let's do it in a separate PR.
created_at: 2025-03-12T21:46:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -415,3 +418,45 @@ def format_data(data: dict[str, bool]) -> str:
     "Advertised: True, Received: True, Enabled: True"
     """
     return ", ".join(f"{k.capitalize()}: {v}" for k, v in data.items())
+
+
+async def limit_concurrency(coroutines: AsyncIterator[Coroutine[Any, Any, T]], limit: int) -> AsyncIterator[asyncio.Task[T]]:
+    """Schedule a limited number of coroutines concurrently.
+
+    Inspired by: https://death.andgravity.com/limit-concurrency
+
+    Parameters
+    ----------
+    coroutines
+        An async iterator of coroutines.
+    limit
+        The maximum number of coroutines to run concurrently.
+
+    Yields
+    ------
+        Each completed task.
+    """
+    if limit <= 0:
+        msg = "Concurrency limit must be greater than 0."
+        raise RuntimeError(msg)
+
+    coros_ended = False
+    pending: set[asyncio.Task[T]] = set()
+
+    while pending or not coros_ended:
+        while len(pending) < limit and not coros_ended:
+            try:
+                # NOTE: The `anext` built-in function is not available in Python 3.9
+                coro = await coroutines.__anext__()  # pylint: disable=unnecessary-dunder-call
+            except StopAsyncIteration:  # noqa: PERF203
path: anta/tools.py
user: login: carl-baillargeon
body: Done.
created_at: 2025-03-12T21:49:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +43,39 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **10000** tests concurrently. This default is a balance between performance and stability, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA schedules the first 10000 tests and waits for some tests to complete before scheduling more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system's CPU usage, memory consumption, and file descriptor limits.
+
+    !!! warning
+
+        Increasing the maximum concurrency limit can lead to system instability if the system is not able to handle the increased load. Monitor system resources and adjust the limit accordingly.
+
+    !!! info "Device Connection Limits"
+
+        Each device is limited to a maximum of **100** concurrent connections. This means that, even if ANTA schedules a high number of tests, each device will only attempt to open up to 100 connections at a time. Furthermore, Arista EOS eAPI is inherently protected against overload and is designed to handle high connection volumes safely.
+
+    ANTA also offers several test scheduling strategies to optimize test execution, particularly relevant when the total number of tests exceeds the maximum concurrency limit. It is configurable via the `ANTA_SCHEDULING_STRATEGY` environment variable (default is `round-robin`), along with `ANTA_SCHEDULING_TESTS_PER_DEVICE` (default is **100**) for the `device-by-count` strategy:
+
+    - **Round-robin (`round-robin`)**: Distributes tests evenly across devices. This is generally suitable for small to medium-sized (around 200 devices) fabrics but can open many simultaneous connections.
+    - **Device-by-Device (`device-by-device`)**: Executes all tests for one device before moving on to the next, which may help reduce peak concurrent connections.
+    - **Device-by-Count (`device-by-count`)**: Limits the number of tests scheduled per device in each round. This provides finer control in larger environments where opening too many connections simultaneously might exceed system limits.
path: docs/faq.md
user: login: carl-baillargeon
body: Yes our team is building a lab internally to test this.
created_at: 2025-03-12T21:50:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -527,3 +534,78 @@ def test_convert_categories(test_input: list[str], expected_raise: AbstractConte
 def test_format_data(input_data: dict[str, bool], expected_output: str) -> None:
     """Test format_data."""
     assert format_data(input_data) == expected_output
+
+
+class TestLimitConcurrency:
+    """Test limit_concurrency function."""
+
+    # Helper classes and functions for testing limit_concurrency function
+    class _EmptyGenerator:
+        """Helper class to create an empty async generator."""
+
+        def __aiter__(self) -> AsyncIterator[Coroutine[Any, Any, Any]]:
+            """Make this class an async iterator."""
+            return self
+
+        async def __anext__(self) -> Coroutine[Any, Any, Any]:
+            """Raise StopAsyncIteration."""
+            raise StopAsyncIteration
+
+    async def _mock_test_coro(self, result: Result) -> Result:
+        """Mock coroutine simulating a test."""
+        # Simulate some work
+        await asyncio.sleep(0.1)
+        return result
+
+    async def _create_test_generator(self, results: Sequence[Result]) -> AsyncGenerator[Coroutine[Any, Any, Result], None]:
+        """Create a test generator yielding mock test coroutines."""
+        for result in results:
+            yield self._mock_test_coro(result)
path: tests/units/test_tools.py
user: login: carl-baillargeon
body: Yes I will add/improve unit tests for the runner in a separate PR.
created_at: 2025-03-12T21:53:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,310 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Test anta._runner.py."""
+
+from __future__ import annotations
+
+import logging
+import os
+
+import pytest
+from pydantic import ValidationError
+
+from anta._runner import AntaRunner, AntaRunnerFilter
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSchedulingStrategy
+
+
+class TestAntaRunnerBasic:
+    """Test AntaRunner basic functionality."""
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    def test_init(self, anta_runner: AntaRunner) -> None:
+        """Test basic initialization."""
+        assert anta_runner.manager is not None
+        assert len(anta_runner.inventory.devices) == 3
+        assert len(anta_runner.catalog.tests) == 11
+        assert len(anta_runner.manager.results) == 0
+
+        # Check private attributes are initialized
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+        # Check default settings
+        assert anta_runner._settings.max_concurrency == 10000
+        assert anta_runner._settings.scheduling_strategy == "round-robin"
+        assert anta_runner._settings.scheduling_tests_per_device == 100
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_reset(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.reset method."""
+        await anta_runner.run(dry_run=True)
+
+        # After a run, the following attributes should be set
+        assert anta_runner._selected_inventory is not None
+        assert anta_runner._selected_tests is not None
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._total_tests != 0
+        assert anta_runner._potential_connections is not None
+
+        anta_runner.reset()
+
+        # After reset, the following attributes should be None
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+
+class TestAntaRunnerRun:
+    """Test AntaRunner.run method."""
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_dry_run(self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method in dry-run mode."""
+        caplog.set_level(logging.INFO)
+
+        await anta_runner.run(dry_run=True)
+
+        # In dry-run mode, the selected inventory is the original inventory
+        assert anta_runner._selected_inventory is not None
+        assert len(anta_runner._selected_inventory) == len(anta_runner.inventory)
+
+        # In dry-run mode, the inventory stats total should match the original inventory length
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._inventory_stats.total == len(anta_runner.inventory)
+
+        assert "Dry-run mode, exiting before running the tests." in caplog.records[-1].message
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_invalid_filters(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with invalid scope."""
+        with pytest.raises(ValidationError, match="1 validation error for AntaRunnerFilter"):
+            await anta_runner.run(filters=AntaRunnerFilter(devices="invalid", tests=None, tags=None), dry_run=True)  # type: ignore[arg-type]
+
+    @pytest.mark.parametrize(
+        ("anta_runner", "filters", "expected_devices", "expected_tests"),
+        [
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags=None),
+                3,
+                27,
+                id="all-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"leaf"}),
+                2,
+                6,
+                id="1-tag",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"leaf", "spine"}),
+                3,
+                9,
+                id="2-tags",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags=None),
+                3,
+                5,
+                id="filtered-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags={"leaf"}),
+                2,
+                4,
+                id="1-tag-filtered-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"invalid"}),
+                0,
+                0,
+                id="invalid-tag",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"dc1"}),
+                0,
+                0,
+                id="device-tag-no-tests",
+            ),
+        ],
+        indirect=["anta_runner"],
+    )
+    async def test_run_filters(
+        self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner, filters: AntaRunnerFilter, expected_devices: int, expected_tests: int
+    ) -> None:
+        """Test AntaRunner.run method with different filters."""
+        caplog.set_level(logging.WARNING)
+
+        await anta_runner.run(filters, dry_run=True)
+
+        # Check when all tests are filtered out
+        if expected_devices == 0 and expected_tests == 0:
+            assert anta_runner._total_tests == 0
+            assert anta_runner._selected_tests is None
+            msg = f"There are no tests matching the tags {filters.tags} to run in the current test catalog and device inventory, please verify your inputs."
+            assert msg in caplog.messages
+            return
+
+        assert anta_runner._selected_tests is not None
+        assert len(anta_runner._selected_tests) == expected_devices
+        assert sum(len(tests) for tests in anta_runner._selected_tests.values()) == expected_tests
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_multiple_runs_no_manager(self, anta_runner: AntaRunner) -> None:
+        """Test multiple runs without a ResultManager instance."""
+        assert anta_runner.manager is None
+
+        first_run_manager = await anta_runner.run(dry_run=True)
+        assert isinstance(first_run_manager, ResultManager)
+        assert len(first_run_manager.results) == 27
+
+        second_run_manager = await anta_runner.run(dry_run=True)
+        assert isinstance(second_run_manager, ResultManager)
+        assert len(second_run_manager.results) == 27
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    async def test_multiple_runs_with_manager(self, anta_runner: AntaRunner) -> None:
+        """Test multiple runs with a provided ResultManager instance."""
+        assert anta_runner.manager is not None
+
+        first_run_manager = await anta_runner.run(dry_run=True)
+        assert len(first_run_manager.results) == 27
+        assert first_run_manager.results == anta_runner.manager.results
+
+        # When a manager is provided, results from subsequent runs are appended to the manager
+        second_run_manager = await anta_runner.run(dry_run=True)
+        assert len(second_run_manager.results) == 54
+        assert first_run_manager.results == second_run_manager.results
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_device_by_device_strategy(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with device-by-device scheduling strategy."""
+        manager = ResultManager()
+        anta_runner._selected_inventory = anta_runner.inventory
+        anta_runner._setup_tests(filters=AntaRunnerFilter())
+        anta_runner._settings.scheduling_strategy = AntaRunnerSchedulingStrategy.DEVICE_BY_DEVICE
+
+        # Exhaust the generator and close the coroutines
+        async for coro in anta_runner._test_generator(manager):
+            coro.close()
+
+        # Check that indices 0-8 all have name "leaf1"
+        assert all(result.name == "leaf1" for result in manager.results[0:9])
+
+        # Check that indices 9-17 all have name "leaf2"
+        assert all(result.name == "leaf2" for result in manager.results[9:18])
+
+        # Check that indices 18-26 all have name "spine1"
+        assert all(result.name == "spine1" for result in manager.results[18:26])
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_device_by_count_strategy(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with device-by-count scheduling strategy."""
+        manager = ResultManager()
+        anta_runner._selected_inventory = anta_runner.inventory
+        anta_runner._setup_tests(filters=AntaRunnerFilter())
+        anta_runner._settings.scheduling_strategy = AntaRunnerSchedulingStrategy.DEVICE_BY_COUNT
+        anta_runner._settings.scheduling_tests_per_device = 2
+
+        # Exhaust the generator and close the coroutines
+        async for coro in anta_runner._test_generator(manager):
+            coro.close()
+
+        # Check that indices 0-1 all have name "leaf1"
+        assert all(result.name == "leaf1" for result in manager.results[0:2])
+
+        # Check that indices 2-3 all have name "leaf2"
+        assert all(result.name == "leaf2" for result in manager.results[2:4])
+
+        # Check that indices 4-5 all have name "spine1"
+        assert all(result.name == "spine1" for result in manager.results[4:6])
+
+        # The last 3 results should be "leaf1", "leaf2", "spine1" since there is no more tests to run
+        assert manager.results[-3].name == "leaf1"
+        assert manager.results[-2].name == "leaf2"
+        assert manager.results[-1].name == "spine1"
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_round_robin_strategy(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with round-robin scheduling strategy."""
+        manager = ResultManager()
+        anta_runner._selected_inventory = anta_runner.inventory
+        anta_runner._setup_tests(filters=AntaRunnerFilter())
+        anta_runner._settings.scheduling_strategy = AntaRunnerSchedulingStrategy.ROUND_ROBIN
+
+        # Exhaust the generator and close the coroutines
+        async for coro in anta_runner._test_generator(manager):
+            coro.close()
+
+        # Round-robin between devices
+        assert manager.results[0].name == "leaf1"
+        assert manager.results[1].name == "leaf2"
+        assert manager.results[2].name == "spine1"
+        assert manager.results[3].name == "leaf1"
+        assert manager.results[4].name == "leaf2"
+        assert manager.results[5].name == "spine1"
path: tests/units/test__runner.py
user: login: carl-baillargeon
body: Done.
created_at: 2025-03-12T21:54:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,310 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Test anta._runner.py."""
+
+from __future__ import annotations
+
+import logging
+import os
+
+import pytest
+from pydantic import ValidationError
+
+from anta._runner import AntaRunner, AntaRunnerFilter
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSchedulingStrategy
+
+
+class TestAntaRunnerBasic:
+    """Test AntaRunner basic functionality."""
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    def test_init(self, anta_runner: AntaRunner) -> None:
+        """Test basic initialization."""
+        assert anta_runner.manager is not None
+        assert len(anta_runner.inventory.devices) == 3
+        assert len(anta_runner.catalog.tests) == 11
+        assert len(anta_runner.manager.results) == 0
+
+        # Check private attributes are initialized
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+        # Check default settings
+        assert anta_runner._settings.max_concurrency == 10000
+        assert anta_runner._settings.scheduling_strategy == "round-robin"
+        assert anta_runner._settings.scheduling_tests_per_device == 100
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_reset(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.reset method."""
+        await anta_runner.run(dry_run=True)
+
+        # After a run, the following attributes should be set
+        assert anta_runner._selected_inventory is not None
+        assert anta_runner._selected_tests is not None
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._total_tests != 0
+        assert anta_runner._potential_connections is not None
+
+        anta_runner.reset()
+
+        # After reset, the following attributes should be None
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+
+class TestAntaRunnerRun:
+    """Test AntaRunner.run method."""
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_dry_run(self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method in dry-run mode."""
+        caplog.set_level(logging.INFO)
+
+        await anta_runner.run(dry_run=True)
+
+        # In dry-run mode, the selected inventory is the original inventory
+        assert anta_runner._selected_inventory is not None
+        assert len(anta_runner._selected_inventory) == len(anta_runner.inventory)
+
+        # In dry-run mode, the inventory stats total should match the original inventory length
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._inventory_stats.total == len(anta_runner.inventory)
+
+        assert "Dry-run mode, exiting before running the tests." in caplog.records[-1].message
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_invalid_filters(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with invalid scope."""
+        with pytest.raises(ValidationError, match="1 validation error for AntaRunnerFilter"):
+            await anta_runner.run(filters=AntaRunnerFilter(devices="invalid", tests=None, tags=None), dry_run=True)  # type: ignore[arg-type]
+
+    @pytest.mark.parametrize(
+        ("anta_runner", "filters", "expected_devices", "expected_tests"),
+        [
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags=None),
+                3,
+                27,
+                id="all-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"leaf"}),
+                2,
+                6,
+                id="1-tag",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"leaf", "spine"}),
+                3,
+                9,
+                id="2-tags",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags=None),
+                3,
+                5,
+                id="filtered-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags={"leaf"}),
+                2,
+                4,
+                id="1-tag-filtered-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"invalid"}),
+                0,
+                0,
+                id="invalid-tag",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"dc1"}),
+                0,
+                0,
+                id="device-tag-no-tests",
+            ),
+        ],
+        indirect=["anta_runner"],
+    )
+    async def test_run_filters(
+        self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner, filters: AntaRunnerFilter, expected_devices: int, expected_tests: int
+    ) -> None:
+        """Test AntaRunner.run method with different filters."""
+        caplog.set_level(logging.WARNING)
+
+        await anta_runner.run(filters, dry_run=True)
+
+        # Check when all tests are filtered out
+        if expected_devices == 0 and expected_tests == 0:
+            assert anta_runner._total_tests == 0
+            assert anta_runner._selected_tests is None
+            msg = f"There are no tests matching the tags {filters.tags} to run in the current test catalog and device inventory, please verify your inputs."
+            assert msg in caplog.messages
+            return
+
+        assert anta_runner._selected_tests is not None
+        assert len(anta_runner._selected_tests) == expected_devices
+        assert sum(len(tests) for tests in anta_runner._selected_tests.values()) == expected_tests
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_multiple_runs_no_manager(self, anta_runner: AntaRunner) -> None:
+        """Test multiple runs without a ResultManager instance."""
+        assert anta_runner.manager is None
+
+        first_run_manager = await anta_runner.run(dry_run=True)
+        assert isinstance(first_run_manager, ResultManager)
+        assert len(first_run_manager.results) == 27
+
+        second_run_manager = await anta_runner.run(dry_run=True)
+        assert isinstance(second_run_manager, ResultManager)
+        assert len(second_run_manager.results) == 27
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    async def test_multiple_runs_with_manager(self, anta_runner: AntaRunner) -> None:
+        """Test multiple runs with a provided ResultManager instance."""
+        assert anta_runner.manager is not None
+
+        first_run_manager = await anta_runner.run(dry_run=True)
+        assert len(first_run_manager.results) == 27
+        assert first_run_manager.results == anta_runner.manager.results
+
+        # When a manager is provided, results from subsequent runs are appended to the manager
+        second_run_manager = await anta_runner.run(dry_run=True)
+        assert len(second_run_manager.results) == 54
+        assert first_run_manager.results == second_run_manager.results
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_device_by_device_strategy(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with device-by-device scheduling strategy."""
+        manager = ResultManager()
+        anta_runner._selected_inventory = anta_runner.inventory
+        anta_runner._setup_tests(filters=AntaRunnerFilter())
+        anta_runner._settings.scheduling_strategy = AntaRunnerSchedulingStrategy.DEVICE_BY_DEVICE
+
+        # Exhaust the generator and close the coroutines
+        async for coro in anta_runner._test_generator(manager):
+            coro.close()
+
+        # Check that indices 0-8 all have name "leaf1"
+        assert all(result.name == "leaf1" for result in manager.results[0:9])
+
+        # Check that indices 9-17 all have name "leaf2"
+        assert all(result.name == "leaf2" for result in manager.results[9:18])
+
+        # Check that indices 18-26 all have name "spine1"
+        assert all(result.name == "spine1" for result in manager.results[18:26])
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_device_by_count_strategy(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with device-by-count scheduling strategy."""
+        manager = ResultManager()
+        anta_runner._selected_inventory = anta_runner.inventory
+        anta_runner._setup_tests(filters=AntaRunnerFilter())
+        anta_runner._settings.scheduling_strategy = AntaRunnerSchedulingStrategy.DEVICE_BY_COUNT
+        anta_runner._settings.scheduling_tests_per_device = 2
+
+        # Exhaust the generator and close the coroutines
+        async for coro in anta_runner._test_generator(manager):
+            coro.close()
+
+        # Check that indices 0-1 all have name "leaf1"
+        assert all(result.name == "leaf1" for result in manager.results[0:2])
+
+        # Check that indices 2-3 all have name "leaf2"
+        assert all(result.name == "leaf2" for result in manager.results[2:4])
+
+        # Check that indices 4-5 all have name "spine1"
+        assert all(result.name == "spine1" for result in manager.results[4:6])
path: tests/units/test__runner.py
user: login: carl-baillargeon
body: Done.
created_at: 2025-03-12T21:54:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +43,39 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **10000** tests concurrently. This default is a balance between performance and stability, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA schedules the first 10000 tests and waits for some tests to complete before scheduling more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system's CPU usage, memory consumption, and file descriptor limits.
+
+    !!! warning
+
+        Increasing the maximum concurrency limit can lead to system instability if the system is not able to handle the increased load. Monitor system resources and adjust the limit accordingly.
+
+    !!! info "Device Connection Limits"
+
+        Each device is limited to a maximum of **100** concurrent connections. This means that, even if ANTA schedules a high number of tests, each device will only attempt to open up to 100 connections at a time. Furthermore, Arista EOS eAPI is inherently protected against overload and is designed to handle high connection volumes safely.
+
+    ANTA also offers several test scheduling strategies to optimize test execution, particularly relevant when the total number of tests exceeds the maximum concurrency limit. It is configurable via the `ANTA_SCHEDULING_STRATEGY` environment variable (default is `round-robin`), along with `ANTA_SCHEDULING_TESTS_PER_DEVICE` (default is **100**) for the `device-by-count` strategy:
path: docs/faq.md
user: login: carl-baillargeon
body: Done.
created_at: 2025-03-12T21:55:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +43,39 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **10000** tests concurrently. This default is a balance between performance and stability, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA schedules the first 10000 tests and waits for some tests to complete before scheduling more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system's CPU usage, memory consumption, and file descriptor limits.
+
+    !!! warning
+
+        Increasing the maximum concurrency limit can lead to system instability if the system is not able to handle the increased load. Monitor system resources and adjust the limit accordingly.
+
+    !!! info "Device Connection Limits"
+
+        Each device is limited to a maximum of **100** concurrent connections. This means that, even if ANTA schedules a high number of tests, each device will only attempt to open up to 100 connections at a time. Furthermore, Arista EOS eAPI is inherently protected against overload and is designed to handle high connection volumes safely.
+
+    ANTA also offers several test scheduling strategies to optimize test execution, particularly relevant when the total number of tests exceeds the maximum concurrency limit. It is configurable via the `ANTA_SCHEDULING_STRATEGY` environment variable (default is `round-robin`), along with `ANTA_SCHEDULING_TESTS_PER_DEVICE` (default is **100**) for the `device-by-count` strategy:
+
+    - **Round-robin (`round-robin`)**: Distributes tests evenly across devices. This is generally suitable for small to medium-sized (around 200 devices) fabrics but can open many simultaneous connections.
+    - **Device-by-Device (`device-by-device`)**: Executes all tests for one device before moving on to the next, which may help reduce peak concurrent connections.
+    - **Device-by-Count (`device-by-count`)**: Limits the number of tests scheduled per device in each round. This provides finer control in larger environments where opening too many connections simultaneously might exceed system limits.
+
+    **Recommendation:** If you're running ANTA on a large fabric or encounter issues related to resource limits, considering tuning these settings. Test different configurations to find the optimal balance for your system.
path: docs/faq.md
user: login: carl-baillargeon
body: Done.
created_at: 2025-03-12T21:55:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,3 +89,64 @@ def yaml_file(request: pytest.FixtureRequest, tmp_path: Path) -> Path:
     content: dict[str, Any] = request.param
     file.write_text(yaml.dump(content, allow_unicode=True))
     return file
+
+
+@pytest.fixture
+def anta_runner(request: pytest.FixtureRequest) -> AntaRunner:
+    """AntaRunner fixture.
+
+    Must be parametrized with a dictionary containing the following keys:
+    - inventory: Inventory file name from the data directory
+    - catalog: Catalog file name from the data directory
+
+    Optional keys:
+    - manager: ResultManager instance
+    - max_concurrency: Maximum concurrency limit
+    - nofile: File descriptor limit
+    """
+    # Import must be inside fixture to prevent circular dependency from breaking CLI tests:
+    # anta.runner -> anta.cli.console -> anta.cli/* (not yet loaded) -> anta.cli.anta
path: tests/units/conftest.py
user: login: carl-baillargeon
body: Yes separate PR.
created_at: 2025-03-12T21:56:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,3 +59,19 @@ def bench() -> list[Coroutine[Any, Any, TestResult]]:
 
     count = sum(len(tests) for tests in selected_tests.values())
     assert count == len(coroutines)
+
+
+def test_setup_tests(benchmark: BenchmarkFixture, catalog: AntaCatalog, inventory: AntaInventory) -> None:
+    """Benchmark `anta._runner.AntaRunner._setup_tests`."""
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+    runner._selected_inventory = inventory
+
+    def bench() -> bool:
path: tests/benchmark/test_runner.py
user: login: carl-baillargeon
body: Fixed.
created_at: 2025-03-12T21:56:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,310 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Test anta._runner.py."""
+
+from __future__ import annotations
+
+import logging
+import os
+
+import pytest
+from pydantic import ValidationError
+
+from anta._runner import AntaRunner, AntaRunnerFilter
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSchedulingStrategy
+
+
+class TestAntaRunnerBasic:
+    """Test AntaRunner basic functionality."""
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    def test_init(self, anta_runner: AntaRunner) -> None:
+        """Test basic initialization."""
+        assert anta_runner.manager is not None
+        assert len(anta_runner.inventory.devices) == 3
+        assert len(anta_runner.catalog.tests) == 11
+        assert len(anta_runner.manager.results) == 0
+
+        # Check private attributes are initialized
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+        # Check default settings
+        assert anta_runner._settings.max_concurrency == 10000
+        assert anta_runner._settings.scheduling_strategy == "round-robin"
+        assert anta_runner._settings.scheduling_tests_per_device == 100
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_reset(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.reset method."""
+        await anta_runner.run(dry_run=True)
+
+        # After a run, the following attributes should be set
+        assert anta_runner._selected_inventory is not None
+        assert anta_runner._selected_tests is not None
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._total_tests != 0
+        assert anta_runner._potential_connections is not None
+
+        anta_runner.reset()
+
+        # After reset, the following attributes should be None
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+
+class TestAntaRunnerRun:
+    """Test AntaRunner.run method."""
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_dry_run(self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method in dry-run mode."""
+        caplog.set_level(logging.INFO)
+
+        await anta_runner.run(dry_run=True)
+
+        # In dry-run mode, the selected inventory is the original inventory
+        assert anta_runner._selected_inventory is not None
+        assert len(anta_runner._selected_inventory) == len(anta_runner.inventory)
+
+        # In dry-run mode, the inventory stats total should match the original inventory length
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._inventory_stats.total == len(anta_runner.inventory)
+
+        assert "Dry-run mode, exiting before running the tests." in caplog.records[-1].message
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_invalid_filters(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with invalid scope."""
+        with pytest.raises(ValidationError, match="1 validation error for AntaRunnerFilter"):
+            await anta_runner.run(filters=AntaRunnerFilter(devices="invalid", tests=None, tags=None), dry_run=True)  # type: ignore[arg-type]
+
+    @pytest.mark.parametrize(
+        ("anta_runner", "filters", "expected_devices", "expected_tests"),
+        [
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags=None),
+                3,
+                27,
+                id="all-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"leaf"}),
+                2,
+                6,
+                id="1-tag",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"leaf", "spine"}),
+                3,
+                9,
+                id="2-tags",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags=None),
+                3,
+                5,
+                id="filtered-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests={"VerifyMlagStatus", "VerifyUptime"}, tags={"leaf"}),
+                2,
+                4,
+                id="1-tag-filtered-tests",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"invalid"}),
+                0,
+                0,
+                id="invalid-tag",
+            ),
+            pytest.param(
+                {"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"},
+                AntaRunnerFilter(devices=None, tests=None, tags={"dc1"}),
+                0,
+                0,
+                id="device-tag-no-tests",
+            ),
+        ],
+        indirect=["anta_runner"],
+    )
+    async def test_run_filters(
+        self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner, filters: AntaRunnerFilter, expected_devices: int, expected_tests: int
+    ) -> None:
+        """Test AntaRunner.run method with different filters."""
+        caplog.set_level(logging.WARNING)
+
+        await anta_runner.run(filters, dry_run=True)
+
+        # Check when all tests are filtered out
+        if expected_devices == 0 and expected_tests == 0:
+            assert anta_runner._total_tests == 0
+            assert anta_runner._selected_tests is None
+            msg = f"There are no tests matching the tags {filters.tags} to run in the current test catalog and device inventory, please verify your inputs."
+            assert msg in caplog.messages
+            return
+
+        assert anta_runner._selected_tests is not None
+        assert len(anta_runner._selected_tests) == expected_devices
+        assert sum(len(tests) for tests in anta_runner._selected_tests.values()) == expected_tests
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_multiple_runs_no_manager(self, anta_runner: AntaRunner) -> None:
+        """Test multiple runs without a ResultManager instance."""
+        assert anta_runner.manager is None
+
+        first_run_manager = await anta_runner.run(dry_run=True)
+        assert isinstance(first_run_manager, ResultManager)
+        assert len(first_run_manager.results) == 27
+
+        second_run_manager = await anta_runner.run(dry_run=True)
+        assert isinstance(second_run_manager, ResultManager)
+        assert len(second_run_manager.results) == 27
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    async def test_multiple_runs_with_manager(self, anta_runner: AntaRunner) -> None:
+        """Test multiple runs with a provided ResultManager instance."""
+        assert anta_runner.manager is not None
+
+        first_run_manager = await anta_runner.run(dry_run=True)
+        assert len(first_run_manager.results) == 27
+        assert first_run_manager.results == anta_runner.manager.results
+
+        # When a manager is provided, results from subsequent runs are appended to the manager
+        second_run_manager = await anta_runner.run(dry_run=True)
+        assert len(second_run_manager.results) == 54
+        assert first_run_manager.results == second_run_manager.results
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_device_by_device_strategy(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with device-by-device scheduling strategy."""
+        manager = ResultManager()
+        anta_runner._selected_inventory = anta_runner.inventory
+        anta_runner._setup_tests(filters=AntaRunnerFilter())
+        anta_runner._settings.scheduling_strategy = AntaRunnerSchedulingStrategy.DEVICE_BY_DEVICE
+
+        # Exhaust the generator and close the coroutines
+        async for coro in anta_runner._test_generator(manager):
+            coro.close()
+
+        # Check that indices 0-8 all have name "leaf1"
+        assert all(result.name == "leaf1" for result in manager.results[0:9])
+
+        # Check that indices 9-17 all have name "leaf2"
+        assert all(result.name == "leaf2" for result in manager.results[9:18])
+
+        # Check that indices 18-26 all have name "spine1"
+        assert all(result.name == "spine1" for result in manager.results[18:26])
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_device_by_count_strategy(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method with device-by-count scheduling strategy."""
+        manager = ResultManager()
+        anta_runner._selected_inventory = anta_runner.inventory
+        anta_runner._setup_tests(filters=AntaRunnerFilter())
+        anta_runner._settings.scheduling_strategy = AntaRunnerSchedulingStrategy.DEVICE_BY_COUNT
+        anta_runner._settings.scheduling_tests_per_device = 2
+
+        # Exhaust the generator and close the coroutines
+        async for coro in anta_runner._test_generator(manager):
+            coro.close()
+
+        # Check that indices 0-1 all have name "leaf1"
+        assert all(result.name == "leaf1" for result in manager.results[0:2])
+
+        # Check that indices 2-3 all have name "leaf2"
+        assert all(result.name == "leaf2" for result in manager.results[2:4])
+
+        # Check that indices 4-5 all have name "spine1"
+        assert all(result.name == "spine1" for result in manager.results[4:6])
+
+        # The last 3 results should be "leaf1", "leaf2", "spine1" since there is no more tests to run
+        assert manager.results[-3].name == "leaf1"
+        assert manager.results[-2].name == "leaf2"
+        assert manager.results[-1].name == "spine1"
path: tests/units/test__runner.py
user: login: carl-baillargeon
body: Fixed.
created_at: 2025-03-12T21:56:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,310 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Test anta._runner.py."""
+
+from __future__ import annotations
+
+import logging
+import os
+
+import pytest
+from pydantic import ValidationError
+
+from anta._runner import AntaRunner, AntaRunnerFilter
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSchedulingStrategy
+
+
+class TestAntaRunnerBasic:
+    """Test AntaRunner basic functionality."""
+
+    @pytest.mark.parametrize(
+        ("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml", "manager": ResultManager()}], indirect=True
+    )
+    def test_init(self, anta_runner: AntaRunner) -> None:
+        """Test basic initialization."""
+        assert anta_runner.manager is not None
+        assert len(anta_runner.inventory.devices) == 3
+        assert len(anta_runner.catalog.tests) == 11
+        assert len(anta_runner.manager.results) == 0
+
+        # Check private attributes are initialized
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+        # Check default settings
+        assert anta_runner._settings.max_concurrency == 10000
+        assert anta_runner._settings.scheduling_strategy == "round-robin"
+        assert anta_runner._settings.scheduling_tests_per_device == 100
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_reset(self, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.reset method."""
+        await anta_runner.run(dry_run=True)
+
+        # After a run, the following attributes should be set
+        assert anta_runner._selected_inventory is not None
+        assert anta_runner._selected_tests is not None
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._total_tests != 0
+        assert anta_runner._potential_connections is not None
+
+        anta_runner.reset()
+
+        # After reset, the following attributes should be None
+        assert anta_runner._selected_inventory is None
+        assert anta_runner._selected_tests is None
+        assert anta_runner._inventory_stats is None
+        assert anta_runner._total_tests == 0
+        assert anta_runner._potential_connections is None
+
+
+class TestAntaRunnerRun:
+    """Test AntaRunner.run method."""
+
+    @pytest.mark.parametrize(("anta_runner"), [{"inventory": "test_inventory_with_tags.yml", "catalog": "test_catalog_with_tags.yml"}], indirect=True)
+    async def test_run_dry_run(self, caplog: pytest.LogCaptureFixture, anta_runner: AntaRunner) -> None:
+        """Test AntaRunner.run method in dry-run mode."""
+        caplog.set_level(logging.INFO)
+
+        await anta_runner.run(dry_run=True)
+
+        # In dry-run mode, the selected inventory is the original inventory
+        assert anta_runner._selected_inventory is not None
+        assert len(anta_runner._selected_inventory) == len(anta_runner.inventory)
+
+        # In dry-run mode, the inventory stats total should match the original inventory length
+        assert anta_runner._inventory_stats is not None
+        assert anta_runner._inventory_stats.total == len(anta_runner.inventory)
+
+        assert "Dry-run mode, exiting before running the tests." in caplog.records[-1].message
path: tests/units/test__runner.py
user: login: carl-baillargeon
body: The manager is tested with `test_multiple_runs_no_manager` and `test_multiple_runs_with_manager`.
created_at: 2025-03-12T21:58:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +43,39 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **10000** tests concurrently. This default is a balance between performance and stability, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA schedules the first 10000 tests and waits for some tests to complete before scheduling more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system's CPU usage, memory consumption, and file descriptor limits.
+
+    !!! warning
+
+        Increasing the maximum concurrency limit can lead to system instability if the system is not able to handle the increased load. Monitor system resources and adjust the limit accordingly.
+
+    !!! info "Device Connection Limits"
+
+        Each device is limited to a maximum of **100** concurrent connections. This means that, even if ANTA schedules a high number of tests, each device will only attempt to open up to 100 connections at a time. Furthermore, Arista EOS eAPI is inherently protected against overload and is designed to handle high connection volumes safely.
path: docs/faq.md
user: login: carl-baillargeon
body: Fixed.
created_at: 2025-03-12T21:58:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +43,39 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **10000** tests concurrently. This default is a balance between performance and stability, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA schedules the first 10000 tests and waits for some tests to complete before scheduling more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system's CPU usage, memory consumption, and file descriptor limits.
path: docs/faq.md
user: login: carl-baillargeon
body: Fixed.
created_at: 2025-03-12T21:58:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,6 +50,7 @@ def run_tests(ctx: click.Context) -> None:
 
     print_settings(inventory, catalog)
     with anta_progress_bar() as AntaTest.progress:
+        # TODO: Use AntaRunner in ANTA v2.0.0
path: anta/cli/nrfu/utils.py
user: login: carl-baillargeon
body: Fixed.
created_at: 2025-03-12T21:58:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -329,9 +337,9 @@ def __init__(  # noqa: PLR0913
         name: str | None = None,
         enable_password: str | None = None,
         port: int | None = None,
-        ssh_port: int | None = 22,
+        ssh_port: int = 22,
         tags: set[str] | None = None,
-        timeout: float | None = None,
+        timeout: float | None = 30.0,
path: anta/device.py
user: login: carl-baillargeon
body: Reverted back.
created_at: 2025-03-12T21:59:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -442,6 +461,14 @@ def _keys(self) -> tuple[Any, ...]:
         """
         return (self._session.host, self._session.port)
 
+    @property
+    def max_connections(self) -> int | None:
+        """Maximum number of concurrent connections allowed by the device. Returns None if not available."""
+        try:
+            return attrgetter("_transport._pool._max_connections")(self._session)
path: anta/device.py
user: login: carl-baillargeon
body: Updated but we need to ignore mypy and ruff :)
created_at: 2025-03-12T22:00:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -385,15 +392,26 @@ def __init__(  # noqa: PLR0913
             message = f"'password' is required to instantiate device '{self.name}'"
             logger.error(message)
             raise ValueError(message)
+
         self.enable = enable
         self._enable_password = enable_password
-        self._session: asynceapi.Device = asynceapi.Device(host=host, port=port, username=username, password=password, proto=proto, timeout=timeout)
-        ssh_params: dict[str, Any] = {}
+
+        # Build the session settings for the `asynceapi` client
+        session_settings: dict[str, Any] = {
+            "host": host,
+            "port": port,
+            "username": username,
+            "password": password,
+            "proto": proto,
+            "timeout": timeout,
+        }
+        self._session: asynceapi.Device = asynceapi.Device(**session_settings)
+
+        # Build the SSH connection options
path: anta/device.py
user: login: carl-baillargeon
body: Old stuff. Reverted back.
created_at: 2025-03-12T22:00:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -385,15 +392,26 @@ def __init__(  # noqa: PLR0913
             message = f"'password' is required to instantiate device '{self.name}'"
             logger.error(message)
             raise ValueError(message)
+
         self.enable = enable
         self._enable_password = enable_password
-        self._session: asynceapi.Device = asynceapi.Device(host=host, port=port, username=username, password=password, proto=proto, timeout=timeout)
-        ssh_params: dict[str, Any] = {}
+
+        # Build the session settings for the `asynceapi` client
+        session_settings: dict[str, Any] = {
path: anta/device.py
user: login: carl-baillargeon
body: No, reverted back.
created_at: 2025-03-12T22:00:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,494 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import get_file_descriptor_limit, get_max_concurrency
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from asyncio import Task
+    from collections.abc import AsyncGenerator, Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics for an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerScope(BaseModel):
+    """Define the scope of an ANTA test run.
+
+    The scope determines which devices and tests to include in a run, and how to
+    filter them. This class is used with the AntaRunner.run() method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If None, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If None, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When True, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If None, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    max_concurrency : int
+        Maximum number of tests that can run concurrently. Defaults from `get_max_concurrency()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    file_descriptor_limit : int
+        System file descriptor limit for connections. Defaults from `get_file_descriptor_limit()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        None if unknown, float('inf') if unlimited.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with _) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerScope
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerScope(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+    max_concurrency: int = Field(default_factory=get_max_concurrency)
+    file_descriptor_limit: int = Field(default_factory=get_file_descriptor_limit)
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = None
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+    _inventory_stats: AntaRunnerInventoryStats | None = None
+    _total_tests: int = 0
+    _potential_connections: float | None = None
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, scope: AntaRunnerScope | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        scope
+            Scope of the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        scope = scope or AntaRunnerScope()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(scope, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(scope):
+                    return manager
+
+            # Set up test coroutines
+            generator = self._setup_coroutines(manager=manager if dry_run else None)
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            async for test in generator:
+                test.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            async for result in self._run(generator):
+                manager.add(result)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, scope: AntaRunnerScope, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices, filtered_by_tags=0, connection_failed=0, established=0)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the CLI provided tags and devices if any
+        filtered_inventory = self.inventory.get_inventory(tags=scope.tags, devices=scope.devices) if scope.tags or scope.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
+        self._selected_inventory = filtered_inventory.get_inventory(established_only=scope.established_only)
+        connection_failed = len(filtered_inventory) - len(self._selected_inventory)
+
+        # If there are no devices in the inventory after filtering, exit
+        if not self._selected_inventory.devices:
+            # Build message parts
+            tag_msg = f"matching the tags {scope.tags} " if scope.tags else ""
+            device_msg = f" Selected devices: {scope.devices} " if scope.devices is not None else ""
+            logger.warning("No reachable device %swas found.%s", tag_msg, device_msg)
+            return False
+
+        self._inventory_stats = AntaRunnerInventoryStats(
+            total=total_devices, filtered_by_tags=filtered_by_tags, connection_failed=connection_failed, established=len(self._selected_inventory)
+        )
+        return True
+
+    def _setup_tests(self, scope: AntaRunnerScope) -> bool:
+        """Set up tests for the ANTA run."""
+        if self._selected_inventory is None:
+            msg = "The selected inventory is not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        # Build indexes for the catalog. If `tests` is set, filter the indexes based on these tests
+        self.catalog.build_indexes(filtered_tests=scope.tests)
+
+        # Using a set to avoid inserting duplicate tests
+        device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
+        total_tests = 0
+        total_connections = 0
+        all_have_connections = True
+
+        # Create the device to tests mapping from the tags and calculate connection stats
+        for device in self._selected_inventory.devices:
+            if scope.tags:
+                # If there are CLI tags, execute tests with matching tags for this device
+                if not (matching_tags := scope.tags.intersection(device.tags)):
+                    # The device does not have any selected tag, skipping
+                    continue
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(matching_tags))
+            else:
+                # If there is no CLI tags, execute all tests that do not have any tags
+                device_to_tests[device].update(self.catalog.tag_to_tests[None])
+
+                # Then add the tests with matching tags from device tags
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(device.tags))
+
+            total_tests += len(device_to_tests[device])
+
+            # Check device connections
+            if not hasattr(device, "max_connections") or device.max_connections is None:
+                all_have_connections = False
+            else:
+                total_connections += device.max_connections
+
+        if total_tests == 0:
+            tag_msg = f" matching the tags {scope.tags} " if scope.tags else " "
+            logger.warning(
+                "There are no tests%sto run in the current test catalog and device inventory, please verify your inputs.",
+                tag_msg,
+            )
+            return False
+
+        self._selected_tests = device_to_tests
+        self._total_tests = total_tests
+        self._potential_connections = None if not all_have_connections else total_connections
+        return True
+
+    async def _setup_coroutines(self, *, manager: ResultManager | None = None) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """Set up test coroutines from selected tests for the ANTA run.
+
+        It creates an async generator of coroutines which are created by the `test` method of the AntaTest instances.
+        Each coroutine is a test to run.
+
+        Parameters
+        ----------
+        manager
+            An optional ResultManager instance to pre-populate with the test results. Used in `dry_run` mode.
path: anta/_runner.py
user: login: carl-baillargeon
body: Fixed.
created_at: 2025-03-12T22:00:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,6 +62,9 @@ def run_tests(ctx: click.Context) -> None:
                 dry_run=dry_run,
             )
         )
+        # AntaRunner returns test results as they finish, not necessarily in order
+        # TODO: Add CLI option to choose sorting order
+        ctx.obj["result_manager"].sort(sort_by=["name", "test", "result", "custom_field"])
path: anta/cli/nrfu/utils.py
user: login: gmuloc
body: do we want this in this PR? (other perf impact I would think)
created_at: 2025-03-13T09:26:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -415,3 +418,45 @@ def format_data(data: dict[str, bool]) -> str:
     "Advertised: True, Received: True, Enabled: True"
     """
     return ", ".join(f"{k.capitalize()}: {v}" for k, v in data.items())
+
+
+async def limit_concurrency(coroutines: AsyncIterator[Coroutine[Any, Any, T]], limit: int) -> AsyncIterator[asyncio.Task[T]]:
+    """Schedule a limited number of coroutines concurrently.
+
+    Inspired by: https://death.andgravity.com/limit-concurrency
+
+    Parameters
+    ----------
+    coroutines
+        An async iterator of coroutines.
+    limit
+        The maximum number of coroutines to run concurrently.
+
+    Yields
+    ------
+        Each completed task.
+    """
+    if limit <= 0:
+        msg = "Concurrency limit must be greater than 0."
+        raise RuntimeError(msg)
+
+    coros_ended = False
+    pending: set[asyncio.Task[T]] = set()
+
+    while pending or not coros_ended:
+        while len(pending) < limit and not coros_ended:
+            try:
+                # NOTE: The `anext` built-in function is not available in Python 3.9
+                coro = await coroutines.__anext__()  # pylint: disable=unnecessary-dunder-call
+            except StopAsyncIteration:  # noqa: PERF203
+                coros_ended = True
+            else:
+                pending.add(asyncio.create_task(coro))
+
+        if not pending:
+            return
+
+        done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
+
+        while done:
+            yield done.pop()
path: anta/tools.py
user: login: gmuloc
body: after tshooting - the issue in this code is that in the else block we have already consumed the coroutine (test) from the generator but we have not scheduled it as a task so it is never scheduled - hence the warning about a coro not being ever awaited.
the warning just give the name of the coro and by essence python does not duplicate warnings so it is a bit hard to track but the solution is to schedule the task as well in the else block before breaking (and adjust the if / else logic as needed)
created_at: 2025-03-14T09:34:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,6 +62,9 @@ def run_tests(ctx: click.Context) -> None:
                 dry_run=dry_run,
             )
         )
+        # AntaRunner returns test results as they finish, not necessarily in order
+        # TODO: Add CLI option to choose sorting order
+        ctx.obj["result_manager"].sort(sort_by=["name", "test", "result", "custom_field"])
path: anta/cli/nrfu/utils.py
user: login: carl-baillargeon
body: Removed.
created_at: 2025-03-17T14:48:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,25 +30,47 @@
 FAKE_CATALOG: AntaCatalog = AntaCatalog.from_list([(FakeTest, None)])
 
 
+# TODO: Move this to AntaRunner tests in ANTA v2.0.0
 async def test_empty_tests(caplog: pytest.LogCaptureFixture, inventory: AntaInventory) -> None:
     """Test that when the list of tests is empty, a log is raised."""
     caplog.set_level(logging.INFO)
     manager = ResultManager()
     await main(manager, inventory, AntaCatalog())
 
-    assert len(caplog.record_tuples) == 1
-    assert "The list of tests is empty, exiting" in caplog.records[0].message
+    # On Windows, there is an extra log message when AntaRunner is initialized and
+    # tries to adjust the file descriptor limit.
+    if os.name != "posix":
+        record_tuples = 2
+        record_index = 1
+    else:
+        record_tuples = 1
+        record_index = 0
 
+    assert len(caplog.record_tuples) == record_tuples
+    assert "The list of tests is empty, exiting" in caplog.records[record_index].message
 
+
+# TODO: Move this to AntaRunner tests in ANTA v2.0.0
 async def test_empty_inventory(caplog: pytest.LogCaptureFixture) -> None:
     """Test that when the Inventory is empty, a log is raised."""
     caplog.set_level(logging.INFO)
     manager = ResultManager()
     await main(manager, AntaInventory(), FAKE_CATALOG)
-    assert len(caplog.record_tuples) == 3
-    assert "The inventory is empty, exiting" in caplog.records[1].message
+
+    # On Windows, there is an extra log message when AntaRunner is initialized and
+    # tries to adjust the file descriptor limit.
+    if os.name != "posix":
+        record_tuples = 4
+        record_index = 2
+    else:
+        record_tuples = 3
+        record_index = 1
+
+    assert len(caplog.record_tuples) == record_tuples
path: tests/units/test_runner.py
user: login: gmuloc
body: lets remove this and just check that "The inventory is empty" is in any record?
created_at: 2025-04-09T13:24:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,25 +30,47 @@
 FAKE_CATALOG: AntaCatalog = AntaCatalog.from_list([(FakeTest, None)])
 
 
+# TODO: Move this to AntaRunner tests in ANTA v2.0.0
 async def test_empty_tests(caplog: pytest.LogCaptureFixture, inventory: AntaInventory) -> None:
     """Test that when the list of tests is empty, a log is raised."""
     caplog.set_level(logging.INFO)
     manager = ResultManager()
     await main(manager, inventory, AntaCatalog())
 
-    assert len(caplog.record_tuples) == 1
-    assert "The list of tests is empty, exiting" in caplog.records[0].message
+    # On Windows, there is an extra log message when AntaRunner is initialized and
+    # tries to adjust the file descriptor limit.
+    if os.name != "posix":
+        record_tuples = 2
+        record_index = 1
+    else:
+        record_tuples = 1
+        record_index = 0
 
+    assert len(caplog.record_tuples) == record_tuples
+    assert "The list of tests is empty, exiting" in caplog.records[record_index].message
 
+
+# TODO: Move this to AntaRunner tests in ANTA v2.0.0
 async def test_empty_inventory(caplog: pytest.LogCaptureFixture) -> None:
     """Test that when the Inventory is empty, a log is raised."""
     caplog.set_level(logging.INFO)
     manager = ResultManager()
     await main(manager, AntaInventory(), FAKE_CATALOG)
-    assert len(caplog.record_tuples) == 3
-    assert "The inventory is empty, exiting" in caplog.records[1].message
+
+    # On Windows, there is an extra log message when AntaRunner is initialized and
+    # tries to adjust the file descriptor limit.
+    if os.name != "posix":
+        record_tuples = 4
+        record_index = 2
+    else:
+        record_tuples = 3
+        record_index = 1
+
+    assert len(caplog.record_tuples) == record_tuples
path: tests/units/test_runner.py
user: login: gmuloc
body: same for above.

The counting of tuples does not really bring anything and is annoying to maintain as you have noticed :)
created_at: 2025-04-09T13:36:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +43,35 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **50000** tests concurrently. This should be sufficient for most use cases, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA executes the first 50000 tests and waits for some tests to complete before executing more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system CPU usage, memory consumption, and file descriptor limits.
+
+    !!! warning
+
+        Increasing the maximum concurrency limit can lead to system instability if the system is not able to handle the increased load. Monitor system resources and adjust the limit accordingly.
+
+    !!! info "Device Connection Limits"
+
+        Each device is limited to a maximum of **100** concurrent connections. This means that, even if ANTA schedules a high number of tests, it will only attempt to open up to 100 connections at a time towards each device.
path: docs/faq.md
user: login: gmuloc
body: probably worth saying EOS somewhere here :)
created_at: 2025-04-09T13:41:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +43,35 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **50000** tests concurrently. This should be sufficient for most use cases, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA executes the first 50000 tests and waits for some tests to complete before executing more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system CPU usage, memory consumption, and file descriptor limits.
+
+    !!! warning
+
+        Increasing the maximum concurrency limit can lead to system instability if the system is not able to handle the increased load. Monitor system resources and adjust the limit accordingly.
+
+    !!! info "Device Connection Limits"
+
+        Each device is limited to a maximum of **100** concurrent connections. This means that, even if ANTA schedules a high number of tests, it will only attempt to open up to 100 connections at a time towards each device.
+
+    !!! tip
+        If you run ANTA on a large fabric or encounter issues related to resource limits, consider tuning this setting.
path: docs/faq.md
user: login: gmuloc
body: which settings ;) ?
created_at: 2025-04-09T13:42:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from typing import Any
+
+from pydantic import Field, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default value for the maximum number of concurrent tests in the event loop
+DEFAULT_MAX_CONCURRENCY = 50000
+
+# Default value for the maximum number of open file descriptors for the ANTA process
+DEFAULT_NOFILE = 16384
path: anta/settings.py
user: login: gmuloc
body: ```suggestion
DEFAULT_MAX_CONCURRENCY = 50000
"""Default value for the maximum number of concurrent tests in the event loop."""

DEFAULT_NOFILE = 16384
"""Default value for the maximum number of open file descriptors for the ANTA process."""
```
created_at: 2025-04-09T13:43:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from typing import Any
+
+from pydantic import Field, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default value for the maximum number of concurrent tests in the event loop
+DEFAULT_MAX_CONCURRENCY = 50000
+
+# Default value for the maximum number of open file descriptors for the ANTA process
+DEFAULT_NOFILE = 16384
+
+
+class AntaRunnerSettings(BaseSettings):
+    """Environment variables for configuring the ANTA runner.
+
+    When initialized, relevant environment variables are loaded. If not set, default values are used.
+
+    On POSIX systems, also adjusts the process's soft limit based on the `ANTA_NOFILE` environment variable
+    while respecting the system's hard limit, meaning the new soft limit cannot exceed the system's hard limit.
path: anta/settings.py
user: login: gmuloc
body: ```suggestion
    while respecting the system hard limit, meaning the new soft limit cannot exceed the system hard limit.
```
created_at: 2025-04-09T13:44:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from typing import Any
+
+from pydantic import Field, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default value for the maximum number of concurrent tests in the event loop
+DEFAULT_MAX_CONCURRENCY = 50000
+
+# Default value for the maximum number of open file descriptors for the ANTA process
+DEFAULT_NOFILE = 16384
+
+
+class AntaRunnerSettings(BaseSettings):
+    """Environment variables for configuring the ANTA runner.
+
+    When initialized, relevant environment variables are loaded. If not set, default values are used.
+
+    On POSIX systems, also adjusts the process's soft limit based on the `ANTA_NOFILE` environment variable
+    while respecting the system's hard limit, meaning the new soft limit cannot exceed the system's hard limit.
+
+    On non-POSIX systems (Windows), sets the limit to `sys.maxsize`.
+
+    The adjusted limit is available with the `file_descriptor_limit` property after initialization.
+
+    Attributes
+    ----------
+    nofile : PositiveInt
+        Environment variable: ANTA_NOFILE
+
+        The maximum number of open file descriptors for the ANTA process. Defaults to 16384.
+
+    max_concurrency : PositiveInt
+        Environment variable: ANTA_MAX_CONCURRENCY
+
+        The maximum number of concurrent tests that can run in the event loop. Defaults to 50000.
+    """
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    nofile: PositiveInt = Field(default=DEFAULT_NOFILE)
+    max_concurrency: PositiveInt = Field(default=DEFAULT_MAX_CONCURRENCY)
+
+    # Computed in post-init
+    _file_descriptor_limit: PositiveInt
+
+    def model_post_init(self, __context: Any) -> None:  # noqa: ANN401, PYI063
+        """Post-initialization method to set the file descriptor limit for the current ANTA process."""
+        if os.name != "posix":
+            logger.warning("Running on a non-POSIX system, cannot adjust the maximum number of file descriptors.")
+            self._file_descriptor_limit = sys.maxsize
+            return
+
+        import resource
+
+        limits = resource.getrlimit(resource.RLIMIT_NOFILE)
+        logger.debug("Initial file descriptor limits: Soft Limit: %s | Hard Limit: %s", limits[0], limits[1])
+
+        # Set new soft limit to minimum of requested and hard limit
+        new_soft_limit = min(limits[1], self.nofile)
+        logger.debug("Setting file descriptor soft limit to %s", new_soft_limit)
+        resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft_limit, limits[1]))
+
+        self._file_descriptor_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
path: anta/settings.py
user: login: gmuloc
body: you have lost the PR catching ValueError exception from David here.
created_at: 2025-04-09T13:45:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -493,3 +494,5 @@ min-similarity-lines=10
 signature-mutators="click.decorators.option"
 load-plugins="pylint_pydantic"
 extension-pkg-whitelist="pydantic"
+# Pylint does not treat dataclasses differently: https://github.com/pylint-dev/pylint/issues/9058
+max-attributes=15
path: pyproject.toml
user: login: gmuloc
body: it does not but I think should rather use the default value and ignore the few places where pylint is unhappy because it is a data class. Though I think we can disable this pylint rule now maybe with ruff?
created_at: 2025-04-09T13:49:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Unit tests for the anta.settings module."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from unittest.mock import patch
+
+import pytest
+from pydantic import ValidationError
+
+from anta.settings import DEFAULT_MAX_CONCURRENCY, DEFAULT_NOFILE, AntaRunnerSettings
+
+if os.name == "posix":
+    # The function is not defined on non-POSIX system
+    import resource
+
+
+class TestAntaRunnerSettings:
+    """Tests for the AntaRunnerSettings class."""
+
+    def test_defaults(self, setenvvar: pytest.MonkeyPatch) -> None:
+        """Test defaults for ANTA runner settings."""
+        settings = AntaRunnerSettings()
+        assert settings.nofile == DEFAULT_NOFILE
+        assert settings.max_concurrency == DEFAULT_MAX_CONCURRENCY
+
+    def test_env_var(self, setenvvar: pytest.MonkeyPatch) -> None:
+        """Test setting different ANTA runner settings."""
+        setenvvar.setenv("ANTA_NOFILE", "20480")
+        settings = AntaRunnerSettings()
+        assert settings.nofile == 20480
+        assert settings.max_concurrency == DEFAULT_MAX_CONCURRENCY
+
+    def test_validation(self, setenvvar: pytest.MonkeyPatch) -> None:
+        """Test validation of ANTA runner settings."""
+        setenvvar.setenv("ANTA_NOFILE", "-1")
+        with pytest.raises(ValidationError):
+            AntaRunnerSettings()
+
+        setenvvar.setenv("ANTA_MAX_CONCURRENCY", "0")
+        with pytest.raises(ValidationError):
+            AntaRunnerSettings()
+
+    @pytest.mark.skipif(os.name == "posix", reason="Run this test on Windows only")
+    def test_file_descriptor_limit_windows(self, caplog: pytest.LogCaptureFixture) -> None:
path: tests/units/test_settings.py
user: login: gmuloc
body: we have lost the _fake test I think
created_at: 2025-04-09T13:50:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,117 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Unit tests for the anta.settings module."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from unittest.mock import patch
+
+import pytest
+from pydantic import ValidationError
+
+from anta.settings import DEFAULT_MAX_CONCURRENCY, DEFAULT_NOFILE, AntaRunnerSettings
+
+if os.name == "posix":
+    # The function is not defined on non-POSIX system
+    import resource
+
+
+class TestAntaRunnerSettings:
+    """Tests for the AntaRunnerSettings class."""
+
+    def test_defaults(self, setenvvar: pytest.MonkeyPatch) -> None:
+        """Test defaults for ANTA runner settings."""
+        settings = AntaRunnerSettings()
+        assert settings.nofile == DEFAULT_NOFILE
+        assert settings.max_concurrency == DEFAULT_MAX_CONCURRENCY
+
+    def test_env_var(self, setenvvar: pytest.MonkeyPatch) -> None:
+        """Test setting different ANTA runner settings."""
+        setenvvar.setenv("ANTA_NOFILE", "20480")
+        settings = AntaRunnerSettings()
+        assert settings.nofile == 20480
+        assert settings.max_concurrency == DEFAULT_MAX_CONCURRENCY
+
+    def test_validation(self, setenvvar: pytest.MonkeyPatch) -> None:
+        """Test validation of ANTA runner settings."""
+        setenvvar.setenv("ANTA_NOFILE", "-1")
+        with pytest.raises(ValidationError):
+            AntaRunnerSettings()
+
+        setenvvar.setenv("ANTA_MAX_CONCURRENCY", "0")
+        with pytest.raises(ValidationError):
+            AntaRunnerSettings()
+
+    @pytest.mark.skipif(os.name == "posix", reason="Run this test on Windows only")
+    def test_file_descriptor_limit_windows(self, caplog: pytest.LogCaptureFixture) -> None:
path: tests/units/test_settings.py
user: login: carl-baillargeon
body: There are still some tests using `main()` (which uses AntaRunner under the hood) in `test_runner.py`. I added `# TODO: Move this to AntaRunner tests in ANTA v2.0.0` on top of each.
created_at: 2025-04-09T14:37:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -493,3 +494,5 @@ min-similarity-lines=10
 signature-mutators="click.decorators.option"
 load-plugins="pylint_pydantic"
 extension-pkg-whitelist="pydantic"
+# Pylint does not treat dataclasses differently: https://github.com/pylint-dev/pylint/issues/9058
+max-attributes=15
path: pyproject.toml
user: login: carl-baillargeon
body: Not implemented yet: https://github.com/astral-sh/ruff/issues/970 check for `too-many-instance-attributes`.

I think it's cleaner to have it in pyproject since we have it in multiple places.
created_at: 2025-04-09T14:40:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from typing import Any
+
+from pydantic import Field, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default value for the maximum number of concurrent tests in the event loop
+DEFAULT_MAX_CONCURRENCY = 50000
+
+# Default value for the maximum number of open file descriptors for the ANTA process
+DEFAULT_NOFILE = 16384
+
+
+class AntaRunnerSettings(BaseSettings):
+    """Environment variables for configuring the ANTA runner.
+
+    When initialized, relevant environment variables are loaded. If not set, default values are used.
+
+    On POSIX systems, also adjusts the process's soft limit based on the `ANTA_NOFILE` environment variable
+    while respecting the system's hard limit, meaning the new soft limit cannot exceed the system's hard limit.
+
+    On non-POSIX systems (Windows), sets the limit to `sys.maxsize`.
+
+    The adjusted limit is available with the `file_descriptor_limit` property after initialization.
+
+    Attributes
+    ----------
+    nofile : PositiveInt
+        Environment variable: ANTA_NOFILE
+
+        The maximum number of open file descriptors for the ANTA process. Defaults to 16384.
+
+    max_concurrency : PositiveInt
+        Environment variable: ANTA_MAX_CONCURRENCY
+
+        The maximum number of concurrent tests that can run in the event loop. Defaults to 50000.
+    """
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    nofile: PositiveInt = Field(default=DEFAULT_NOFILE)
+    max_concurrency: PositiveInt = Field(default=DEFAULT_MAX_CONCURRENCY)
+
+    # Computed in post-init
+    _file_descriptor_limit: PositiveInt
+
+    def model_post_init(self, __context: Any) -> None:  # noqa: ANN401, PYI063
+        """Post-initialization method to set the file descriptor limit for the current ANTA process."""
+        if os.name != "posix":
+            logger.warning("Running on a non-POSIX system, cannot adjust the maximum number of file descriptors.")
+            self._file_descriptor_limit = sys.maxsize
+            return
+
+        import resource
+
+        limits = resource.getrlimit(resource.RLIMIT_NOFILE)
+        logger.debug("Initial file descriptor limits: Soft Limit: %s | Hard Limit: %s", limits[0], limits[1])
+
+        # Set new soft limit to minimum of requested and hard limit
+        new_soft_limit = min(limits[1], self.nofile)
+        logger.debug("Setting file descriptor soft limit to %s", new_soft_limit)
+        resource.setrlimit(resource.RLIMIT_NOFILE, (new_soft_limit, limits[1]))
+
+        self._file_descriptor_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
path: anta/settings.py
user: login: carl-baillargeon
body: Added in last commit + unit test for it.
created_at: 2025-04-09T14:40:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,25 +30,47 @@
 FAKE_CATALOG: AntaCatalog = AntaCatalog.from_list([(FakeTest, None)])
 
 
+# TODO: Move this to AntaRunner tests in ANTA v2.0.0
 async def test_empty_tests(caplog: pytest.LogCaptureFixture, inventory: AntaInventory) -> None:
     """Test that when the list of tests is empty, a log is raised."""
     caplog.set_level(logging.INFO)
     manager = ResultManager()
     await main(manager, inventory, AntaCatalog())
 
-    assert len(caplog.record_tuples) == 1
-    assert "The list of tests is empty, exiting" in caplog.records[0].message
+    # On Windows, there is an extra log message when AntaRunner is initialized and
+    # tries to adjust the file descriptor limit.
+    if os.name != "posix":
+        record_tuples = 2
+        record_index = 1
+    else:
+        record_tuples = 1
+        record_index = 0
 
+    assert len(caplog.record_tuples) == record_tuples
+    assert "The list of tests is empty, exiting" in caplog.records[record_index].message
 
+
+# TODO: Move this to AntaRunner tests in ANTA v2.0.0
 async def test_empty_inventory(caplog: pytest.LogCaptureFixture) -> None:
     """Test that when the Inventory is empty, a log is raised."""
     caplog.set_level(logging.INFO)
     manager = ResultManager()
     await main(manager, AntaInventory(), FAKE_CATALOG)
-    assert len(caplog.record_tuples) == 3
-    assert "The inventory is empty, exiting" in caplog.records[1].message
+
+    # On Windows, there is an extra log message when AntaRunner is initialized and
+    # tries to adjust the file descriptor limit.
+    if os.name != "posix":
+        record_tuples = 4
+        record_index = 2
+    else:
+        record_tuples = 3
+        record_index = 1
+
+    assert len(caplog.record_tuples) == record_tuples
path: tests/units/test_runner.py
user: login: carl-baillargeon
body: Done in both.
created_at: 2025-04-09T14:49:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +43,35 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **50000** tests concurrently. This should be sufficient for most use cases, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA executes the first 50000 tests and waits for some tests to complete before executing more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system CPU usage, memory consumption, and file descriptor limits.
+
+    !!! warning
+
+        Increasing the maximum concurrency limit can lead to system instability if the system is not able to handle the increased load. Monitor system resources and adjust the limit accordingly.
+
+    !!! info "Device Connection Limits"
+
+        Each device is limited to a maximum of **100** concurrent connections. This means that, even if ANTA schedules a high number of tests, it will only attempt to open up to 100 connections at a time towards each device.
path: docs/faq.md
user: login: carl-baillargeon
body: Done.
created_at: 2025-04-09T14:49:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +43,35 @@ anta_title: Frequently Asked Questions (FAQ)
     The `user` is the one with which the ANTA process is started.
     The `value` is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again.
 
+## Tests throttling WARNING in the logs
+
+???+ faq "Tests throttling `WARNING` in the logs"
+
+    ANTA is designed to execute many tests concurrently while ensuring system stability. If the total test count exceeds the maximum concurrency limit, tests are throttled to avoid overwhelming the asyncio event loop and exhausting system resources. A `WARNING` message is logged at startup when this occurs.
+
+    By default, ANTA schedules up to **50000** tests concurrently. This should be sufficient for most use cases, but it may not be optimal for every system. If the number of tests exceeds this value, ANTA executes the first 50000 tests and waits for some tests to complete before executing more.
+
+    ### Solution
+
+    You can adjust the maximum concurrency limit using the `ANTA_MAX_CONCURRENCY` environment variable. The optimal value depends on your system CPU usage, memory consumption, and file descriptor limits.
+
+    !!! warning
+
+        Increasing the maximum concurrency limit can lead to system instability if the system is not able to handle the increased load. Monitor system resources and adjust the limit accordingly.
+
+    !!! info "Device Connection Limits"
+
+        Each device is limited to a maximum of **100** concurrent connections. This means that, even if ANTA schedules a high number of tests, it will only attempt to open up to 100 connections at a time towards each device.
+
+    !!! tip
+        If you run ANTA on a large fabric or encounter issues related to resource limits, consider tuning this setting.
path: docs/faq.md
user: login: carl-baillargeon
body: Updated.
created_at: 2025-04-09T14:49:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,81 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from typing import Any
+
+from pydantic import Field, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+logger = logging.getLogger(__name__)
+
+# Default value for the maximum number of concurrent tests in the event loop
+DEFAULT_MAX_CONCURRENCY = 50000
+
+# Default value for the maximum number of open file descriptors for the ANTA process
+DEFAULT_NOFILE = 16384
+
+
+class AntaRunnerSettings(BaseSettings):
+    """Environment variables for configuring the ANTA runner.
+
+    When initialized, relevant environment variables are loaded. If not set, default values are used.
+
+    On POSIX systems, also adjusts the process's soft limit based on the `ANTA_NOFILE` environment variable
+    while respecting the system's hard limit, meaning the new soft limit cannot exceed the system's hard limit.
path: anta/settings.py
user: login: carl-baillargeon
body: Done.
created_at: 2025-04-09T14:49:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -415,3 +418,45 @@ def format_data(data: dict[str, bool]) -> str:
     "Advertised: True, Received: True, Enabled: True"
     """
     return ", ".join(f"{k.capitalize()}: {v}" for k, v in data.items())
+
+
+async def limit_concurrency(coroutines: AsyncIterator[Coroutine[Any, Any, T]], limit: int) -> AsyncIterator[asyncio.Task[T]]:
+    """Schedule a limited number of coroutines concurrently.
+
+    Inspired by: https://death.andgravity.com/limit-concurrency
+
+    Parameters
+    ----------
+    coroutines
+        An async iterator of coroutines.
+    limit
+        The maximum number of coroutines to run concurrently.
+
+    Yields
+    ------
+        Each completed task.
+    """
+    if limit <= 0:
+        msg = "Concurrency limit must be greater than 0."
+        raise RuntimeError(msg)
+
+    coros_ended = False
+    pending: set[asyncio.Task[T]] = set()
+
+    while pending or not coros_ended:
+        while len(pending) < limit and not coros_ended:
+            try:
+                # NOTE: The `anext` built-in function is not available in Python 3.9
+                coro = await coroutines.__anext__()  # pylint: disable=unnecessary-dunder-call
+            except StopAsyncIteration:  # noqa: PERF203
+                coros_ended = True
+            else:
+                pending.add(asyncio.create_task(coro))
+
+        if not pending:
+            return
+
+        done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
+
+        while done:
+            yield done.pop()
path: anta/tools.py
user: login: carl-baillargeon
body: Nice findings. Will use Semaphore instead :)
created_at: 2025-04-09T14:55:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,88 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from typing import Any
+
+from pydantic import Field, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+from anta.logger import exc_to_str
+
+logger = logging.getLogger(__name__)
+
+DEFAULT_MAX_CONCURRENCY = 50000
+"""Default value for the maximum number of concurrent tests in the event loop."""
+
+DEFAULT_NOFILE = 16384
+"""Default value for the maximum number of open file descriptors for the ANTA process."""
+
+
+class AntaRunnerSettings(BaseSettings):
+    """Environment variables for configuring the ANTA runner.
+
+    When initialized, relevant environment variables are loaded. If not set, default values are used.
+
+    On POSIX systems, also adjusts the process's soft limit based on the `ANTA_NOFILE` environment variable
path: anta/settings.py
user: login: gmuloc
body: ```suggestion
    On POSIX systems, also adjusts the process soft limit based on the `ANTA_NOFILE` environment variable
```
created_at: 2025-04-10T09:06:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,88 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from typing import Any
+
+from pydantic import Field, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+from anta.logger import exc_to_str
+
+logger = logging.getLogger(__name__)
+
+DEFAULT_MAX_CONCURRENCY = 50000
+"""Default value for the maximum number of concurrent tests in the event loop."""
+
+DEFAULT_NOFILE = 16384
+"""Default value for the maximum number of open file descriptors for the ANTA process."""
+
+
+class AntaRunnerSettings(BaseSettings):
+    """Environment variables for configuring the ANTA runner.
+
+    When initialized, relevant environment variables are loaded. If not set, default values are used.
+
+    On POSIX systems, also adjusts the process soft limit based on the `ANTA_NOFILE` environment variable
+    while respecting the system hard limit, meaning the new soft limit cannot exceed the system's hard limit.
+
+    On non-POSIX systems (Windows), sets the limit to `sys.maxsize`.
+
+    The adjusted limit is available with the `file_descriptor_limit` property after initialization.
+
+    Attributes
+    ----------
+    nofile : PositiveInt
+        Environment variable: ANTA_NOFILE
+
+        The maximum number of open file descriptors for the ANTA process. Defaults to 16384.
+
+    max_concurrency : PositiveInt
+        Environment variable: ANTA_MAX_CONCURRENCY
+
+        The maximum number of concurrent tests that can run in the event loop. Defaults to 50000.
+    """
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    nofile: PositiveInt = Field(default=DEFAULT_NOFILE)
+    max_concurrency: PositiveInt = Field(default=DEFAULT_MAX_CONCURRENCY)
+
+    # Computed in post-init
+    _file_descriptor_limit: PositiveInt
+
+    # pylint: disable=arguments-differ
+    def model_post_init(self, context: Any, /) -> None:  # noqa: ANN401
path: anta/settings.py
user: login: gmuloc
body: why forcing non keyword arguments?
```suggestion
    def model_post_init(self, context: Any) -> None:  # noqa: ANN401
```
created_at: 2025-04-10T09:09:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,88 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from typing import Any
+
+from pydantic import Field, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+from anta.logger import exc_to_str
+
+logger = logging.getLogger(__name__)
+
+DEFAULT_MAX_CONCURRENCY = 50000
+"""Default value for the maximum number of concurrent tests in the event loop."""
+
+DEFAULT_NOFILE = 16384
+"""Default value for the maximum number of open file descriptors for the ANTA process."""
+
+
+class AntaRunnerSettings(BaseSettings):
+    """Environment variables for configuring the ANTA runner.
+
+    When initialized, relevant environment variables are loaded. If not set, default values are used.
+
+    On POSIX systems, also adjusts the process soft limit based on the `ANTA_NOFILE` environment variable
+    while respecting the system hard limit, meaning the new soft limit cannot exceed the system's hard limit.
+
+    On non-POSIX systems (Windows), sets the limit to `sys.maxsize`.
+
+    The adjusted limit is available with the `file_descriptor_limit` property after initialization.
+
+    Attributes
+    ----------
+    nofile : PositiveInt
+        Environment variable: ANTA_NOFILE
+
+        The maximum number of open file descriptors for the ANTA process. Defaults to 16384.
+
+    max_concurrency : PositiveInt
+        Environment variable: ANTA_MAX_CONCURRENCY
+
+        The maximum number of concurrent tests that can run in the event loop. Defaults to 50000.
+    """
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    nofile: PositiveInt = Field(default=DEFAULT_NOFILE)
+    max_concurrency: PositiveInt = Field(default=DEFAULT_MAX_CONCURRENCY)
+
+    # Computed in post-init
+    _file_descriptor_limit: PositiveInt
+
+    # pylint: disable=arguments-differ
+    def model_post_init(self, context: Any, /) -> None:  # noqa: ANN401
+        """Post-initialization method to set the file descriptor limit for the current ANTA process."""
+        _ = context
path: anta/settings.py
user: login: gmuloc
body: ?
created_at: 2025-04-10T09:09:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
path: anta/_runner.py
user: login: gmuloc
body: does this one needs to be a BaseModel?
created_at: 2025-04-10T09:16:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If `None`, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        `None` if unknown.
+    _settings : AntaRunnerSettings
+        Internal settings loaded from environment variables. See the class definition
+        in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with `_`) are managed by the `run()` method
path: anta/_runner.py
user: login: gmuloc
body: we could look at prefixing them with `__` https://dbader.org/blog/meaning-of-underscores-in-python#:~:text=A%20double%20underscore%20prefix%20causes,the%20class%20is%20extended%20later.
created_at: 2025-04-10T09:19:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If `None`, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        `None` if unknown.
+    _settings : AntaRunnerSettings
+        Internal settings loaded from environment variables. See the class definition
+        in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with `_`) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerFilter
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerFilter(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
path: anta/_runner.py
user: login: gmuloc
body: This could be done with a regular __init__ method. I would remove the pydantic link for this class as we don't need the validation
created_at: 2025-04-10T09:20:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If `None`, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        `None` if unknown.
+    _settings : AntaRunnerSettings
+        Internal settings loaded from environment variables. See the class definition
+        in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with `_`) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerFilter
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerFilter(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = PrivateAttr(default=None)
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = PrivateAttr(default=None)
+    _inventory_stats: AntaRunnerInventoryStats | None = PrivateAttr(default=None)
+    _total_tests: int = PrivateAttr(default=0)
+    _potential_connections: float | None = PrivateAttr(default=None)
+
+    # Internal settings loaded from environment variables
+    _settings: AntaRunnerSettings = PrivateAttr(default_factory=AntaRunnerSettings)
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, filters: AntaRunnerFilter | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        filters
+            Filters for the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        filters = filters or AntaRunnerFilter()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(filters, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(filters):
+                    return manager
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+            # Build test coroutines
+            test_coroutines = self._get_test_coroutines()
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            for coro in test_coroutines:
+                # Get the AntaTest instance from the coroutine locals, can be in `args` when decorated
+                coro_locals = getcoroutinelocals(coro)
+                test = coro_locals.get("self") or coro_locals.get("args", (None))[0]
path: anta/_runner.py
user: login: gmuloc
body: hmmm maybe we are not decorating properly? I would think we could get do this properly. Not for this PR but lets investigate
created_at: 2025-04-10T09:25:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If `None`, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        `None` if unknown.
+    _settings : AntaRunnerSettings
+        Internal settings loaded from environment variables. See the class definition
+        in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with `_`) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerFilter
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerFilter(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = PrivateAttr(default=None)
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = PrivateAttr(default=None)
+    _inventory_stats: AntaRunnerInventoryStats | None = PrivateAttr(default=None)
+    _total_tests: int = PrivateAttr(default=0)
+    _potential_connections: float | None = PrivateAttr(default=None)
+
+    # Internal settings loaded from environment variables
+    _settings: AntaRunnerSettings = PrivateAttr(default_factory=AntaRunnerSettings)
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, filters: AntaRunnerFilter | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        filters
+            Filters for the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        filters = filters or AntaRunnerFilter()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(filters, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(filters):
+                    return manager
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+            # Build test coroutines
+            test_coroutines = self._get_test_coroutines()
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            for coro in test_coroutines:
+                # Get the AntaTest instance from the coroutine locals, can be in `args` when decorated
+                coro_locals = getcoroutinelocals(coro)
+                test = coro_locals.get("self") or coro_locals.get("args", (None))[0]
+                if isinstance(test, AntaTest):
+                    manager.add(test.result)
+                else:
+                    logger.error("Coroutine %s does not have an AntaTest instance.", coro)
+                coro.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            sem = Semaphore(self._settings.max_concurrency)
+
+            async def run_with_sem(test_coro: Coroutine[Any, Any, TestResult]) -> TestResult:
+                """Wrap the test coroutine with semaphore control."""
+                async with sem:
+                    return await test_coro
+
+            results = await gather(*[run_with_sem(coro) for coro in test_coroutines])
+            for res in results:
+                manager.add(res)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, filters: AntaRunnerFilter, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices, filtered_by_tags=0, connection_failed=0, established=0)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the provided filters any
+        filtered_inventory = self.inventory.get_inventory(tags=filters.tags, devices=filters.devices) if filters.tags or filters.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
path: anta/_runner.py
user: login: gmuloc
body: it depends on the value of the filter maybe lets do 

```
if filters.established_only:
  ...
```

to not call get_inventory is not useful?
created_at: 2025-04-10T09:27:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If `None`, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        `None` if unknown.
+    _settings : AntaRunnerSettings
+        Internal settings loaded from environment variables. See the class definition
+        in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with `_`) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerFilter
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerFilter(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = PrivateAttr(default=None)
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = PrivateAttr(default=None)
+    _inventory_stats: AntaRunnerInventoryStats | None = PrivateAttr(default=None)
+    _total_tests: int = PrivateAttr(default=0)
+    _potential_connections: float | None = PrivateAttr(default=None)
+
+    # Internal settings loaded from environment variables
+    _settings: AntaRunnerSettings = PrivateAttr(default_factory=AntaRunnerSettings)
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, filters: AntaRunnerFilter | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        filters
+            Filters for the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        filters = filters or AntaRunnerFilter()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(filters, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(filters):
+                    return manager
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+            # Build test coroutines
+            test_coroutines = self._get_test_coroutines()
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            for coro in test_coroutines:
+                # Get the AntaTest instance from the coroutine locals, can be in `args` when decorated
+                coro_locals = getcoroutinelocals(coro)
+                test = coro_locals.get("self") or coro_locals.get("args", (None))[0]
+                if isinstance(test, AntaTest):
+                    manager.add(test.result)
+                else:
+                    logger.error("Coroutine %s does not have an AntaTest instance.", coro)
+                coro.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            sem = Semaphore(self._settings.max_concurrency)
+
+            async def run_with_sem(test_coro: Coroutine[Any, Any, TestResult]) -> TestResult:
+                """Wrap the test coroutine with semaphore control."""
+                async with sem:
+                    return await test_coro
+
+            results = await gather(*[run_with_sem(coro) for coro in test_coroutines])
+            for res in results:
+                manager.add(res)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, filters: AntaRunnerFilter, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
path: anta/_runner.py
user: login: gmuloc
body: lets document waht the bool return means in docstring
created_at: 2025-04-10T09:30:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If `None`, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        `None` if unknown.
+    _settings : AntaRunnerSettings
+        Internal settings loaded from environment variables. See the class definition
+        in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with `_`) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerFilter
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerFilter(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = PrivateAttr(default=None)
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = PrivateAttr(default=None)
+    _inventory_stats: AntaRunnerInventoryStats | None = PrivateAttr(default=None)
+    _total_tests: int = PrivateAttr(default=0)
+    _potential_connections: float | None = PrivateAttr(default=None)
+
+    # Internal settings loaded from environment variables
+    _settings: AntaRunnerSettings = PrivateAttr(default_factory=AntaRunnerSettings)
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, filters: AntaRunnerFilter | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        filters
+            Filters for the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        filters = filters or AntaRunnerFilter()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(filters, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(filters):
+                    return manager
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+            # Build test coroutines
+            test_coroutines = self._get_test_coroutines()
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            for coro in test_coroutines:
+                # Get the AntaTest instance from the coroutine locals, can be in `args` when decorated
+                coro_locals = getcoroutinelocals(coro)
+                test = coro_locals.get("self") or coro_locals.get("args", (None))[0]
+                if isinstance(test, AntaTest):
+                    manager.add(test.result)
+                else:
+                    logger.error("Coroutine %s does not have an AntaTest instance.", coro)
+                coro.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            sem = Semaphore(self._settings.max_concurrency)
+
+            async def run_with_sem(test_coro: Coroutine[Any, Any, TestResult]) -> TestResult:
+                """Wrap the test coroutine with semaphore control."""
+                async with sem:
+                    return await test_coro
+
+            results = await gather(*[run_with_sem(coro) for coro in test_coroutines])
+            for res in results:
+                manager.add(res)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, filters: AntaRunnerFilter, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices, filtered_by_tags=0, connection_failed=0, established=0)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the provided filters any
+        filtered_inventory = self.inventory.get_inventory(tags=filters.tags, devices=filters.devices) if filters.tags or filters.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
+        self._selected_inventory = filtered_inventory.get_inventory(established_only=filters.established_only)
+        connection_failed = len(filtered_inventory) - len(self._selected_inventory)
+
+        # If there are no devices in the inventory after filtering, exit
+        if not self._selected_inventory.devices:
+            # Build message parts
+            tag_msg = f"matching the tags {filters.tags} " if filters.tags else ""
+            device_msg = f" Selected devices: {filters.devices} " if filters.devices is not None else ""
+            logger.warning("No reachable device %swas found.%s", tag_msg, device_msg)
+            return False
+
+        self._inventory_stats = AntaRunnerInventoryStats(
+            total=total_devices, filtered_by_tags=filtered_by_tags, connection_failed=connection_failed, established=len(self._selected_inventory)
+        )
+        return True
+
+    def _setup_tests(self, filters: AntaRunnerFilter) -> bool:
+        """Set up tests for the ANTA run."""
+        if self._selected_inventory is None:
+            msg = "The selected inventory is not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        # Build indexes for the catalog. If `filters.tests` is set, filter the indexes based on these tests
+        self.catalog.build_indexes(filtered_tests=filters.tests)
+
+        # Using a set to avoid inserting duplicate tests
+        device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
+        total_tests = 0
+        total_connections = 0
+        all_have_connections = True
+
+        # Create the device to tests mapping from the tags and calculate connection stats
+        for device in self._selected_inventory.devices:
+            if filters.tags:
+                # If there are CLI tags, execute tests with matching tags for this device
+                if not (matching_tags := filters.tags.intersection(device.tags)):
+                    # The device does not have any selected tag, skipping
+                    continue
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(matching_tags))
+            else:
+                # If there is no CLI tags, execute all tests that do not have any tags
+                device_to_tests[device].update(self.catalog.tag_to_tests[None])
+
+                # Then add the tests with matching tags from device tags
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(device.tags))
+
+            total_tests += len(device_to_tests[device])
+
+            # Check device connections
+            if not hasattr(device, "max_connections") or device.max_connections is None:
+                all_have_connections = False
+            else:
+                total_connections += device.max_connections
+
+        if total_tests == 0:
+            tag_msg = f" matching the tags {filters.tags} " if filters.tags else " "
+            logger.warning(
+                "There are no tests%sto run in the current test catalog and device inventory, please verify your inputs.",
+                tag_msg,
+            )
+            return False
+
+        self._selected_tests = device_to_tests
path: anta/_runner.py
user: login: gmuloc
body: Could we update `self._selected_tests` directly rather than using the intermediate `device_to_tests`?
created_at: 2025-04-10T09:35:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If `None`, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        `None` if unknown.
+    _settings : AntaRunnerSettings
+        Internal settings loaded from environment variables. See the class definition
+        in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with `_`) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerFilter
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerFilter(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = PrivateAttr(default=None)
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = PrivateAttr(default=None)
+    _inventory_stats: AntaRunnerInventoryStats | None = PrivateAttr(default=None)
+    _total_tests: int = PrivateAttr(default=0)
+    _potential_connections: float | None = PrivateAttr(default=None)
+
+    # Internal settings loaded from environment variables
+    _settings: AntaRunnerSettings = PrivateAttr(default_factory=AntaRunnerSettings)
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, filters: AntaRunnerFilter | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        filters
+            Filters for the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        filters = filters or AntaRunnerFilter()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(filters, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(filters):
+                    return manager
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+            # Build test coroutines
+            test_coroutines = self._get_test_coroutines()
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            for coro in test_coroutines:
+                # Get the AntaTest instance from the coroutine locals, can be in `args` when decorated
+                coro_locals = getcoroutinelocals(coro)
+                test = coro_locals.get("self") or coro_locals.get("args", (None))[0]
+                if isinstance(test, AntaTest):
+                    manager.add(test.result)
+                else:
+                    logger.error("Coroutine %s does not have an AntaTest instance.", coro)
+                coro.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            sem = Semaphore(self._settings.max_concurrency)
+
+            async def run_with_sem(test_coro: Coroutine[Any, Any, TestResult]) -> TestResult:
+                """Wrap the test coroutine with semaphore control."""
+                async with sem:
+                    return await test_coro
+
+            results = await gather(*[run_with_sem(coro) for coro in test_coroutines])
+            for res in results:
+                manager.add(res)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, filters: AntaRunnerFilter, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices, filtered_by_tags=0, connection_failed=0, established=0)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the provided filters any
+        filtered_inventory = self.inventory.get_inventory(tags=filters.tags, devices=filters.devices) if filters.tags or filters.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
+        self._selected_inventory = filtered_inventory.get_inventory(established_only=filters.established_only)
+        connection_failed = len(filtered_inventory) - len(self._selected_inventory)
+
+        # If there are no devices in the inventory after filtering, exit
+        if not self._selected_inventory.devices:
+            # Build message parts
+            tag_msg = f"matching the tags {filters.tags} " if filters.tags else ""
+            device_msg = f" Selected devices: {filters.devices} " if filters.devices is not None else ""
+            logger.warning("No reachable device %swas found.%s", tag_msg, device_msg)
+            return False
+
+        self._inventory_stats = AntaRunnerInventoryStats(
+            total=total_devices, filtered_by_tags=filtered_by_tags, connection_failed=connection_failed, established=len(self._selected_inventory)
+        )
+        return True
+
+    def _setup_tests(self, filters: AntaRunnerFilter) -> bool:
+        """Set up tests for the ANTA run."""
+        if self._selected_inventory is None:
+            msg = "The selected inventory is not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        # Build indexes for the catalog. If `filters.tests` is set, filter the indexes based on these tests
+        self.catalog.build_indexes(filtered_tests=filters.tests)
+
+        # Using a set to avoid inserting duplicate tests
+        device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
+        total_tests = 0
+        total_connections = 0
+        all_have_connections = True
+
+        # Create the device to tests mapping from the tags and calculate connection stats
+        for device in self._selected_inventory.devices:
+            if filters.tags:
+                # If there are CLI tags, execute tests with matching tags for this device
+                if not (matching_tags := filters.tags.intersection(device.tags)):
+                    # The device does not have any selected tag, skipping
+                    continue
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(matching_tags))
+            else:
+                # If there is no CLI tags, execute all tests that do not have any tags
+                device_to_tests[device].update(self.catalog.tag_to_tests[None])
+
+                # Then add the tests with matching tags from device tags
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(device.tags))
+
+            total_tests += len(device_to_tests[device])
+
+            # Check device connections
+            if not hasattr(device, "max_connections") or device.max_connections is None:
+                all_have_connections = False
+            else:
+                total_connections += device.max_connections
+
+        if total_tests == 0:
+            tag_msg = f" matching the tags {filters.tags} " if filters.tags else " "
+            logger.warning(
+                "There are no tests%sto run in the current test catalog and device inventory, please verify your inputs.",
+                tag_msg,
+            )
+            return False
+
+        self._selected_tests = device_to_tests
+        self._total_tests = total_tests
+        self._potential_connections = None if not all_have_connections else total_connections
path: anta/_runner.py
user: login: gmuloc
body: should this not rather be done in inventory? it does not seem to be related to tests
created_at: 2025-04-10T09:36:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,88 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from typing import Any
+
+from pydantic import Field, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+from anta.logger import exc_to_str
+
+logger = logging.getLogger(__name__)
+
+DEFAULT_MAX_CONCURRENCY = 50000
+"""Default value for the maximum number of concurrent tests in the event loop."""
+
+DEFAULT_NOFILE = 16384
+"""Default value for the maximum number of open file descriptors for the ANTA process."""
+
+
+class AntaRunnerSettings(BaseSettings):
+    """Environment variables for configuring the ANTA runner.
+
+    When initialized, relevant environment variables are loaded. If not set, default values are used.
+
+    On POSIX systems, also adjusts the process soft limit based on the `ANTA_NOFILE` environment variable
+    while respecting the system hard limit, meaning the new soft limit cannot exceed the system's hard limit.
+
+    On non-POSIX systems (Windows), sets the limit to `sys.maxsize`.
+
+    The adjusted limit is available with the `file_descriptor_limit` property after initialization.
+
+    Attributes
+    ----------
+    nofile : PositiveInt
+        Environment variable: ANTA_NOFILE
+
+        The maximum number of open file descriptors for the ANTA process. Defaults to 16384.
+
+    max_concurrency : PositiveInt
+        Environment variable: ANTA_MAX_CONCURRENCY
+
+        The maximum number of concurrent tests that can run in the event loop. Defaults to 50000.
+    """
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    nofile: PositiveInt = Field(default=DEFAULT_NOFILE)
+    max_concurrency: PositiveInt = Field(default=DEFAULT_MAX_CONCURRENCY)
+
+    # Computed in post-init
+    _file_descriptor_limit: PositiveInt
+
+    # pylint: disable=arguments-differ
+    def model_post_init(self, context: Any, /) -> None:  # noqa: ANN401
path: anta/settings.py
user: login: carl-baillargeon
body: Fixed.
created_at: 2025-05-12T15:58:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,88 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""Settings for ANTA."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from typing import Any
+
+from pydantic import Field, PositiveInt
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+from anta.logger import exc_to_str
+
+logger = logging.getLogger(__name__)
+
+DEFAULT_MAX_CONCURRENCY = 50000
+"""Default value for the maximum number of concurrent tests in the event loop."""
+
+DEFAULT_NOFILE = 16384
+"""Default value for the maximum number of open file descriptors for the ANTA process."""
+
+
+class AntaRunnerSettings(BaseSettings):
+    """Environment variables for configuring the ANTA runner.
+
+    When initialized, relevant environment variables are loaded. If not set, default values are used.
+
+    On POSIX systems, also adjusts the process soft limit based on the `ANTA_NOFILE` environment variable
+    while respecting the system hard limit, meaning the new soft limit cannot exceed the system's hard limit.
+
+    On non-POSIX systems (Windows), sets the limit to `sys.maxsize`.
+
+    The adjusted limit is available with the `file_descriptor_limit` property after initialization.
+
+    Attributes
+    ----------
+    nofile : PositiveInt
+        Environment variable: ANTA_NOFILE
+
+        The maximum number of open file descriptors for the ANTA process. Defaults to 16384.
+
+    max_concurrency : PositiveInt
+        Environment variable: ANTA_MAX_CONCURRENCY
+
+        The maximum number of concurrent tests that can run in the event loop. Defaults to 50000.
+    """
+
+    model_config = SettingsConfigDict(env_prefix="ANTA_")
+
+    nofile: PositiveInt = Field(default=DEFAULT_NOFILE)
+    max_concurrency: PositiveInt = Field(default=DEFAULT_MAX_CONCURRENCY)
+
+    # Computed in post-init
+    _file_descriptor_limit: PositiveInt
+
+    # pylint: disable=arguments-differ
+    def model_post_init(self, context: Any, /) -> None:  # noqa: ANN401
+        """Post-initialization method to set the file descriptor limit for the current ANTA process."""
+        _ = context
path: anta/settings.py
user: login: carl-baillargeon
body: Fixed.
created_at: 2025-05-12T15:58:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
path: anta/_runner.py
user: login: carl-baillargeon
body: Removed in latest commits.
created_at: 2025-05-12T15:59:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If `None`, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        `None` if unknown.
+    _settings : AntaRunnerSettings
+        Internal settings loaded from environment variables. See the class definition
+        in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with `_`) are managed by the `run()` method
path: anta/_runner.py
user: login: carl-baillargeon
body: No longer applicable in latest commits.
created_at: 2025-05-12T15:59:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If `None`, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        `None` if unknown.
+    _settings : AntaRunnerSettings
+        Internal settings loaded from environment variables. See the class definition
+        in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with `_`) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerFilter
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerFilter(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
path: anta/_runner.py
user: login: carl-baillargeon
body: Pydantic class removed.
created_at: 2025-05-12T15:59:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If `None`, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        `None` if unknown.
+    _settings : AntaRunnerSettings
+        Internal settings loaded from environment variables. See the class definition
+        in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with `_`) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerFilter
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerFilter(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = PrivateAttr(default=None)
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = PrivateAttr(default=None)
+    _inventory_stats: AntaRunnerInventoryStats | None = PrivateAttr(default=None)
+    _total_tests: int = PrivateAttr(default=0)
+    _potential_connections: float | None = PrivateAttr(default=None)
+
+    # Internal settings loaded from environment variables
+    _settings: AntaRunnerSettings = PrivateAttr(default_factory=AntaRunnerSettings)
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, filters: AntaRunnerFilter | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        filters
+            Filters for the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        filters = filters or AntaRunnerFilter()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(filters, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(filters):
+                    return manager
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+            # Build test coroutines
+            test_coroutines = self._get_test_coroutines()
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            for coro in test_coroutines:
+                # Get the AntaTest instance from the coroutine locals, can be in `args` when decorated
+                coro_locals = getcoroutinelocals(coro)
+                test = coro_locals.get("self") or coro_locals.get("args", (None))[0]
path: anta/_runner.py
user: login: carl-baillargeon
body: Leaving this as is for now as discussed live.
created_at: 2025-05-12T15:59:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If `None`, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        `None` if unknown.
+    _settings : AntaRunnerSettings
+        Internal settings loaded from environment variables. See the class definition
+        in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with `_`) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerFilter
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerFilter(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = PrivateAttr(default=None)
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = PrivateAttr(default=None)
+    _inventory_stats: AntaRunnerInventoryStats | None = PrivateAttr(default=None)
+    _total_tests: int = PrivateAttr(default=0)
+    _potential_connections: float | None = PrivateAttr(default=None)
+
+    # Internal settings loaded from environment variables
+    _settings: AntaRunnerSettings = PrivateAttr(default_factory=AntaRunnerSettings)
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, filters: AntaRunnerFilter | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        filters
+            Filters for the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        filters = filters or AntaRunnerFilter()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(filters, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(filters):
+                    return manager
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+            # Build test coroutines
+            test_coroutines = self._get_test_coroutines()
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            for coro in test_coroutines:
+                # Get the AntaTest instance from the coroutine locals, can be in `args` when decorated
+                coro_locals = getcoroutinelocals(coro)
+                test = coro_locals.get("self") or coro_locals.get("args", (None))[0]
+                if isinstance(test, AntaTest):
+                    manager.add(test.result)
+                else:
+                    logger.error("Coroutine %s does not have an AntaTest instance.", coro)
+                coro.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            sem = Semaphore(self._settings.max_concurrency)
+
+            async def run_with_sem(test_coro: Coroutine[Any, Any, TestResult]) -> TestResult:
+                """Wrap the test coroutine with semaphore control."""
+                async with sem:
+                    return await test_coro
+
+            results = await gather(*[run_with_sem(coro) for coro in test_coroutines])
+            for res in results:
+                manager.add(res)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, filters: AntaRunnerFilter, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices, filtered_by_tags=0, connection_failed=0, established=0)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the provided filters any
+        filtered_inventory = self.inventory.get_inventory(tags=filters.tags, devices=filters.devices) if filters.tags or filters.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
path: anta/_runner.py
user: login: carl-baillargeon
body: Fixed.
created_at: 2025-05-12T16:00:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If `None`, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        `None` if unknown.
+    _settings : AntaRunnerSettings
+        Internal settings loaded from environment variables. See the class definition
+        in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with `_`) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerFilter
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerFilter(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = PrivateAttr(default=None)
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = PrivateAttr(default=None)
+    _inventory_stats: AntaRunnerInventoryStats | None = PrivateAttr(default=None)
+    _total_tests: int = PrivateAttr(default=0)
+    _potential_connections: float | None = PrivateAttr(default=None)
+
+    # Internal settings loaded from environment variables
+    _settings: AntaRunnerSettings = PrivateAttr(default_factory=AntaRunnerSettings)
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, filters: AntaRunnerFilter | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        filters
+            Filters for the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        filters = filters or AntaRunnerFilter()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(filters, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(filters):
+                    return manager
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+            # Build test coroutines
+            test_coroutines = self._get_test_coroutines()
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            for coro in test_coroutines:
+                # Get the AntaTest instance from the coroutine locals, can be in `args` when decorated
+                coro_locals = getcoroutinelocals(coro)
+                test = coro_locals.get("self") or coro_locals.get("args", (None))[0]
+                if isinstance(test, AntaTest):
+                    manager.add(test.result)
+                else:
+                    logger.error("Coroutine %s does not have an AntaTest instance.", coro)
+                coro.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            sem = Semaphore(self._settings.max_concurrency)
+
+            async def run_with_sem(test_coro: Coroutine[Any, Any, TestResult]) -> TestResult:
+                """Wrap the test coroutine with semaphore control."""
+                async with sem:
+                    return await test_coro
+
+            results = await gather(*[run_with_sem(coro) for coro in test_coroutines])
+            for res in results:
+                manager.add(res)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, filters: AntaRunnerFilter, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices, filtered_by_tags=0, connection_failed=0, established=0)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the provided filters any
+        filtered_inventory = self.inventory.get_inventory(tags=filters.tags, devices=filters.devices) if filters.tags or filters.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
+        self._selected_inventory = filtered_inventory.get_inventory(established_only=filters.established_only)
+        connection_failed = len(filtered_inventory) - len(self._selected_inventory)
+
+        # If there are no devices in the inventory after filtering, exit
+        if not self._selected_inventory.devices:
+            # Build message parts
+            tag_msg = f"matching the tags {filters.tags} " if filters.tags else ""
+            device_msg = f" Selected devices: {filters.devices} " if filters.devices is not None else ""
+            logger.warning("No reachable device %swas found.%s", tag_msg, device_msg)
+            return False
+
+        self._inventory_stats = AntaRunnerInventoryStats(
+            total=total_devices, filtered_by_tags=filtered_by_tags, connection_failed=connection_failed, established=len(self._selected_inventory)
+        )
+        return True
+
+    def _setup_tests(self, filters: AntaRunnerFilter) -> bool:
+        """Set up tests for the ANTA run."""
+        if self._selected_inventory is None:
+            msg = "The selected inventory is not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        # Build indexes for the catalog. If `filters.tests` is set, filter the indexes based on these tests
+        self.catalog.build_indexes(filtered_tests=filters.tests)
+
+        # Using a set to avoid inserting duplicate tests
+        device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
+        total_tests = 0
+        total_connections = 0
+        all_have_connections = True
+
+        # Create the device to tests mapping from the tags and calculate connection stats
+        for device in self._selected_inventory.devices:
+            if filters.tags:
+                # If there are CLI tags, execute tests with matching tags for this device
+                if not (matching_tags := filters.tags.intersection(device.tags)):
+                    # The device does not have any selected tag, skipping
+                    continue
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(matching_tags))
+            else:
+                # If there is no CLI tags, execute all tests that do not have any tags
+                device_to_tests[device].update(self.catalog.tag_to_tests[None])
+
+                # Then add the tests with matching tags from device tags
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(device.tags))
+
+            total_tests += len(device_to_tests[device])
+
+            # Check device connections
+            if not hasattr(device, "max_connections") or device.max_connections is None:
+                all_have_connections = False
+            else:
+                total_connections += device.max_connections
+
+        if total_tests == 0:
+            tag_msg = f" matching the tags {filters.tags} " if filters.tags else " "
+            logger.warning(
+                "There are no tests%sto run in the current test catalog and device inventory, please verify your inputs.",
+                tag_msg,
+            )
+            return False
+
+        self._selected_tests = device_to_tests
path: anta/_runner.py
user: login: carl-baillargeon
body: Yes. Fixed in latest commits.
created_at: 2025-05-12T16:00:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,429 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, PrivateAttr
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Store inventory filtering statistics of an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int
+    filtered_by_tags: int
+    connection_failed: int
+    established: int
+
+
+class AntaRunnerFilter(BaseModel):
+    """Define a filter for an ANTA run.
+
+    The filter determines which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If `None`, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If `None`, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When `True`, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If `None`, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        `None` if unknown.
+    _settings : AntaRunnerSettings
+        Internal settings loaded from environment variables. See the class definition
+        in the `anta.settings` module for details.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with `_`) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerFilter
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerFilter(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = PrivateAttr(default=None)
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = PrivateAttr(default=None)
+    _inventory_stats: AntaRunnerInventoryStats | None = PrivateAttr(default=None)
+    _total_tests: int = PrivateAttr(default=0)
+    _potential_connections: float | None = PrivateAttr(default=None)
+
+    # Internal settings loaded from environment variables
+    _settings: AntaRunnerSettings = PrivateAttr(default_factory=AntaRunnerSettings)
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, filters: AntaRunnerFilter | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        filters
+            Filters for the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        filters = filters or AntaRunnerFilter()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(filters, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(filters):
+                    return manager
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+            # Build test coroutines
+            test_coroutines = self._get_test_coroutines()
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            for coro in test_coroutines:
+                # Get the AntaTest instance from the coroutine locals, can be in `args` when decorated
+                coro_locals = getcoroutinelocals(coro)
+                test = coro_locals.get("self") or coro_locals.get("args", (None))[0]
+                if isinstance(test, AntaTest):
+                    manager.add(test.result)
+                else:
+                    logger.error("Coroutine %s does not have an AntaTest instance.", coro)
+                coro.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            sem = Semaphore(self._settings.max_concurrency)
+
+            async def run_with_sem(test_coro: Coroutine[Any, Any, TestResult]) -> TestResult:
+                """Wrap the test coroutine with semaphore control."""
+                async with sem:
+                    return await test_coro
+
+            results = await gather(*[run_with_sem(coro) for coro in test_coroutines])
+            for res in results:
+                manager.add(res)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, filters: AntaRunnerFilter, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices, filtered_by_tags=0, connection_failed=0, established=0)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the provided filters any
+        filtered_inventory = self.inventory.get_inventory(tags=filters.tags, devices=filters.devices) if filters.tags or filters.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
+        self._selected_inventory = filtered_inventory.get_inventory(established_only=filters.established_only)
+        connection_failed = len(filtered_inventory) - len(self._selected_inventory)
+
+        # If there are no devices in the inventory after filtering, exit
+        if not self._selected_inventory.devices:
+            # Build message parts
+            tag_msg = f"matching the tags {filters.tags} " if filters.tags else ""
+            device_msg = f" Selected devices: {filters.devices} " if filters.devices is not None else ""
+            logger.warning("No reachable device %swas found.%s", tag_msg, device_msg)
+            return False
+
+        self._inventory_stats = AntaRunnerInventoryStats(
+            total=total_devices, filtered_by_tags=filtered_by_tags, connection_failed=connection_failed, established=len(self._selected_inventory)
+        )
+        return True
+
+    def _setup_tests(self, filters: AntaRunnerFilter) -> bool:
+        """Set up tests for the ANTA run."""
+        if self._selected_inventory is None:
+            msg = "The selected inventory is not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        # Build indexes for the catalog. If `filters.tests` is set, filter the indexes based on these tests
+        self.catalog.build_indexes(filtered_tests=filters.tests)
+
+        # Using a set to avoid inserting duplicate tests
+        device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
+        total_tests = 0
+        total_connections = 0
+        all_have_connections = True
+
+        # Create the device to tests mapping from the tags and calculate connection stats
+        for device in self._selected_inventory.devices:
+            if filters.tags:
+                # If there are CLI tags, execute tests with matching tags for this device
+                if not (matching_tags := filters.tags.intersection(device.tags)):
+                    # The device does not have any selected tag, skipping
+                    continue
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(matching_tags))
+            else:
+                # If there is no CLI tags, execute all tests that do not have any tags
+                device_to_tests[device].update(self.catalog.tag_to_tests[None])
+
+                # Then add the tests with matching tags from device tags
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(device.tags))
+
+            total_tests += len(device_to_tests[device])
+
+            # Check device connections
+            if not hasattr(device, "max_connections") or device.max_connections is None:
+                all_have_connections = False
+            else:
+                total_connections += device.max_connections
+
+        if total_tests == 0:
+            tag_msg = f" matching the tags {filters.tags} " if filters.tags else " "
+            logger.warning(
+                "There are no tests%sto run in the current test catalog and device inventory, please verify your inputs.",
+                tag_msg,
+            )
+            return False
+
+        self._selected_tests = device_to_tests
+        self._total_tests = total_tests
+        self._potential_connections = None if not all_have_connections else total_connections
path: anta/_runner.py
user: login: carl-baillargeon
body: Yes moved to inventory.
created_at: 2025-05-12T16:00:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -305,6 +305,29 @@ def _filter_devices(device: AntaDevice) -> bool:
             result.add_device(device)
         return result
 
+    def get_potential_connections(self) -> int | None:
path: anta/inventory/__init__.py
user: login: gmuloc
body: Lets make this internal
```suggestion
    def _get_potential_connections(self) -> int | None:
```
created_at: 2025-05-13T09:33:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,3 +90,36 @@ def test_parse_os_error(self, caplog: pytest.LogCaptureFixture) -> None:
         with pytest.raises(OSError, match="No such file or directory"):
             _ = AntaInventory.parse(filename="dummy.yml", username="arista", password="arista123")
         assert "Unable to parse ANTA Device Inventory file" in caplog.records[0].message
+
+    @pytest.mark.parametrize(("inventory"), [{"count": 3}], indirect=True)
+    def test_get_potential_connections(self, inventory: AntaInventory) -> None:
+        """Test get_potential_connections with regular AsyncEOSDevice objects in the inventory."""
+        # Each AsyncEOSDevice has a max_connections of 100
+        assert inventory.get_potential_connections() == 300
+
+    def test_get_potential_connections_custom_anta_device(self, caplog: pytest.LogCaptureFixture, async_device: AsyncEOSDevice) -> None:
+        """Test get_potential_connections with a custom AntaDevice implementation with no max_connections in the inventory."""
+        caplog.set_level(logging.DEBUG)
+
+        class CustomAntaDevice(AntaDevice):
path: tests/units/inventory/test__init__.py
user: login: gmuloc
body: don't we have one already in some place like conftest? I think you can use `device` fixture which is a mocked AntaDevice 
created_at: 2025-05-13T09:39:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -114,16 +114,19 @@ class AntaDevice(ABC):
         True if the device IP is reachable and a port can be open.
     established : bool
         True if remote command execution succeeds.
-    hw_model : str
+    hw_model : str | None
         Hardware model of the device.
     tags : set[str]
         Tags for this device.
     cache : AntaCache | None
         In-memory cache for this device (None if cache is disabled).
-    cache_locks : dict
+    cache_locks : defaultdict[str, asyncio.Lock] | None
         Dictionary mapping keys to asyncio locks to guarantee exclusive access to the cache if not disabled.
         Deprecated, will be removed in ANTA v2.0.0, use self.cache.locks instead.
-
+    max_connections : int | None
+        For informational/logging purposes only. Can be used by the runner to verify that
+        the total potential connections of a run do not exceed the system's file descriptor limit.
path: anta/device.py
user: login: gmuloc
body: ```suggestion
        the total potential connections of a run do not exceed the system file descriptors limit.
```
created_at: 2025-05-13T09:42:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,479 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import logging
+from asyncio import Semaphore, gather
+from collections import defaultdict
+from dataclasses import dataclass, field
+from datetime import datetime, timedelta, timezone
+from inspect import getcoroutinelocals
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict
+
+from anta import GITHUB_SUGGESTION
+from anta.cli.console import console
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import AntaRunnerSettings
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from collections.abc import Coroutine
+
+    from anta.catalog import AntaCatalog, AntaTestDefinition
+    from anta.device import AntaDevice
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+class AntaRunFilters(BaseModel):
+    """Define filters for an ANTA run.
+
+    Filters determine which devices and tests to include in a run, and how to
+    filter them with tags. This class is used with the `AntaRunner.run()` method.
path: anta/_runner.py
user: login: gmuloc
body: ```suggestion
    filter them with tags. This class is used by the `AntaRunner.run()` method.
```
created_at: 2025-05-13T13:19:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,147 @@
+[
path: anta.json
user: login: gmuloc
body: should remove
created_at: 2025-05-13T14:00:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,147 @@
+[
path: anta.json
user: login: carl-baillargeon
body: Done
created_at: 2025-05-13T14:02:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,508 @@
+# Copyright (c) 2023-2025 Arista Networks, Inc.
+# Use of this source code is governed by the Apache License 2.0
+# that can be found in the LICENSE file.
+"""ANTA runner classes."""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from anta import GITHUB_SUGGESTION
+from anta.catalog import AntaCatalog, AntaTestDefinition
+from anta.cli.console import console
+from anta.device import AntaDevice
+from anta.inventory import AntaInventory
+from anta.logger import anta_log_exception
+from anta.models import AntaTest
+from anta.result_manager import ResultManager
+from anta.settings import get_file_descriptor_limit, get_max_concurrency
+from anta.tools import Catchtime
+
+if TYPE_CHECKING:
+    from asyncio import Task
+    from collections.abc import AsyncGenerator, Coroutine
+
+    from anta.result_manager.models import TestResult
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class AntaRunnerInventoryStats:
+    """Track inventory filtering statistics during an ANTA run.
+
+    This class maintains counters for tracking how the device inventory is filtered
+    during test setup and.
+
+    When initialized, the counters are set to zero.
+
+    Attributes
+    ----------
+    total : int
+        Total number of devices in the original inventory before filtering.
+    filtered_by_tags : int
+        Number of devices excluded due to tag filtering.
+    connection_failed : int
+        Number of devices that failed to establish a connection.
+    established : int
+        Number of devices with successfully established connections that are
+        included in the final test run.
+    """
+
+    total: int = 0
+    filtered_by_tags: int = 0
+    connection_failed: int = 0
+    established: int = 0
+
+
+class AntaRunnerScope(BaseModel):
+    """Define the scope of an ANTA test run.
+
+    The scope determines which devices and tests to include in a run, and how to
+    filter them. This class is used with the AntaRunner.run() method.
+
+    Attributes
+    ----------
+    devices : set[str] | None, optional
+        Set of device names to run tests on. If None, includes all devices in
+        the inventory. Commonly set via the NRFU CLI `--device/-d` option.
+    tests : set[str] | None, optional
+        Set of test names to run. If None, runs all available tests in the
+        catalog. Commonly set via the NRFU CLI `--test/-t` option.
+    tags : set[str] | None, optional
+        Set of tags used to filter both devices and tests. A device or test
+        must match any of the provided tags to be included. Commonly set via
+        the NRFU CLI `--tags` option.
+    established_only : bool, default=True
+        When True, only includes devices with established connections in the
+        test run.
+    """
+
+    model_config = ConfigDict(frozen=True, extra="forbid")
+    devices: set[str] | None = None
+    tests: set[str] | None = None
+    tags: set[str] | None = None
+    established_only: bool = True
+
+
+class AntaRunner(BaseModel):
+    """Run and manage ANTA test execution.
+
+    This class orchestrates the execution of ANTA tests across network devices. It handles
+    inventory filtering, test selection, concurrent test execution, and result collection.
+
+    Attributes
+    ----------
+    inventory : AntaInventory
+        Inventory of network devices to test.
+    catalog : AntaCatalog
+        Catalog of available tests to run.
+    manager : ResultManager | None, optional
+        Manager for collecting and storing test results. If None, a new manager
+        is returned for each run, otherwise the provided manager is used
+        and results from subsequent runs are appended to it.
+    max_concurrency : int
+        Maximum number of tests that can run concurrently. Defaults from `get_max_concurrency()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    file_descriptor_limit : int
+        System file descriptor limit for connections. Defaults from `get_file_descriptor_limit()`.
+        See the `anta.settings` module for details on how this value is determined from
+        environment variables and system defaults.
+    _selected_inventory : AntaInventory | None
+        Internal state of filtered inventory for current run.
+    _selected_tests : defaultdict[AntaDevice, set[AntaTestDefinition]] | None
+        Mapping of devices to their selected tests for current run.
+    _inventory_stats : AntaRunnerInventoryStats | None
+        Statistics about inventory filtering for current run.
+    _total_tests : int
+        Total number of tests to run in current execution.
+    _potential_connections : float | None
+        Total potential concurrent connections needed for current run.
+        None if unknown, float('inf') if unlimited.
+
+    Notes
+    -----
+    After initializing an `AntaRunner` instance, tests should only be executed through
+    the `run()` method. This method manages the complete test lifecycle including setup,
+    execution, and cleanup.
+
+    All internal methods and state (prefixed with _) are managed by the `run()` method
+    and should not be called directly. The internal state is reset between runs to
+    ensure clean execution.
+
+
+    Examples
+    --------
+    ```python
+    import asyncio
+
+    from anta._runner import AntaRunner, AntaRunnerScope
+    from anta.catalog import AntaCatalog
+    from anta.inventory import AntaInventory
+
+    inventory = AntaInventory.parse(
+        filename="anta_inventory.yml",
+        username="arista",
+        password="arista",
+    )
+    catalog = AntaCatalog.parse(filename="anta_catalog.yml")
+
+    # Create an ANTA runner
+    runner = AntaRunner(inventory=inventory, catalog=catalog)
+
+    # Run all tests
+    first_run_results = asyncio.run(runner.run())
+
+    # Run with filters
+    second_run_results = asyncio.run(runner.run(scope=AntaRunnerScope(tags={"leaf"})))
+    ```
+    """
+
+    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
+    inventory: AntaInventory
+    catalog: AntaCatalog
+    manager: ResultManager | None = None
+    max_concurrency: int = Field(default_factory=get_max_concurrency)
+    file_descriptor_limit: int = Field(default_factory=get_file_descriptor_limit)
+
+    # Internal attributes set during setup phases before each run
+    _selected_inventory: AntaInventory | None = None
+    _selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+    _inventory_stats: AntaRunnerInventoryStats | None = None
+    _total_tests: int = 0
+    _potential_connections: float | None = None
+
+    def reset(self) -> None:
+        """Reset the internal attributes of the ANTA runner."""
+        self._selected_inventory: AntaInventory | None = None
+        self._selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] | None = None
+        self._inventory_stats: AntaRunnerInventoryStats | None = None
+        self._total_tests: int = 0
+        self._potential_connections: float | None = None
+
+    async def run(self, scope: AntaRunnerScope | None = None, *, dry_run: bool = False) -> ResultManager:
+        """Run ANTA.
+
+        Parameters
+        ----------
+        scope
+            Scope of the ANTA run. If None, runs all tests on all devices.
+        dry_run
+            Dry-run mode flag. If True, runs all setup steps but does not execute tests.
+        """
+        scope = scope or AntaRunnerScope()
+
+        # Cleanup the instance before each run
+        self.reset()
+        self.catalog.clear_indexes()
+        manager = ResultManager() if self.manager is None else self.manager
+
+        if not self.catalog.tests:
+            logger.info("The list of tests is empty, exiting")
+            return manager
+
+        with Catchtime(logger=logger, message="Preparing ANTA NRFU Run"):
+            # Set up inventory
+            if not await self._setup_inventory(scope, dry_run=dry_run):
+                return manager
+
+            # Set up tests
+            with Catchtime(logger=logger, message="Preparing Tests"):
+                if not self._setup_tests(scope):
+                    return manager
+
+            # Set up test coroutines
+            generator = self._setup_coroutines(manager=manager if dry_run else None)
+
+            # Log run information
+            self._log_run_information(dry_run=dry_run)
+
+        if dry_run:
+            logger.info("Dry-run mode, exiting before running the tests.")
+            async for test in generator:
+                test.close()
+            return manager
+
+        if AntaTest.progress is not None:
+            AntaTest.nrfu_task = AntaTest.progress.add_task("Running NRFU Tests...", total=self._total_tests)
+
+        with Catchtime(logger=logger, message="Running Tests"):
+            async for result in self._run(generator):
+                manager.add(result)
+
+        self._log_cache_statistics()
+        return manager
+
+    async def _setup_inventory(self, scope: AntaRunnerScope, *, dry_run: bool = False) -> bool:
+        """Set up the inventory for the ANTA run."""
+        total_devices = len(self.inventory)
+
+        # In dry-run mode, set the selected inventory to the full inventory
+        if dry_run:
+            self._selected_inventory = self.inventory
+            self._inventory_stats = AntaRunnerInventoryStats(total=total_devices)
+            return True
+
+        # If the inventory is empty, exit
+        if total_devices == 0:
+            logger.info("The inventory is empty, exiting")
+            return False
+
+        # Filter the inventory based on the CLI provided tags and devices if any
+        filtered_inventory = self.inventory.get_inventory(tags=scope.tags, devices=scope.devices) if scope.tags or scope.devices else self.inventory
+        filtered_by_tags = total_devices - len(filtered_inventory)
+
+        # Connect to devices
+        with Catchtime(logger=logger, message="Connecting to devices"):
+            await filtered_inventory.connect_inventory()
+
+        # Remove devices that are unreachable
+        self._selected_inventory = filtered_inventory.get_inventory(established_only=scope.established_only)
+        connection_failed = len(filtered_inventory) - len(self._selected_inventory)
+
+        # If there are no devices in the inventory after filtering, exit
+        if not self._selected_inventory.devices:
+            # Build message parts
+            tag_msg = f"matching the tags {scope.tags} " if scope.tags else ""
+            device_msg = f" Selected devices: {scope.devices} " if scope.devices is not None else ""
+            logger.warning("No reachable device %swas found.%s", tag_msg, device_msg)
+            return False
+
+        self._inventory_stats = AntaRunnerInventoryStats(
+            total=total_devices, filtered_by_tags=filtered_by_tags, connection_failed=connection_failed, established=len(self._selected_inventory)
+        )
+        return True
+
+    def _setup_tests(self, scope: AntaRunnerScope) -> bool:
+        """Set up tests for the ANTA run."""
+        if self._selected_inventory is None:
+            msg = "The selected inventory is not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        # Build indexes for the catalog. If `tests` is set, filter the indexes based on these tests
+        self.catalog.build_indexes(filtered_tests=scope.tests)
+
+        # Using a set to avoid inserting duplicate tests
+        device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)
+        total_tests = 0
+        total_connections = 0
+        unlimited_connections = False
+        all_have_connections = True
+
+        # Create the device to tests mapping from the tags and calculate connection stats
+        for device in self._selected_inventory.devices:
+            if scope.tags:
+                # If there are CLI tags, execute tests with matching tags for this device
+                if not (matching_tags := scope.tags.intersection(device.tags)):
+                    # The device does not have any selected tag, skipping
+                    continue
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(matching_tags))
+            else:
+                # If there is no CLI tags, execute all tests that do not have any tags
+                device_to_tests[device].update(self.catalog.tag_to_tests[None])
+
+                # Then add the tests with matching tags from device tags
+                device_to_tests[device].update(self.catalog.get_tests_by_tags(device.tags))
+
+            total_tests += len(device_to_tests[device])
+
+            # Check device connections
+            if not hasattr(device, "max_connections"):
+                all_have_connections = False
+            elif device.max_connections is None:
+                unlimited_connections = True
+            else:
+                total_connections += device.max_connections
+
+        if total_tests == 0:
+            tag_msg = f" matching the tags {scope.tags} " if scope.tags else " "
+            logger.warning(
+                "There are no tests%sto run in the current test catalog and device inventory, please verify your inputs.",
+                tag_msg,
+            )
+            return False
+
+        self._selected_tests = device_to_tests
+        self._total_tests = total_tests
+        self._potential_connections = None if not all_have_connections else float("inf") if unlimited_connections else total_connections
+        return True
+
+    async def _setup_coroutines(self, *, manager: ResultManager | None = None) -> AsyncGenerator[Coroutine[Any, Any, TestResult], None]:
+        """Set up test coroutines from selected tests for the ANTA run.
+
+        It creates an async generator of coroutines which are created by the `test` method of the AntaTest instances.
+        Each coroutine is a test to run.
+
+        Parameters
+        ----------
+        manager
+            An optional ResultManager instance to pre-populate with the test results. Used in `dry_run` mode.
+
+        Yields
+        ------
+            The coroutine (test) to run.
+        """
+        if self._selected_tests is None:
+            msg = "The selected tests are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        for device, test_definitions in self._selected_tests.items():
+            for test in test_definitions:
+                try:
+                    test_instance = test.test(device=device, inputs=test.inputs)
+                    if manager is not None:
+                        manager.add(test_instance.result)
+                    coroutine = test_instance.test()
+                except Exception as e:  # noqa: PERF203, BLE001
+                    # An AntaTest instance is potentially user-defined code.
+                    # We need to catch everything and exit gracefully with an error message.
+                    message = "\n".join(
+                        [
+                            f"There is an error when creating test {test.test.__module__}.{test.test.__name__}.",
+                            f"If this is not a custom test implementation: {GITHUB_SUGGESTION}",
+                        ],
+                    )
+                    anta_log_exception(e, message, logger)
+                else:
+                    yield coroutine
+
+    def _log_run_information(self, *, dry_run: bool = False) -> None:
+        """Log ANTA run information and potential resource limit warnings.
+
+        Parameters
+        ----------
+        dry_run
+            Dry-run mode flag.
+        """
+        if self._inventory_stats is None:
+            msg = "The inventory stats are not available. ANTA must be executed through AntaRunner.run()"
+            raise RuntimeError(msg)
+
+        width = min(int(console.width) - 34, len("  Total potential connections: Unlimited\n"))
+
+        # Build device information
+        device_lines = [
+            "Devices:",
+            f"  Total: {self._inventory_stats.total}",
+        ]
+        if self._inventory_stats.filtered_by_tags > 0:
+            device_lines.append(f"  Excluded by tags: {self._inventory_stats.filtered_by_tags}")
+        if self._inventory_stats.connection_failed > 0:
+            device_lines.append(f"  Failed to connect: {self._inventory_stats.connection_failed}")
+        device_lines.append(f"  Selected: {self._inventory_stats.established}{' (dry-run mode)' if dry_run else ''}")
+
+        # Build connection information
+        connections_line = (
+            ""
+            if self._potential_connections is None
+            else f"  Total potential connections: {'Unlimited' if self._potential_connections == float('inf') else self._potential_connections}\n"
+        )
+
+        run_info = (
+            f"{' ANTA NRFU Run Information ':-^{width}}\n"
+            f"{chr(10).join(device_lines)}\n"
+            f"Tests: {self._total_tests} total\n"
+            f"Limits:\n"
+            f"  Max concurrent tests: {self.max_concurrency}\n"
+            f"{connections_line}"
+            f"  Max file descriptors: {self.file_descriptor_limit}\n"
+            f"{'':-^{width}}"
+        )
+        logger.info(run_info)
+
+        # Log warnings for potential resource limits
+        if self._total_tests > self.max_concurrency:
+            logger.warning(
+                "Tests count (%s) exceeds concurrent limit (%s). Tests will be throttled. See Scaling ANTA documentation.", self._total_tests, self.max_concurrency
+            )
+        if self._potential_connections == float("inf"):
+            logger.warning(
+                "Running with unlimited connections. Connection errors may occur due to file descriptor limit (%s). See Scaling ANTA documentation.",
+                self.file_descriptor_limit,
+            )
+        elif self._potential_connections is not None and self._potential_connections > self.file_descriptor_limit:
+            logger.warning(
+                "Potential connections (%s) exceeds file descriptor limit (%s). Connection errors may occur. See Scaling ANTA documentation.",
+                self._potential_connections,
+                self.file_descriptor_limit,
+            )
+
+    async def _run(self, generator: AsyncGenerator[Coroutine[Any, Any, TestResult], None]) -> AsyncGenerator[TestResult, None]:
+        """Run tests with a concurrency limit.
+
+        This function takes an asynchronous generator of test coroutines and runs them
+        with a limit on the number of concurrent tests. It yields test results as each
+        test completes.
+
+        Inspired by: https://death.andgravity.com/limit-concurrency
+
+        Parameters
+        ----------
+        generator
+            An asynchronous generator that yields test coroutines.
+
+        Yields
+        ------
+            The result of each completed test.
+        """
+        if self.max_concurrency <= 0:
+            msg = "Concurrency limit must be greater than 0."
+            raise RuntimeError(msg)
+
+        # NOTE: The `aiter` built-in function is not available in Python 3.9
+        tests = generator.__aiter__()  # pylint: disable=unnecessary-dunder-call
+        tests_ended = False
+        tests_pending: set[Task[TestResult]] = set()
+
+        while tests_pending or not tests_ended:
+            # Add tests to the pending set until the limit is reached or no more tests are available
+            while len(tests_pending) < self.max_concurrency and not tests_ended:
+                try:
+                    # NOTE: The `anext` built-in function is not available in Python 3.9
+                    test = await tests.__anext__()  # pylint: disable=unnecessary-dunder-call
+                except StopAsyncIteration:  # noqa: PERF203
+                    tests_ended = True
+                    logger.debug("All tests have been added to the pending set.")
+                else:
+                    # Ensure the coroutine is scheduled to run and add it to the pending set
+                    tests_pending.add(asyncio.create_task(test))
+                    logger.debug("Added a test to the pending set: %s", test)
+
+            if len(tests_pending) >= self.max_concurrency:
+                logger.debug("Concurrency limit reached: %s tests running. Waiting for tests to complete.", self.max_concurrency)
+
+            if not tests_pending:
+                logger.debug("No pending tests and all tests have been processed. Exiting.")
+                return
path: anta/_runner.py
user: login: gmuloc
body: not relevant anymore for this PR
created_at: 2025-05-13T14:30:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2024-06-12T16:43:30Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=680) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=680&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=680&resolutions=WONTFIX)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=aristanetworks_anta&pullRequest=680&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=680&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=680)


author_association: NONE
user: login: github-actions[bot]
created_at: 2024-07-04T14:03:49Z
body: This pull request has conflicts, please resolve those before we can evaluate the pull request.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2024-08-28T18:50:12Z
body: Conflicts have been resolved. A maintainer will review the pull request shortly.
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2024-08-28T19:07:52Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=680) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=680&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=680&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=aristanetworks_anta&pullRequest=680&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [84.8% Coverage on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=680&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=680&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=680)


author_association: NONE
user: login: github-actions[bot]
created_at: 2024-09-11T13:47:41Z
body: This pull request has conflicts, please resolve those before we can evaluate the pull request.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2024-10-02T15:05:25Z
body: Conflicts have been resolved. A maintainer will review the pull request shortly.
author_association: CONTRIBUTOR
user: login: codspeed-hq[bot]
created_at: 2024-10-02T15:15:13Z
body: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Performance Report](https://codspeed.io/aristanetworks/anta/branches/carl-baillargeon%3Arefactor%2Frunner_limit)

### Merging #680 will **not alter performance**

<sub>Comparing <code>carl-baillargeon:refactor/runner_limit</code> (3fbf3e0) with <code>main</code> (0231fa6)</sub>



### Summary

`✅ 22` untouched benchmarks  
`🆕 4` new benchmarks  



### Benchmarks breakdown

|     | Benchmark | `BASE` | `HEAD` | Change |
| --- | --------- | ----------------------- | ------------------- | ------ |
| 🆕 | `` test__get_test_coroutines[1-device] `` | N/A | 40.2 ms | N/A |
| 🆕 | `` test__get_test_coroutines[2-devices] `` | N/A | 79.2 ms | N/A |
| 🆕 | `` test__setup_tests[1-device] `` | N/A | 23.3 ms | N/A |
| 🆕 | `` test__setup_tests[2-devices] `` | N/A | 23.4 ms | N/A |

author_association: NONE
user: login: github-actions[bot]
created_at: 2024-10-10T18:49:29Z
body: This pull request has conflicts, please resolve those before we can evaluate the pull request.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2024-11-04T21:15:42Z
body: Conflicts have been resolved. A maintainer will review the pull request shortly.
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2024-11-28T05:27:55Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=680) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [3 New issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=680&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=680&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=aristanetworks_anta&pullRequest=680&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [86.4% Coverage on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=680&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=680&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=680)


author_association: NONE
user: login: github-actions[bot]
created_at: 2024-12-18T16:38:39Z
body: This pull request has conflicts, please resolve those before we can evaluate the pull request.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2024-12-24T03:28:51Z
body: Conflicts have been resolved. A maintainer will review the pull request shortly.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-01-17T00:17:53Z
body: This pull request has conflicts, please resolve those before we can evaluate the pull request.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-01-17T00:36:29Z
body: Conflicts have been resolved. A maintainer will review the pull request shortly.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-01-17T18:12:49Z
body: This pull request has conflicts, please resolve those before we can evaluate the pull request.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-01-17T18:14:48Z
body: Conflicts have been resolved. A maintainer will review the pull request shortly.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-02-07T13:16:51Z
body: This pull request has conflicts, please resolve those before we can evaluate the pull request.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-02-07T13:20:37Z
body: Conflicts have been resolved. A maintainer will review the pull request shortly.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-02-21T08:22:15Z
body: This pull request has conflicts, please resolve those before we can evaluate the pull request.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-02-24T12:21:05Z
body: Conflicts have been resolved. A maintainer will review the pull request shortly.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-11T13:41:49Z
body: This pull request has conflicts, please resolve those before we can evaluate the pull request.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-12T12:28:20Z
body: Conflicts have been resolved. A maintainer will review the pull request shortly.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-07T17:59:47Z
body: This pull request has conflicts, please resolve those before we can evaluate the pull request.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-09T12:58:33Z
body: Conflicts have been resolved. A maintainer will review the pull request shortly.
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-13T14:12:20Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=680) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=680&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=aristanetworks_anta&pullRequest=680&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=aristanetworks_anta&pullRequest=680&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [97.7% Coverage on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=680&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=aristanetworks_anta&pullRequest=680&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aristanetworks_anta&pullRequest=680)


author_association: NONE