john867
05/27/2026, 3:29 PMprefect server services ls, no service appears representing a scheduler, enabled or disabled.
There appears to be a scheduler service: https://github.com/PrefectHQ/prefect/blob/4e141b688237d1b0b4722fab1de9e43d058957e1/src/prefect/settings/models/server/services.py#L371 , https://docs.prefect.io/v3/api-ref/python/prefect-server-services-scheduler
There also appears to be an environment variable that enables and disables it: PREFECT_API_SERVICES_SCHEDULER_ENABLED It is enabled for me according to prefect config view
PREFECT_API_SERVICES_SCHEDULER_ENABLED does not appear in prefect-helm, so it can't have been configured. Where does the scheduler run, and how can I best debug it?Marvin
05/27/2026, 3:30 PMMarvin
05/27/2026, 3:32 PMprefect server services ls
- In Helm/self-hosted setups, it should run in the process/container running prefect server services start (or inside prefect server start if you are not using --no-services)
- If you moved to Redis, the most important thing to check is whether your background services process is actually running and whether PREFECT_SERVER_DOCKET_URL is set correctly
A few key references:
- Scheduler settings source: https://github.com/PrefectHQ/prefect/blob/4e141b688237d1b0b4722fab1de9e43d058957e1/src/prefect/settings/models/server/services.py#L371
- Scheduler API docs: https://docs.prefect.io/v3/api-ref/python/prefect-server-services-scheduler
- Prefect CLI help confirms prefect server services ls only lists “available services and their status”, but in practice it does not include the scheduler’s perpetual-service implementation
Why prefect server services ls doesn’t show the scheduler
In Prefect 3, the scheduler is implemented differently from the older service classes. It’s registered as a perpetual service, not one of the Service subclasses that prefect server services ls enumerates.
So this is the important bit:
- prefect server services ls not showing a scheduler does not mean the scheduler is absent or disabled
- The scheduler is still controlled by settings like PREFECT_SERVER_SERVICES_SCHEDULER_ENABLED
Also, the env var name for Prefect 3.x is:
- PREFECT_SERVER_SERVICES_SCHEDULER_ENABLED
not PREFECT_API_SERVICES_SCHEDULER_ENABLED
If prefect config view is showing the scheduler enabled, that part is probably fine.
Where the scheduler runs
In self-hosted server deployments, there are two common patterns:
1. Single-process/server setup
- prefect server start
- API + background services run together
2. Production / Helm-style split setup
- API container(s): prefect server start --no-services
- Background services container(s): prefect server services start
In the split setup, the scheduler should run in the background services pod/container, not the API pod.
So if you’re using prefect-helm, the first question is:
- Do you currently have a background services deployment/pod running prefect server services start?
If not, that would explain exactly why no future runs are being created.
What changed when you enabled Redis
Redis is important for coordination in distributed/self-hosted Prefect 3 setups. In particular, the scheduler and other background services use the Docket backend for coordination/locking.
The key setting is:
- PREFECT_SERVER_DOCKET_URL
For a distributed deployment, this should point to Redis, for example:
bash
PREFECT_SERVER_DOCKET_URL=<redis://redis:6379/1>
If this is left at the default in-memory value (memory://), then coordination won’t work correctly across pods/processes.
That can cause confusing behavior:
- duplicated service work in multi-pod setups
- services not coordinating as expected
- scheduler behavior that looks inconsistent
What I would check first
1. Confirm which pod is supposed to run background services
Look for a deployment/container command like:
bash
prefect server services start
If your API pod is running:
bash
prefect server start --no-services
then you must have a separate services pod.
2. Check the actual config in the background-services pod
In the pod that should be running services:
bash
prefect config view
Look for at least:
- PREFECT_SERVER_SERVICES_SCHEDULER_ENABLED
- PREFECT_SERVER_DOCKET_URL
- Redis messaging settings, if configured
3. Check whether Docket is pointed at Redis
You want something like:
bash
PREFECT_SERVER_DOCKET_URL=redis://...
If it says memory://, that’s a red flag in Kubernetes / multi-pod deployments.Marvin
05/27/2026, 3:32 PMbash
kubectl logs <background-services-pod>
or restart that pod with debug logging enabled:
bash
PREFECT_LOGGING_LEVEL=DEBUG
Then look for messages related to:
- perpetual services registration
- scheduler loops
- deployment scheduling
- Redis/Docket connection errors
5. Verify that scheduled runs are actually being created in the DB/UI
The scheduler’s job is to create future flow runs in Scheduled state. If none are being created ahead of time, either:
- the scheduler isn’t running
- it can’t acquire coordination/lock state
- the deployment schedules are inactive/misconfigured
- there are scheduler errors in logs
Important Helm-specific point
You noted that PREFECT_SERVER_SERVICES_SCHEDULER_ENABLED doesn’t appear in prefect-helm.
That’s not necessarily a problem. Since the default is enabled, the chart may simply not expose it explicitly unless you override it.
What does matter is whether the chart is deploying:
- API server(s)
- background services process
- correct shared PostgreSQL
- correct Redis-backed Docket config
So the missing env var in Helm values alone is probably not the root cause.
Best way to debug this
I’d suggest this sequence:
1. Find the service runner pod/deployment
- confirm there is one
- confirm it runs prefect server services start
2. Inspect config inside that pod
- prefect config view | grep -i SCHEDULER
- prefect config view | grep -i DOCKET
3. Inspect logs
- search for scheduler
- search for perpetual
- search for redis
- search for docket
4. Confirm deployments still have active schedules
In the UI or via CLI/API, verify the deployment’s schedule is still active.
5. Check whether switching to Redis also changed service topology
Sometimes Redis gets enabled, but the dedicated background-services deployment isn’t updated with the same env/config as the API pods.
What I suspect most strongly
Given your description, the most likely causes are:
- There is no prefect server services start process running anymore, or
- The background services pod does not have the right Redis/Docket settings, especially PREFECT_SERVER_DOCKET_URL
If you want, paste:
- your relevant prefect-helm values for server/background services/redis
- the command for the API pod and any background-services pod
- output of prefect config view | grep -Ei 'SCHEDULER|DOCKET|REDIS|MESSAGING'
and I can help narrow it down.john867
05/27/2026, 4:13 PMNAME READY STATUS RESTARTS AGE
prefect-postgresql-0 1/1 Running 0 16m
prefect-redis-master-0 1/1 Running 0 16m
prefect-server-777f8976d5-bv7nk 1/1 Running 1 (15m ago) 16m
prefect-server-background-services-cdb9d755d-9s5qx 1/1 Running 3 (15m ago) 16m
The pod prefect-server-background-services-cdb9d755d-9s5qx is running prefect server services start . Both pods appear to be configured correctly, to talk to redis. PREFECT_SERVER_DOCKET_URL is set correctly. In the logs, there is evidence that both the prefect-server and prefect-server-background-services pods can access redis.
Now I see a log error related to scheduling for the first time - it's a database timeout:
16:03:36.370 | ERROR | docket.dependencies - ↩ [ 27143ms] schedule_deployments(){schedule_deployments}
Traceback (most recent call last):
File "/usr/local/lib/python3.13/site-packages/docket/worker.py", line 1002, in _execute
result = await execution.function(
^^^^^^^^^^^^^^^^^^^^^^^^^
*execution.args, **final_kwargs
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/usr/local/lib/python3.13/site-packages/prefect/server/services/scheduler.py", line 248, in schedule_deployments
result = await session.execute(query)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/ext/asyncio/session.py", line 449, in execute
result = await greenlet_spawn(
^^^^^^^^^^^^^^^^^^^^^
...<6 lines>...
)
^
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 203, in greenlet_spawn
result = context.switch(value)
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/orm/session.py", line 2351, in execute
return self._execute_internal(
~~~~~~~~~~~~~~~~~~~~~~^
statement,
^^^^^^^^^^
...<4 lines>...
_add_event=_add_event,
^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/orm/session.py", line 2249, in _execute_internal
result: Result[Any] = compile_state_cls.orm_execute_statement(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
self,
^^^^^
...<4 lines>...
conn,
^^^^^
)
^
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/orm/context.py", line 306, in orm_execute_statement
result = conn.execute(
statement, params or {}, execution_options=execution_options
)
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1419, in execute
return meth(
self,
distilled_parameters,
execution_options or NO_OPTIONS,
)
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/sql/elements.py", line 527, in _execute_on_connection
return connection._execute_clauseelement(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
self, distilled_params, execution_options
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1641, in _execute_clauseelement
ret = self._execute_context(
dialect,
...<8 lines>...
cache_hit=cache_hit,
)
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1846, in _execute_context
return self._exec_single_context(
~~~~~~~~~~~~~~~~~~~~~~~~~^
dialect, context, statement, parameters
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1986, in _exec_single_context
self._handle_dbapi_exception(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
e, str_statement, effective_parameters, cursor, context
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 2366, in _handle_dbapi_exception
raise exc_info[1].with_traceback(exc_info[2])
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/engine/base.py", line 1967, in _exec_single_context
self.dialect.do_execute(
~~~~~~~~~~~~~~~~~~~~~~~^
cursor, str_statement, effective_parameters, context
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/engine/default.py", line 952, in do_execute
cursor.execute(statement, parameters)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 585, in execute
self._adapt_connection.await_(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
self._prepare_and_execute(operation, parameters)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 132, in await_only
return current.parent.switch(awaitable) # type: ignore[no-any-return,attr-defined] # noqa: E501
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 196, in greenlet_spawn
value = await result
^^^^^^^^^^^^
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 563, in _prepare_and_execute
self._handle_exception(error)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 513, in _handle_exception
self._adapt_connection._handle_exception(error)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 799, in _handle_exception
raise error
File "/usr/local/lib/python3.13/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 550, in _prepare_and_execute
self._rows = deque(await prepared_stmt.fetch(*parameters))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.13/site-packages/asyncpg/prepared_stmt.py", line 177, in fetch
data = await self.__bind_execute(args, 0, timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.13/site-packages/asyncpg/prepared_stmt.py", line 268, in __bind_execute
data, status, _ = await self.__do_execute(
^^^^^^^^^^^^^^^^^^^^^^^^
lambda protocol: protocol.bind_execute(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
self._state, args, '', limit, True, timeout))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.13/site-packages/asyncpg/prepared_stmt.py", line 257, in __do_execute
return await executor(protocol)
^^^^^^^^^^^^^^^^^^^^^^^^
File "asyncpg/protocol/protocol.pyx", line 205, in bind_execute
TimeoutError
I think the query used to schedule the flows is timing out, possibly due to some sort of deadlock. Other errors in the logs indicate that other queries are also timing out. How can I work around this problem? There do not appear to be any performance-related issues on my server - no other pods are using high CPU, memory, or IO.Marvin
05/27/2026, 4:18 PMNAME READY STATUS RESTARTS AGE prefect-postgresql-0 1/1 Running 0 16m prefect-redis-master-0 1/1 Running 0 16m prefect-server-777f8976d5-bv7nk 1/1 Running 1 (15m ago) 16m prefect-server-background-services-cdb9d755d-9s5qx 1/1 Running 3 (15m ago) 16m The pod prefect-server-background-services-cdb9d755d-9s5qx is running prefect server services start Both pods appear to be configured correctly to talk to redis PREFECT_SERVER_DOCKET_URL is set correctly In the logs there is evidence that both the prefect-server and prefect-server-background-services pods can access redis Now I see a log error related to scheduling for the first time - it's a database timeout ``` 16 03 36 370 | ERROR | docket dependencies - ↩️ [ 27143ms] schedule_deployments(){schedule_deployments} Traceback (most recent call last) File /usr/local/lib/python3 13/site-packages/docket/worker py line 1002 in _execute result = await execution function( ^^^^^^^^^^^^^^^^^^^^^^^^^ *execution args **final_kwargs ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File /usr/local/lib/python3 13/site-packages/prefect/server/services/scheduler py line 248 in schedule_deployments result = await session execute(query) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File /usr/local/lib/python3 13/site-packages/sqlalchemy/ext/asyncio/session py line 449 in execute result = await greenlet_spawn( ^^^^^^^^^^^^^^^^^^^^^ <6 lines> ) ^ File /usr/local/lib/python3 13/site-packages/sqlalchemy/util/_concurrency_py3k py line 203 in greenlet_spawn result = context switch(value) File /usr/local/lib/python3 13/site-packages/sqlalchemy/orm/session py line 2351 in execute return self _execute_internal( ~~~~~~~~~~~~~~~~~~~~~~^ statement ^^^^^^^^^^ <4 lines> _add_event=_add_event ^^^^^^^^^^^^^^^^^^^^^^ ) ^ File /usr/local/lib/python3 13/site-packages/sqlalchemy/orm/session py line 2249 in _execute_internal result Result[Any] = compile_state_cls orm_execute_statement( ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ self ^^^^^ <4 lines> conn ^^^^^ ) ^ File /usr/local/lib/python3 13/site-packages/sqlalchemy/orm/context py line 306 in orm_execute_statement result = conn execute( statement params or {} execution_options=execution_options ) File /usr/local/lib/python3 13/site-packages/sqlalchemy/engine/base py line 1419 in execute return meth( self distilled_parameters execution_options or NO_OPTIONS ) File /usr/local/lib/python3 13/site-packages/sqlalchemy/sql/elements py line 527 in _execute_on_connection return connection _execute_clauseelement( ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ self distilled_params execution_options ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File /usr/local/lib/python3 13/site-packages/sqlalchemy/engine/base py line 1641 in _execute_clauseelement ret = self _execute_context( dialect <8 lines> cache_hit=cache_hit ) File /usr/local/lib/python3 13/site-packages/sqlalchemy/engine/base py line 1846 in _execute_context return self _exec_single_context( ~~~~~~~~~~~~~~~~~~~~~~~~~^ dialect context statement parameters ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File /usr/local/lib/python3 13/site-packages/sqlalchemy/engine/base py line 1986 in _exec_single_context self _handle_dbapi_exception( ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ e str_statement effective_parameters cursor context ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File /usr/local/lib/python3 13/site-packages/sqlalchemy/engine/base py line 2366 in _handle_dbapi_exception raise exc_info[1] with_traceback(exc_info[2]) File /usr/local/lib/python3 13/site-packages/sqlalchemy/engine/base py line 1967 in _exec_single_context self dialect do_execute( ~~~~~~~~~~~~~~~~~~~~~~~^ cursor str_statement effective_parameters context ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File /usr/local/lib/python3 13/site-packages/sqlalchemy/engine/default py line 952 in do_execute cursor execute(statement parameters) ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^ FileMarvin
05/27/2026, 4:18 PMNate
05/27/2026, 4:23 PMNate
05/27/2026, 4:23 PMjohn867
05/27/2026, 4:43 PM2026-05-27 16:03:36.340 GMT [159] STATEMENT: SELECT deployment.id
FROM deployment
WHERE deployment.paused IS NOT true AND (EXISTS (SELECT $1::INTEGER AS anon_1
FROM deployment_schedule
WHERE deployment_schedule.deployment_id = deployment.id AND deployment_schedule.active IS true AND ((SELECT count(*) AS count_1
FROM flow_run
WHERE flow_run.deployment_id = deployment_schedule.deployment_id AND flow_run.state_type = $2::state_type AND flow_run.next_scheduled_start_time >= $3::TIMESTAMP WITH TIME ZONE AND flow_run.auto_scheduled IS true AND CAST((flow_run.created_by ->> $4::TEXT) AS VARCHAR) = CAST(deployment_schedule.id AS VARCHAR)) < $5::INTEGER OR (SELECT max(flow_run.next_scheduled_start_time) AS max_1
FROM flow_run
WHERE flow_run.deployment_id = deployment_schedule.deployment_id AND flow_run.state_type = $6::state_type AND flow_run.next_scheduled_start_time >= $7::TIMESTAMP WITH TIME ZONE AND flow_run.auto_scheduled IS true AND CAST((flow_run.created_by ->> $4::TEXT) AS VARCHAR) = CAST(deployment_schedule.id AS VARCHAR)) < $8::TIMESTAMP WITH TIME ZONE))) ORDER BY deployment.id
LIMIT $9::INTEGER
2026-05-27 16:03:50.484 GMT [2716] ERROR: canceling statement due to user request
The logs indicate a 15s timeout, which I've now increased to 60s.
Edit: this is not what the logs indicate - the statement follows "ERROR: canceling statement due to user request"
Having deleted my prefect installation and redeployed it, my flows are now being scheduled. Some flows started being scheduled yesterday after I did this, then stopped. I'll post here if the flows stop being scheduled.
The size of the events table is 108634 rows, 208MB.
The size of the deployment table is 49 rows, 192kB
The size of the flow table is 574 rows, 688kBNate
05/27/2026, 5:38 PMjohn867
05/27/2026, 9:54 PMcreated time on any flow run: 2026-05-27 200221.219929+00.
Info from logs:
At 20:03, there's about a minute of network errors in the background services pod logs caused by failing DNS lookups when connecting to redis. Around 20:17, there are lots of network errors where it fails to connect to postgres.
At 200428, the prefect-worker pod got restarted, then again at 201915. I don't have the logs from before 200428.
I'm also seeing lots of network errors in the prefect-server pod.
The postgres pod was shut down at 201351 then restarted, but there's nothing interesting in the logs around 2002 2004.
The proximal cause would appear to be flaky networking in my k8s cluster, but I'd expect the flow run scheduling to survive that.john867
05/29/2026, 4:57 PMprefect server services start locally with some breakpoints. If redis gets restarted, then schedule_deployments stops being called.john867
05/29/2026, 5:36 PM