aio-libs/aiohttp · error · ValueError

total timeout must be a positive number or None to disable…

Error message

total timeout must be a positive number or None to disable, got 0. Using 0 to disable timeouts is no longer supported, use None instead.

What it means

Raised by ClientTimeout.__post_init__ when the 'total' timeout resolves to exactly 0. Historically, total=0 meant 'disable timeout', but aiohttp now requires None for that; a literal 0 (or all sub-timeouts being 0 with total defaulting to 0) is treated as a misconfiguration and raises ValueError. Note the dataclass first bumps total to the max of itself and any sub-timeout, so total becomes 0 only if total and every sub-timeout are 0/None.

Solutions

  1. Pass total=None to disable the overall timeout: ClientTimeout(total=None).
  2. If you want a small timeout, use a positive number: ClientTimeout(total=30).
  3. Audit config sources (env vars, YAML) that map 'disable' to 0 and remap them to None.
  4. Upgrade guides: search the codebase for 'ClientTimeout(total=0' and replace.

Example fix

# before
timeout = ClientTimeout(total=0)  # ValueError

# after — disable total timeout explicitly
timeout = ClientTimeout(total=None)
Defensive patterns

Strategy: validation

Validate before calling

from aiohttp import ClientTimeout

def safe_timeout(total=None, **kw):
    # normalize legacy 0 (disable) to None
    if total == 0:
        total = None
    for k, v in list(kw.items()):
        if v == 0:
            kw[k] = None
    return ClientTimeout(total=total, **kw)

Type guard

def is_valid_total(total) -> bool:
    return total is None or (isinstance(total, (int, float)) and total > 0)

Try / catch

from aiohttp import ClientTimeout

try:
    timeout = ClientTimeout(total=cfg_total)
except ValueError as e:
    if 'got 0' in str(e):
        timeout = ClientTimeout(total=None)  # migrate legacy disable semantics
    else:
        raise

Prevention

When it happens

Trigger: Constructing ClientTimeout(total=0); or ClientTimeout(total=0, connect=0, sock_read=0) where all resolve to 0; or passing total=0 implicitly through a session/request. The check in __post_init__ fires at construction time, not at request time.

Common situations: Code ported from an older aiohttp version where total=0 disabled timeouts; config loaded from a file/env where 0 was the legacy 'off' sentinel; copy-paste of a snippet targeting an older API.

Understand the failure class

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/1e34f2e7566d9b59. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/client_reqrep.py:125

    def __post_init__(self) -> None:
        # Ensure total is never lower than a more specific timeout, otherwise
        # the latter would be silently capped by total and rendered useless.
        # total=None means the user explicitly disabled the total timeout.
        if self.total is None:
            return
        object.__setattr__(
            self,
            "total",
            max(
                self.total,
                self.connect or 0,
                self.sock_read or 0,
                self.sock_connect or 0,
            ),
        )

        if self.total == 0:
            raise ValueError(
                "total timeout must be a positive number or None to disable, "
                "got 0. Using 0 to disable timeouts is no longer supported, "
                "use None instead."
            )


def _gen_default_accept_encoding() -> str:
    encodings = [
        "gzip",
        "deflate",
    ]
    if HAS_BROTLI:
        encodings.append("br")
    if HAS_ZSTD:
        encodings.append("zstd")
    return ", ".join(encodings)

View on GitHub (pinned to d041d4d0fd)