redis/redis-py · error · DataError

XNACK retrycount must be >= 0

Error message

XNACK retrycount must be >= 0

What it means

Raised by Redis.xnack() when retrycount is provided and is < 0. Note: there is NO isinstance check here -- only `if retrycount < 0:`. So a non-int that supports comparison may slip through to the server; only a negative comparable value raises. retrycount overrides the mode's implicit delivery-counter adjustment.

Solutions

  1. Pass a non-negative int for retrycount, or omit it.
  2. Clamp: retrycount = max(0, int(retrycount)).

Example fix

# before
r.xnack('mystream', 'grp', 'FAIL', '1-0', retrycount=delivered - maxr)  # negative

# after
r.xnack('mystream', 'grp', 'FAIL', '1-0', retrycount=max(0, delivered - maxr))
Defensive patterns

Strategy: validation

Validate before calling

if retrycount is not None:
    retrycount = int(retrycount)
    if retrycount < 0:
        raise ValueError('retrycount must be >= 0')

Type guard

lambda v: v is None or (isinstance(v, int) and not isinstance(v, bool) and v >= 0)

Try / catch

from redis.exceptions import DataError
try:
    r.xnack('mystream', 'grp', 'FAIL', *ids, retrycount=rc)
except DataError as e:
    log.warning('bad xnack retrycount %r: %s', rc, e)

Prevention

When it happens

Trigger: Calling r.xnack('mystream', 'grp', 'FAIL', '1-0', retrycount=-1). A float like -0.5 also raises (< 0). A non-comparable retrycount would raise TypeError, not DataError.

Common situations: Computing retrycount as delivered - max_retries and underflowing below zero.

Related errors


AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10). Data as JSON: /api/errors/1a4363c8bafb59d4. Report an issue: GitHub.

Appendix: source

Thrown at redis/commands/core.py:7690

                already in the group's PEL.

        Returns:
            The number of messages successfully NACKed.

        For more information, see https://redis.io/commands/xnack
        """
        if not ids:
            raise DataError("XNACK requires at least one message ID")

        if mode not in {"SILENT", "FAIL", "FATAL"}:
            raise DataError("XNACK mode must be one of: SILENT, FAIL, FATAL")

        pieces: list = [name, groupname, mode, "IDS", len(ids)]
        pieces.extend(ids)

        if retrycount is not None:
            if retrycount < 0:
                raise DataError("XNACK retrycount must be >= 0")
            pieces.extend([b"RETRYCOUNT", retrycount])

        if force:
            pieces.append(b"FORCE")

        return self.execute_command("XNACK", *pieces)

    @overload
    def xpending(
        self: SyncClientProtocol, name: KeyT, groupname: GroupT
    ) -> dict[str, Any]: ...

    @overload
    def xpending(
        self: AsyncClientProtocol, name: KeyT, groupname: GroupT
    ) -> Awaitable[dict[str, Any]]: ...

    def xpending(

View on GitHub (pinned to 6a6b581b48)