redis/redis-py · error · RedisError

Cannot issue nested calls to MULTI

Error message

Cannot issue nested calls to MULTI

What it means

Raised by Pipeline.multi() when self.explicit_transaction is already True. MULTI is a Redis command that begins a transaction; calling .multi() twice mirrors sending MULTI twice, which the server rejects. The library enforces it client-side before the second MULTI is queued.

Solutions

  1. Call pipe.multi() exactly once per transaction; subsequent commands are queued automatically.
  2. Use the async context manager form (`async with client.pipeline(transaction=True) as pipe:`) which manages MULTI for you.
  3. Remove the redundant explicit multi() call in the caller.

Example fix

// before
pipe = client.multi_transaction()
await pipe.set('k', 'v')
pipe.multi()
// after
pipe = client.multi_transaction()
await pipe.set('k', 'v')
# multi() already called by multi_transaction(); just execute
await pipe.execute()
Defensive patterns

Strategy: validation

Validate before calling

assert not pipe.explicit_transaction, 'multi() already called on this pipeline'
pipe.multi()

Type guard

def can_call_multi(pipe) -> bool:
    return not pipe.explicit_transaction

Try / catch

try:
    pipe.multi()
except RedisError as e:
    if 'nested calls to MULTI' in str(e):
        pass  # already in transaction
    else:
        raise

Prevention

When it happens

Trigger: Calling pipe.multi() twice on the same async pipeline: pipe = client.multi_transaction(); ...; pipe.multi(); or calling client.multi() inside a context manager that itself issues MULTI.

Common situations: Wrapping pipeline usage in a helper that calls multi() and the caller also calls it; porting sync code that relied on the transactional=True default without realizing multi() is explicit.

Related errors


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

Appendix: source

Thrown at redis/asyncio/client.py:1895

            # the pipeline must not be left holding a reference to a
            # connection that is being returned to the pool. Shield the
            # release itself so a second cancel cannot split the pool's
            # internal in-use/available bookkeeping mid-update.
            if self.connection:
                connection, self.connection = self.connection, None
                await asyncio.shield(self.connection_pool.release(connection))

    async def aclose(self) -> None:
        """Alias for reset(), a standard method name for cleanup"""
        await self.reset()

    def multi(self):
        """
        Start a transactional block of the pipeline after WATCH commands
        are issued. End the transactional block with `execute`.
        """
        if self.explicit_transaction:
            raise RedisError("Cannot issue nested calls to MULTI")
        if self.command_stack:
            raise RedisError(
                "Commands without an initial WATCH have already been issued"
            )
        self.explicit_transaction = True

    def execute_command(
        self, *args, **kwargs
    ) -> Union["Pipeline", Awaitable["Pipeline"]]:
        if (self.watching or args[0] == "WATCH") and not self.explicit_transaction:
            return self.immediate_execute_command(*args, **kwargs)
        return self.pipeline_execute_command(*args, **kwargs)

    async def _disconnect_reset_raise_on_watching(
        self,
        conn: Connection,
        error: Exception,
        failure_count: Optional[int] = None,

View on GitHub (pinned to 6a6b581b48)