Environment
- Temporal Server 1.25.2, self-hosted on Kubernetes
- Standard (SQL) visibility on MySQL 8, same instance as the main
temporaldatabase - ~460 shards, 4 history replicas,
maxConns: 100per datasource
What happened
We hit #9784 — DeleteFromVisibility
in the MySQL plugin opens a transaction with db.BeginTxx() but runs its DELETE statements
on the pool, so each delete needs two connections at once and the pool deadlocks. Every
VisibilityTaskDeleteExecution fails with context deadline exceeded against the hardcoded
3s taskTimeout.
Because the failing statement is the first one (custom_search_attributes), the
executions_visibility DELETE never runs. Retention deletes the history and mutable state,
but the visibility row survives.
This has been happening since at least December 2024. Current state:
| table | rows | data | indexes |
|---|---|---|---|
executions_visibility |
~32.2M | 34.8 GB | 225.8 GB (22 indexes) |
custom_search_attributes |
~32.8M | 15.8 GB | 177.7 GB (28 indexes) |
Only ~1.7M of those rows are live (running executions plus the last few days of closed
ones). The rest are orphans — temporal workflow describe returns not found for all of
them, including samples from 2024, 2025 and 2026.
What we tried
Lowering history.visibilityProcessorSchedulerWorkerCount from the 512 default to 20 breaks
the deadlock immediately — delete errors went to zero and deletions went from ~0 to 200/s.
But at 200/s the instance saturates: 3,600 read IOPS, 3,900 write IOPS, 82% CPU, and
GetWorkflowExecution on the main path starts failing. Index maintenance across 50 indexes,
32 million times, is simply more work than this instance can absorb. We rolled back.
Worker count turned out to be close to binary: 5 workers still produced ~190/s, and 1 worker
produced ~0. We found no setting in between that gives a sustainable rate.
What we are planning
Instead of deleting 32M rows, rebuild both tables and swap:
CREATE TABLE ... LIKEfor both tables- Scale
temporal-historyto 0 (the only writer of visibility) INSERT ... SELECTthe ~1.7M live rows, one statement per namespace, using
FORCE INDEX (default_idx)with
COALESCE(close_time, '9999-12-31 23:59:59') > UTC_TIMESTAMP() - INTERVAL 8 DAYRENAME TABLEall four atomically- Scale history back up
Estimated window: under an hour.
Questions
-
Is swapping these two tables underneath a stopped history service safe? Does the
server hold any state that assumes continuity of those tables, beyond what lives in the
shard records? -
What happens to the ~32M queued
DeleteExecutionVisibilityTaskafter the swap? Our
reading of 1.25.2 is that they succeed as no-ops:VisibilityStore.DeleteWorkflowExecution
discards thesql.Resultand only checks the error, and a DELETE matching zero rows is not
an error. So the tasks should ack and drain. Is that right? -
Is there a supported way to drop or skip those queued tasks in bulk?
tdbg shard remove-taskworks one task at a time, which is not practical at this volume. We did not
find a command to advance a queue’s ack level. -
Is anything other than the Web UI and the CLI reading these tables? We confirmed our
own services never query visibility, but we would rather not discover a server-side
consumer during the window.
We know the real fix is upgrading to ≥ 1.31.0 and that is planned separately. The question
here is whether this cleanup approach is sound in the meantime, or whether we are missing
something.