Summary
lake_dev_reader — the credential lake's own README tells developers to use for local development against production ClickHouse — consumed 12.5 cores, 27.8% of the entire prod cluster, for at least a day. One developer's local k3d stack, running the full page-cache refresh loop against prod data.
It stopped at 20:53Z on 2026-08-20 when the developer shut it down. Nothing about the setup prevents the next person from reproducing it.
What happened
A local api pod (api-fcfb7459c-fhjj9, k3d, pod CIDR 10.42.0.0/16) connected as lake_dev_reader and ran lake's page-cache refresh loop — all ~39 entries — against prod ClickHouse.
Measured over a 15-minute window while live:
| pod |
runs |
cores |
read |
api-fcfb7459c-fhjj9 |
3,673 |
12.48 |
4.61 TiB |
indexer-6694979854-j4d59 |
57 |
0.02 |
2.56 GiB |
4.61 TiB per 15 minutes is ~18 TiB/hour. Over a 5.9h window it was 84,773 queries at 11.8 cores sustained. It had already been running the previous day at ~6.6 cores (16.8% of cluster), so it roughly doubled.
The queries were byte-for-byte prod's own most expensive page-cache entries, at the same cost per run:
| shape |
cores |
interval |
cpu/run |
read/run |
prod cpu/run |
| metro-pair join |
1.30 |
90s |
117.1 |
49.4 GiB |
~113.6 |
jitter_improvement_pct |
1.23 |
82s |
100.6 |
44.2 GiB |
~97.8 |
| validators listing |
0.74 |
10s |
7.5 |
3.9 GiB |
~7.5 |
| bandwidth series |
0.68 |
41s |
27.9 |
7.6 GiB |
~27.9 |
| edge scoreboard |
0.61 |
75s |
45.9 |
15.7 GiB |
~402 (24h variant) |
Top 10 shapes were only 5.85 of the 12.48 cores — the other 53% is the long tail of remaining page-cache entries. No single query is at fault; the refresh loop is.
Local instances also refresh faster than prod: metro-pair at 90s here versus prod's 107s, because a local cycle has less competing work and completes sooner.
Why this is a documented path, not a mistake
lake_dev_reader is not an ad-hoc credential. It is named in three places in this repo:
README.md:166 — REMOTE_CH_USER=lake_dev_reader
.env.example:176 — same
scripts/setup-remote-tables.sh:60 — printed as setup guidance
Nothing in any of them warns that running the stack against prod imposes production-scale load. A developer following the README exactly reproduces this.
Proposed fixes
1. Disable the page-cache worker for local --use-remote runs. This is the actual fix — the refresh loop is where all 12 cores went. --no-worker already exists (api/main.go:244, help text "for prod where it runs standalone"). A local instance pointed at prod almost never needs a background loop pre-warming 39 entries. Either default it on when --use-remote is set, or document it prominently.
2. Quota the user as a backstop, so no future instance can do this regardless of flags:
ALTER USER lake_dev_reader SETTINGS
max_execution_time = 60,
max_bytes_to_read = 20000000000; -- 20 GB
The two heaviest shapes read 49.4 and 44.2 GiB per run, so 20 GB stops them outright rather than slowing them.
⚠️ Check this against our own traffic first. Five of our pods share this credential — lake-api-558cf9695f-5ngwc, lake-api-79796bf5c8-vx9sg, lake-indexer-7f748ddb87-57n9j, lake-indexer-5999968459-7hhnf, lake-indexer-7559c77454-xxzl7 — for the remoteSecure() proxy tables described in CLAUDE.md:50. They totalled 2.29 cores over 7,824 queries, far lighter than the local instance, so a 20 GB ceiling should clear them. Verify before applying. The cleaner alternative is a separate per-developer user, leaving lake_dev_reader for our own services.
3. Warn in the README at the point where the credential is introduced.
Note on attribution
Identifying the source took several hours and three wrong conclusions. The trap worth recording: system.query_log.initial_address is self-reported by the client; address is the real TCP peer. The local pod reported 10.42.0.19, which looked like an in-VPC address and sent the investigation through AWS route tables, VPC peerings and PrivateLink endpoints — none of which were involved. address gave the true external IP immediately, and client_hostname named the pod. Query those two first.
Related: malbeclabs/infra#1123 — ClickHouse users are created by hand with no version control or audit trail, so there was no record of who provisioned this credential or when.
Summary
lake_dev_reader— the credential lake's own README tells developers to use for local development against production ClickHouse — consumed 12.5 cores, 27.8% of the entire prod cluster, for at least a day. One developer's local k3d stack, running the full page-cache refresh loop against prod data.It stopped at 20:53Z on 2026-08-20 when the developer shut it down. Nothing about the setup prevents the next person from reproducing it.
What happened
A local
apipod (api-fcfb7459c-fhjj9, k3d, pod CIDR10.42.0.0/16) connected aslake_dev_readerand ran lake's page-cache refresh loop — all ~39 entries — against prod ClickHouse.Measured over a 15-minute window while live:
api-fcfb7459c-fhjj9indexer-6694979854-j4d594.61 TiB per 15 minutes is ~18 TiB/hour. Over a 5.9h window it was 84,773 queries at 11.8 cores sustained. It had already been running the previous day at ~6.6 cores (16.8% of cluster), so it roughly doubled.
The queries were byte-for-byte prod's own most expensive page-cache entries, at the same cost per run:
jitter_improvement_pctTop 10 shapes were only 5.85 of the 12.48 cores — the other 53% is the long tail of remaining page-cache entries. No single query is at fault; the refresh loop is.
Local instances also refresh faster than prod: metro-pair at 90s here versus prod's 107s, because a local cycle has less competing work and completes sooner.
Why this is a documented path, not a mistake
lake_dev_readeris not an ad-hoc credential. It is named in three places in this repo:README.md:166—REMOTE_CH_USER=lake_dev_reader.env.example:176— samescripts/setup-remote-tables.sh:60— printed as setup guidanceNothing in any of them warns that running the stack against prod imposes production-scale load. A developer following the README exactly reproduces this.
Proposed fixes
1. Disable the page-cache worker for local
--use-remoteruns. This is the actual fix — the refresh loop is where all 12 cores went.--no-workeralready exists (api/main.go:244, help text "for prod where it runs standalone"). A local instance pointed at prod almost never needs a background loop pre-warming 39 entries. Either default it on when--use-remoteis set, or document it prominently.2. Quota the user as a backstop, so no future instance can do this regardless of flags:
The two heaviest shapes read 49.4 and 44.2 GiB per run, so 20 GB stops them outright rather than slowing them.
lake-api-558cf9695f-5ngwc,lake-api-79796bf5c8-vx9sg,lake-indexer-7f748ddb87-57n9j,lake-indexer-5999968459-7hhnf,lake-indexer-7559c77454-xxzl7— for theremoteSecure()proxy tables described inCLAUDE.md:50. They totalled 2.29 cores over 7,824 queries, far lighter than the local instance, so a 20 GB ceiling should clear them. Verify before applying. The cleaner alternative is a separate per-developer user, leavinglake_dev_readerfor our own services.3. Warn in the README at the point where the credential is introduced.
Note on attribution
Identifying the source took several hours and three wrong conclusions. The trap worth recording:
system.query_log.initial_addressis self-reported by the client;addressis the real TCP peer. The local pod reported10.42.0.19, which looked like an in-VPC address and sent the investigation through AWS route tables, VPC peerings and PrivateLink endpoints — none of which were involved.addressgave the true external IP immediately, andclient_hostnamenamed the pod. Query those two first.Related: malbeclabs/infra#1123 — ClickHouse users are created by hand with no version control or audit trail, so there was no record of who provisioned this credential or when.