#3657·patroni

REST API restart does not apply primary_stop_timeout when stopping a primary

Author: sebawebberCreated Jun 29, 2026Updated Jun 29, 2026

What happened?

When Patroni restarts PostgreSQL through the REST API /restart path, Postgresql.restart() calls Postgresql.stop() without passing stop_timeout.

That differs from the demotion/shutdown paths, which apply primary_stop_timeout() when stopping a primary.

As a result, a primary restart requested through /restart can wait indefinitely for PostgreSQL shutdown, even when primary_stop_timeout is configured and the same stop during demotion/shutdown would be bounded.

Why I think this is unexpected

The dynamic configuration docs describe primary_stop_timeout as:

The number of seconds Patroni is allowed to wait when stopping Postgres and effective only when synchronous_mode is enabled. When set to > 0 and the synchronous_mode is enabled, Patroni sends SIGKILL to the postmaster if the stop operation is running for more than the value set by primary_stop_timeout.

https://patroni.readthedocs.io/en/latest/dynamic_configuration.html

The /restart endpoint is documented as the way to restart PostgreSQL on a specific node:

https://patroni.readthedocs.io/en/latest/rest_api.html#restart-endpoint

So I expected a primary restart to use the same stop timeout semantics as demotion/shutdown, or for the difference to be documented explicitly.

How can we reproduce it?

I created a small Docker-based reproduction with more details:

https://gist.github.com/sebastianwebber/9ff908975ece526d801f03769f36f14d

It builds a local image with Patroni, instruments Postgresql.stop(), calls Postgresql.restart(timeout=30, block_callbacks=True, role="primary"), and prints the arguments passed to stop().

Relevant output:

Expected:
  Postgresql.restart() should call stop(..., stop_timeout=<primary_stop_timeout>)

Actual:
  Postgresql.restart() called stop(..., stop_timeout=None)
  Full stop() kwargs: {'block_callbacks': True, 'before_shutdown': None}

Result: BUG REPRODUCED
  restart() does not pass stop_timeout to stop().
  Therefore, a primary restart cannot apply primary_stop_timeout in this path.

I also confirmed this end-to-end with a disposable three-node Patroni cluster by setting:

yaml
synchronous_mode: quorum
primary_stop_timeout: 31

Then I stopped a normal SQL backend on the leader with SIGSTOP and called:

bash
curl -v --max-time 90 -X POST http://127.0.0.1:8008/restart \
  -H 'Content-Type: application/json' \
  -d '{"role":"primary"}'

The request timed out after 90 seconds with no response, even though primary_stop_timeout was set to 31 seconds. After resuming the stopped backend with SIGCONT, the node returned to running.

Code references

Postgresql.stop() accepts stop_timeout and uses it to bound shutdown behavior:

https://github.com/patroni/patroni/blob/v4.1.0/patroni/postgresql/__init__.py#L844-L861

Postgresql.restart() calls stop() without stop_timeout:

https://github.com/patroni/patroni/blob/v4.1.0/patroni/postgresql/__init__.py#L1024-L1039

Ha.primary_stop_timeout() returns the configured timeout only when synchronous mode is enabled:

https://github.com/patroni/patroni/blob/v4.1.0/patroni/ha.py#L267-L270

The demotion path passes stop_timeout=self.primary_stop_timeout():

https://github.com/patroni/patroni/blob/v4.1.0/patroni/ha.py#L1611-L1615

The restart path calls self.state_handler.restart(...) without threading a stop timeout:

https://github.com/patroni/patroni/blob/v4.1.0/patroni/ha.py#L1905-L1938

What did you expect to happen?

When restarting a primary, Patroni should apply primary_stop_timeout() to the stop phase, matching demotion/shutdown behavior.

What happened instead?

The /restart path does not pass any stop timeout to Postgresql.stop(), so the configured primary_stop_timeout is not applied.

Patroni/PostgreSQL/DCS version

Confirmed against Patroni 4.1.0 and 4.1.2.

This appears independent of PostgreSQL major version because the issue is in Patroni’s restart call path.

Anything else we need to know?

This seems related to, but distinct from, https://github.com/patroni/patroni/issues/3639.

That issue concerns watchdog behavior after primary_stop_timeout expires. This issue is about the /restart path not applying primary_stop_timeout at all.

Would you be open to a PR that threads an optional stop_timeout through Postgresql.restart() and passes primary_stop_timeout() from the HA restart path when restarting a primary?