<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Temir Kussainov</title>
    <description>The latest articles on DEV Community by Temir Kussainov (@protemir).</description>
    <link>https://dev.to/protemir</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4143530%2F43211e42-6857-44c4-9482-cc9ffaa1a556.JPG</url>
      <title>DEV Community: Temir Kussainov</title>
      <link>https://dev.to/protemir</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/protemir"/>
    <language>en</language>
    <item>
      <title>I built a CLI that tells you which MCP servers your repo actually needs</title>
      <dc:creator>Temir Kussainov</dc:creator>
      <pubDate>Tue, 20 Oct 2026 08:11:11 +0000</pubDate>
      <link>https://dev.to/protemir/i-built-a-cli-that-tells-you-which-mcp-servers-your-repo-actually-needs-2pb</link>
      <guid>https://dev.to/protemir/i-built-a-cli-that-tells-you-which-mcp-servers-your-repo-actually-needs-2pb</guid>
      <description>&lt;p&gt;Every week someone posts another "top 10 MCP servers for Claude Code" list. I kept installing things from those lists, forgetting what I had, and once I noticed a project of mine was still running &lt;code&gt;@modelcontextprotocol/server-postgres&lt;/code&gt;. That server was archived in May 2025, and Datadog showed its read-only mode can be bypassed with &lt;code&gt;COMMIT; ...&lt;/code&gt;. Nobody is going to fix it.&lt;/p&gt;

&lt;p&gt;So I wrote &lt;a href="https://github.com/Protemir/stackhint" rel="noopener noreferrer"&gt;stackhint&lt;/a&gt;. It looks at a repository, checks what is already configured in Claude Code and Codex, and suggests the MCP servers, plugins, skills and CLIs that fit. It explains why each one fits, and it can add a server to your config after showing you the exact change.&lt;/p&gt;

&lt;p&gt;It runs locally, has no dependencies and sends nothing anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ npx -y github:Protemir/stackhint recommend --task "orders query is slow"
Detected: PostgreSQL, C#, .NET, Redis, GitHub Actions, Docker, EF Core, Jira, OpenAPI, GitHub

1. Recommendation: Postgres MCP Pro  fit 67/100, confidence high
Why now: Service image postgres:17 (docker-compose.yml); your task mentions "query"; 9 database migrations.
Permissions: stackhint uses --access-mode=restricted (read-only transactions).
Actions:
  - stackhint install postgres-mcp-pro --host claude - shows the exact change and asks before writing
  - stackhint try postgres-mcp-pro --host claude - one session without installing
  - stackhint dismiss postgres-mcp-pro --for 14d - remind me later

2. Recommendation: Microsoft Learn MCP Server  fit 58/100, confidence high
Why now: Solution file (DotnetShop.sln); ASP.NET Core web SDK.

3. Recommendation: Atlassian Rovo MCP Server  fit 52/100, confidence high
Why now: Links to acme-demo.atlassian.net Jira (README.md).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line in "Why now" points at a file. If your README only says "we might try Kubernetes one day", you get no Kubernetes suggestion. It needs a manifest, a compose image, a dependency or a link.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it decides
&lt;/h2&gt;

&lt;p&gt;The analyzer reads the files that usually give a stack away: &lt;code&gt;*.csproj&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;go.mod&lt;/code&gt;, compose files, Kubernetes and Terraform files, CI configs, &lt;code&gt;.http&lt;/code&gt; files, the key names in &lt;code&gt;.env.example&lt;/code&gt;, and links in the README. It also reads git remotes and recent commit subjects. Issue keys like &lt;code&gt;SHOP-123&lt;/code&gt; hint at Jira, and twelve "add migration" commits hint that a project skill would help. It never opens &lt;code&gt;.env&lt;/code&gt;, private keys or secret settings files.&lt;/p&gt;

&lt;p&gt;Then it reads your setup: &lt;code&gt;~/.claude.json&lt;/code&gt;, &lt;code&gt;.mcp.json&lt;/code&gt;, enabled Claude Code plugins and the servers they bundle, &lt;code&gt;~/.codex/config.toml&lt;/code&gt;, Codex plugins, skills. If you already have GitHub's MCP server, or &lt;code&gt;gh&lt;/code&gt; on your PATH, it won't push another GitHub integration on you.&lt;/p&gt;

&lt;p&gt;The ranking is a plain formula: relevance from the evidence, times value, minus setup cost and risk. The numbers are my estimates, and the tool says so in its output. &lt;code&gt;stackhint explain &amp;lt;id&amp;gt;&lt;/code&gt; prints the whole breakdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  The registry is the actual work
&lt;/h2&gt;

&lt;p&gt;The hard part wasn't the code. It was checking 68 entries by hand: the endpoint, the package name, the read-only flag, whether the project is still maintained. A few things I didn't expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Atlassian's recommended endpoint is now &lt;code&gt;/v2/mcp&lt;/code&gt;, and the old &lt;code&gt;/v1/sse&lt;/code&gt; stopped working in June.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubernetes-mcp-server&lt;/code&gt; removed its &lt;code&gt;--read-only&lt;/code&gt; flag on main. The next release will reject it, so stackhint pins 0.0.67, where the flag still works.&lt;/li&gt;
&lt;li&gt;Microsoft deleted its MSSQL MCP sample as unsafe in June. The supported route is Data API builder.&lt;/li&gt;
&lt;li&gt;CircleCI's npm package, Notion's local server, Semgrep's standalone repo and Cloudflare's per-product servers are all deprecated or archived.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;stackhint knows the old ones too. If it finds one in your config, &lt;code&gt;stackhint check&lt;/code&gt; tells you what to replace it with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing without surprises
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;stackhint install&lt;/code&gt; prints a plan first: where it will write, the diff, what the server can access, which environment variables it needs (by name only) and how to undo it. It asks, saves a backup, writes atomically, parses the file back and restores the original if anything looks wrong. &lt;code&gt;stackhint rollback &amp;lt;id&amp;gt;&lt;/code&gt; puts the file back byte for byte.&lt;/p&gt;

&lt;p&gt;A few rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read-only variants are the default whenever the vendor offers one.&lt;/li&gt;
&lt;li&gt;Tokens are never written. Claude Code gets &lt;code&gt;${GITHUB_PERSONAL_ACCESS_TOKEN}&lt;/code&gt;, and Codex gets &lt;code&gt;bearer_token_env_var&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For Codex it appends one &lt;code&gt;[mcp_servers.x]&lt;/code&gt; table and leaves the rest of &lt;code&gt;config.toml&lt;/code&gt; alone. &lt;code&gt;codex mcp add&lt;/code&gt; rewrites the whole table and drops your comments.&lt;/li&gt;
&lt;li&gt;For Claude Code's user and local scopes it calls &lt;code&gt;claude mcp add&lt;/code&gt;, because Claude rewrites &lt;code&gt;~/.claude.json&lt;/code&gt; all the time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things I learned the hard way
&lt;/h2&gt;

&lt;p&gt;My unit tests were green, then the first run against the real &lt;code&gt;claude&lt;/code&gt; binary failed. &lt;code&gt;claude mcp add&lt;/code&gt; takes &lt;code&gt;--env&lt;/code&gt; with several values, so &lt;code&gt;--env KUBECONFIG=... kubernetes -- npx ...&lt;/code&gt; swallows the server name. The fix is to put &lt;code&gt;--env&lt;/code&gt; and &lt;code&gt;--header&lt;/code&gt; before &lt;code&gt;--transport&lt;/code&gt;. There is now a test that runs the real Claude Code and Codex CLIs in CI against configs stackhint wrote.&lt;/p&gt;

&lt;p&gt;On native Windows both clients need &lt;code&gt;cmd /c npx ...&lt;/code&gt; for npx-based servers, and the tool now writes it that way.&lt;/p&gt;

&lt;p&gt;Before the release I put the code through three rounds of review. The worst find: &lt;code&gt;git log&lt;/code&gt; inside a scanned repository reads that repository's own &lt;code&gt;.git/config&lt;/code&gt;, and with &lt;code&gt;log.showSignature&lt;/code&gt; plus a custom &lt;code&gt;gpg.program&lt;/code&gt; it runs whatever program the repo points at. So "just read the commit history" could execute code from a repo you only wanted to look at. stackhint now switches all of that off, and a test with a fake signed commit checks it stays off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Code and Codex in one repo
&lt;/h2&gt;

&lt;p&gt;The same GitHub repo works as a plugin marketplace for both clients. It ships a skill, so you can ask "which MCP servers would help here?", and two hooks. In the default balanced mode, the session-start hook passes up to two high-confidence suggestions to the agent, at most once a day per project. You can turn it off with &lt;code&gt;stackhint config set mode manual&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin marketplace add Protemir/stackhint
/plugin install stackhint@stackhint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;codex plugin marketplace add Protemir/stackhint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a read-only MCP server (&lt;code&gt;stackhint mcp&lt;/code&gt;) if you prefer that. On purpose, it has no install tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update: 0.2.0
&lt;/h2&gt;

&lt;p&gt;The same day I released 0.2.0. Three more review passes found 34 problems, and a review of the fixes found 9 more; each one now has a test. The worst: installing into a &lt;code&gt;config.toml&lt;/code&gt; saved in a non-UTF-8 encoding could corrupt it, and secrets from a team's &lt;code&gt;.vscode/mcp.json&lt;/code&gt; could end up in &lt;code&gt;scan --format json&lt;/code&gt; output.&lt;/p&gt;

&lt;p&gt;The registry grew from 68 to 143 entries: servers for Next.js, Angular, Svelte, Expo, Flutter, Unity and Unreal, hosting like Netlify and Fly.io, observability like Datadog, and all 13 official LSP plugins. The independent check of the new entries caught a nice one: monday.com's MCP server ignores &lt;code&gt;--read-only&lt;/code&gt; unless you pass a value, so "read-only" quietly meant full access. The scan is also about three times faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; github:Protemir/stackhint recommend
npx &lt;span class="nt"&gt;-y&lt;/span&gt; github:Protemir/stackhint check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's MIT and on GitHub: &lt;a href="https://github.com/Protemir/stackhint" rel="noopener noreferrer"&gt;https://github.com/Protemir/stackhint&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The registry is where help matters most. If your stack isn't covered, or a vendor changed an endpoint, open an issue or a PR with the source you checked.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>opensource</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Syncing a browser extension's settings without a server: three reviews, three ways to lose data</title>
      <dc:creator>Temir Kussainov</dc:creator>
      <pubDate>Sun, 18 Oct 2026 10:57:04 +0000</pubDate>
      <link>https://dev.to/protemir/syncing-a-browser-extensions-settings-without-a-server-three-reviews-three-ways-to-lose-data-16a4</link>
      <guid>https://dev.to/protemir/syncing-a-browser-extensions-settings-without-a-server-three-reviews-three-ways-to-lose-data-16a4</guid>
      <description>&lt;p&gt;Headerwise is a header editor for Chrome and Edge I wrote after ModHeader got pulled from the stores. People asked for one thing ModHeader charged for: having the same profiles on the work laptop and the home desktop.&lt;/p&gt;

&lt;p&gt;I didn't want a server. The extension's manifest forbids network requests (&lt;code&gt;connect-src 'none'&lt;/code&gt;), and I'd like to keep it that way. Luckily the browser already has sync: &lt;code&gt;chrome.storage.sync&lt;/code&gt; goes through the Google (or Microsoft, in Edge) account the browser is signed in to, and the extension never talks to anything itself.&lt;/p&gt;

&lt;p&gt;So the job looked small: copy the profiles into &lt;code&gt;chrome.storage.sync&lt;/code&gt;, copy them back out on the other computer. It took four rounds of review to get right, and the first three each found a way to lose someone's profiles. Here's what broke, in case you're about to do the same thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  The limits shape the design
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;chrome.storage.sync&lt;/code&gt; gives you 100 KB in total, 8 KB per item, 512 items, and 120 writes a minute. A profile with a few headers is a few hundred bytes, so one item per profile works, and it means two computers editing different profiles never touch the same item.&lt;/p&gt;

&lt;p&gt;Some things shouldn't sync at all: whether a profile is switched on (turning on a production header at work shouldn't turn it on at home), "only in this tab", the timer, and secret values. Tokens and cookies are stripped before anything leaves the computer, and each computer keeps its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version 1: push and pull. Lost edits.
&lt;/h3&gt;

&lt;p&gt;Push: write every profile and an index of ids. Pull: replace local profiles with what's in sync; anything I'd seen before that is gone from the index was deleted elsewhere.&lt;/p&gt;

&lt;p&gt;The review found the problem fast. A write to &lt;code&gt;chrome.storage.sync&lt;/code&gt; fires &lt;code&gt;onChanged&lt;/code&gt; on the same computer too, so a pull follows every push. If you type again between the push and that pull, the pull puts back the version you just pushed. Your text jumps back while you type.&lt;/p&gt;

&lt;p&gt;Worse, "gone from the index" isn't the same as "deleted". The browser keeps the last write of each key. If computer B writes its index before it has seen the profile A just created, A's profile is missing from the index, and A dutifully deletes it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version 2: a three-way merge. Lost edits, differently.
&lt;/h3&gt;

&lt;p&gt;The fix for the first problem is old: keep a base. Each computer remembers every profile as it was when it and sync last agreed. Then a round can tell who changed what:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local equals base, sync differs: changed elsewhere, take it in;&lt;/li&gt;
&lt;li&gt;sync equals base, local differs: changed here, send it;&lt;/li&gt;
&lt;li&gt;both differ: changed on both, keep both versions (the other one as a copy, so nothing disappears silently).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And deletions became explicit marks instead of "missing from the index". Profiles exist as items; the order is just an order.&lt;/p&gt;

&lt;p&gt;The second review modelled each computer with its own copy of sync storage and a last-write-wins server, which is how the browser actually behaves, and found that "local equals base" doesn't mean what I thought. A edits a profile and sends it, so A's base is A's version. B, which hadn't seen it yet, sends its own edit. Last write wins: B's. On A, local equals base, sync differs, so "changed elsewhere": A takes B's version and A's edit is gone, with no copy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version 3: remember the parent. Old copies win.
&lt;/h3&gt;

&lt;p&gt;So every item got a &lt;code&gt;parent&lt;/code&gt;: a fingerprint of the version it was edited from. If what arrives wasn't built on what I last agreed on, both versions are kept.&lt;/p&gt;

&lt;p&gt;The third review found that this keeps far too much. A computer that was asleep misses versions all the time. A goes v0 → v1 → v2, B wakes up and sees only v2, whose parent is v1. B's base is v0, so "not built on what I have", and B kept its v0 under the id, sent it, and turned v2 into a copy. Every computer ended up back on v0. With three computers it was worse: each one saw a parent mismatch and resent its own version, forever, until the browser's write quota ran out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version 4: only send what changed here
&lt;/h3&gt;

&lt;p&gt;The rule that finally held: a computer sends a profile only if it changed on that computer. If it changed nothing, it takes the newest version, whatever it missed in between. That alone makes every round settle, because writes only follow edits.&lt;/p&gt;

&lt;p&gt;The parent check stays, but only for the one case it's for: this computer wrote the version it last agreed on, and something replaced it without having seen it. Then (and only then) both are kept. The base remembers which versions it sent itself for exactly this.&lt;/p&gt;

&lt;p&gt;The fourth review ran all 18 timelines from the first three, plus new ones aimed at this rule, and found nothing in the merge. It did find that the popup could drop a token pasted in the same quarter second a change arrived, which is a different story.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd tell myself at the start
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Model the browser honestly in tests: separate copies of sync per computer, last write wins per key, rounds that don't see each other. My first tests shared one store and ran rounds in a tidy order, and everything passed.&lt;/li&gt;
&lt;li&gt;"Missing" is not "deleted". Write deletions down.&lt;/li&gt;
&lt;li&gt;A base tells you who changed what; a parent tells you who saw what. You need both, and only the computer that wrote a version may defend it.&lt;/li&gt;
&lt;li&gt;Every round should be able to run again and change nothing. If it can't, it will eventually run forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code is MIT and the merge is one file, &lt;code&gt;src/core/sync.ts&lt;/code&gt;, with the timelines as tests in &lt;code&gt;tests/sync.test.ts&lt;/code&gt;: &lt;a href="https://github.com/Protemir/headerwise" rel="noopener noreferrer"&gt;https://github.com/Protemir/headerwise&lt;/a&gt;. Headerwise itself is free in the Chrome Web Store: &lt;a href="https://chromewebstore.google.com/detail/jedoaeaapdofoldmkacjbpojbnpapkna" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/jedoaeaapdofoldmkacjbpojbnpapkna&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've done sync over &lt;code&gt;chrome.storage.sync&lt;/code&gt; differently, I'd like to hear how.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>chrome</category>
      <category>extensions</category>
    </item>
    <item>
      <title>ModHeader in Selenium stopped working. Here's what sets HTTP headers in tests in 2026</title>
      <dc:creator>Temir Kussainov</dc:creator>
      <pubDate>Mon, 12 Oct 2026 10:00:59 +0000</pubDate>
      <link>https://dev.to/protemir/modheader-in-selenium-stopped-working-heres-what-sets-http-headers-in-tests-in-2026-1jgg</link>
      <guid>https://dev.to/protemir/modheader-in-selenium-stopped-working-heres-what-sets-http-headers-in-tests-in-2026-1jgg</guid>
      <description>&lt;p&gt;&lt;em&gt;Updated October 18, 2026: Chrome for Testing 155 stopped loading extensions from &lt;code&gt;--load-extension&lt;/code&gt;, so the Selenium examples below now install the extension over WebDriver BiDi.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If your browser tests set headers through ModHeader (&lt;code&gt;chrome-modheader&lt;/code&gt; on npm, &lt;code&gt;modheader_selenium&lt;/code&gt; for Python and Java), they probably broke this year, and not only because ModHeader was pulled from the stores in July. Two other things happened:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The extension bundled in &lt;code&gt;chrome-modheader&lt;/code&gt; is ModHeader 4.3.2, a Manifest V2 build. Chrome turned off MV2 extensions, so it never loads, and the package hasn't been updated since.&lt;/li&gt;
&lt;li&gt;Google Chrome 137 and newer ignore &lt;code&gt;--load-extension&lt;/code&gt;, which is how ChromeDriver installs any extension you pass with &lt;code&gt;addExtensions&lt;/code&gt;. Chrome for Testing followed in version 155 (October 2026), so Selenium now installs extensions over WebDriver BiDi instead, below.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The package still gets about 150k downloads a month, mostly from CI, so a lot of pipelines are installing something that does nothing. Here's what works now, from the simplest option up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request headers on every request: no extension needed
&lt;/h2&gt;

&lt;p&gt;Playwright has it built in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;extraHTTPHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Env&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;staging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Selenium 4 with Chrome can ask the DevTools protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_cdp_cmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Network.enable&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute_cdp_cmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Network.setExtraHTTPHeaders&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;X-Env&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;staging&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both add the headers to every request from that context or tab, which covers most "log in as a test user" and "route to staging" cases. They don't change response headers, can't remove a header the browser sends, and don't limit headers to some URLs (in Playwright, &lt;code&gt;page.route()&lt;/code&gt; can, at the cost of handling each request in your test).&lt;/p&gt;

&lt;h2&gt;
  
  
  Response headers, removing headers, some URLs only
&lt;/h2&gt;

&lt;p&gt;That's where an extension still earns its place: CORS or CSP response headers, stripping &lt;code&gt;Referer&lt;/code&gt; or &lt;code&gt;Cookie&lt;/code&gt;, a header only for &lt;code&gt;api.example.com&lt;/code&gt;. A proxy like mitmproxy can do all of that too, but then you manage certificates, and on a cloud grid that's often not an option.&lt;/p&gt;

&lt;p&gt;Disclosure: I maintain &lt;a href="https://github.com/Protemir/headerwise" rel="noopener noreferrer"&gt;Headerwise&lt;/a&gt;, an open source (MIT) MV3 header extension, and it has an automation build for this. It has a fixed extension id, and your test sets headers by opening one URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chrome-extension://ogjbgamdhnjnagcdgboifgmhlgddcdce/automation.html?X-Env=staging&amp;amp;res:Access-Control-Allow-Origin=*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page waits until Chrome has the rules and then reports &lt;code&gt;ready&lt;/code&gt;. Playwright, with its own Chromium:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/path/to/headerwise-automation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launchPersistentContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chromium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`--disable-extensions-except=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`--load-extension=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chrome-extension://ogjbgamdhnjnagcdgboifgmhlgddcdce/automation.html?X-Env=staging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForFunction&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Selenium installs it over WebDriver BiDi (checked with Selenium 4.51; the path is the unzipped folder):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ChromeOptions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable_bidi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable_webextensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;webextension&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/path/to/headerwise-automation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  If your tests are written for ModHeader
&lt;/h2&gt;

&lt;p&gt;The automation build also answers ModHeader's own &lt;code&gt;webdriver.modheader.com/add&lt;/code&gt;, &lt;code&gt;/clear&lt;/code&gt; and &lt;code&gt;/load&lt;/code&gt; URLs (nothing is sent to modheader.com: the browser redirects them to the extension before any request leaves) and titles the page "Done", like ModHeader did. So a test like this only changes how the extension gets in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getAddHeaderUrl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chrome-modheader&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;addArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--enable-unsafe-extension-debugging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--remote-debugging-pipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// was addExtensions(getExtension())&lt;/span&gt;
&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enableBidi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;forBrowser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chrome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setChromeOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBidi&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webExtension.install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;extensionData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/path/to/headerwise-automation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getAddHeaderUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Env&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;staging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;until&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;titleIs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a grid that takes the extension as a base64 &lt;code&gt;.crx&lt;/code&gt; in the capabilities, ask for Chrome for Testing 154 for now (BrowserStack calls it &lt;code&gt;chromefortesting&lt;/code&gt;): newer versions don't install a packed extension that way, and BiDi installs only a folder on the machine that runs the browser.&lt;/p&gt;

&lt;p&gt;Setup for Selenium, Playwright and Puppeteer: &lt;a href="https://protemir.github.io/headerwise/automation.html" rel="noopener noreferrer"&gt;https://protemir.github.io/headerwise/automation.html&lt;/a&gt;. CI runs those snippets on every change, including one test written for ModHeader with only the extension swapped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one to pick
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Same request headers everywhere: &lt;code&gt;extraHTTPHeaders&lt;/code&gt; or CDP. Nothing to install.&lt;/li&gt;
&lt;li&gt;Response headers, removals, per-URL rules, or a lot of existing ModHeader URLs in your tests: an extension (Headerwise or whatever you trust).&lt;/li&gt;
&lt;li&gt;Firefox, or traffic outside the browser too: a proxy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If ModHeader itself got switched off in your everyday browser too, I wrote up &lt;a href="https://dev.to/protemir/modheader-got-pulled-from-the-chrome-web-store-heres-how-to-get-your-profiles-back-1mc0"&gt;how to get your profiles back&lt;/a&gt;. And if something in your test setup doesn't fit any of these, tell me in the comments.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>selenium</category>
      <category>playwright</category>
      <category>webdev</category>
    </item>
    <item>
      <title>ModHeader got pulled from the Chrome Web Store. Here's how to get your profiles back</title>
      <dc:creator>Temir Kussainov</dc:creator>
      <pubDate>Mon, 12 Oct 2026 09:58:17 +0000</pubDate>
      <link>https://dev.to/protemir/modheader-got-pulled-from-the-chrome-web-store-heres-how-to-get-your-profiles-back-1mc0</link>
      <guid>https://dev.to/protemir/modheader-got-pulled-from-the-chrome-web-store-heres-how-to-get-your-profiles-back-1mc0</guid>
      <description>&lt;p&gt;If you opened Chrome recently and ModHeader was gone, or sitting there turned off, you're not alone. Microsoft pulled it from Edge Add-ons on July 3, 2026 and Google followed on July 10. Version 7.0.18 shipped a dormant collector for the domains you visit, plus install and update pings to a third-party server (&lt;a href="https://thehackernews.com/2026/07/google-and-microsoft-pull-modheader.html" rel="noopener noreferrer"&gt;The Hacker News has the details&lt;/a&gt;). The collector reportedly never switched on, but the listings are gone either way.&lt;/p&gt;

&lt;p&gt;The annoying part: once Chrome turns an extension off, you can't open it, so the Export button is gone too. If you had a dozen profiles with tokens, URL filters and staging headers, that hurts.&lt;/p&gt;

&lt;p&gt;Your profiles are still on your disk, though. Chrome keeps every extension's &lt;code&gt;chrome.storage.local&lt;/code&gt; in a LevelDB folder under your browser profile, and ModHeader keeps everything under one key called &lt;code&gt;profiles&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the folder is
&lt;/h2&gt;

&lt;p&gt;ModHeader's id in the Chrome Web Store was &lt;code&gt;idgpnmonknjnojddfkpgkljpfnnfcklj&lt;/code&gt; (on Edge Add-ons it was &lt;code&gt;opgbiafapkbbnbnjcdomjaghbckfkglc&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Windows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%LOCALAPPDATA%\Google\Chrome\User Data\Default\Local Extension Settings\idgpnmonknjnojddfkpgkljpfnnfcklj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;macOS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/Library/Application Support/Google/Chrome/Default/Local Extension Settings/idgpnmonknjnojddfkpgkljpfnnfcklj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Linux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.config/google-chrome/Default/Local Extension Settings/idgpnmonknjnojddfkpgkljpfnnfcklj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use several Chrome profiles, open &lt;code&gt;chrome://version&lt;/code&gt; in the one where you had ModHeader and use its "Profile Path" instead of &lt;code&gt;Default&lt;/code&gt;. If the folder isn't there, Chrome may have deleted it when it removed the extension, and then there's nothing left to recover. So do this soon, and don't click "Remove" on ModHeader before you have your profiles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: a command line tool
&lt;/h2&gt;

&lt;p&gt;Requestly published a small CLI that finds the folder and writes the same JSON file ModHeader's own export would have: &lt;code&gt;npx @requestly/modheader-export-backup&lt;/code&gt;. You need Node 20 or newer. The file works with any tool that imports ModHeader JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: read it in the browser
&lt;/h2&gt;

&lt;p&gt;This is partly why I built Headerwise, a free and open source header editor. Its "Move from ModHeader" page lets you pick that folder and reads the LevelDB files right in the page (I wrote a small LevelDB and snappy reader in TypeScript for it), with nothing uploaded. Headers, URL filters, site, type and method filters, URL replacements and cookie rules come over, and the profile you had selected stays on. &lt;a href="https://chromewebstore.google.com/detail/jedoaeaapdofoldmkacjbpojbnpapkna" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt;, &lt;a href="https://github.com/Protemir/headerwise" rel="noopener noreferrer"&gt;source&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: by hand
&lt;/h2&gt;

&lt;p&gt;If you'd rather not run anything: copy the folder somewhere safe first, then open the copy with any LevelDB viewer and read the value of the &lt;code&gt;profiles&lt;/code&gt; key. It's plain JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  Whatever you pick
&lt;/h2&gt;

&lt;p&gt;Exports often hold Authorization tokens and session cookies. Treat the file like a password: don't commit it, and rotate anything sensitive you kept in ModHeader if you're not sure what happened to it.&lt;/p&gt;

&lt;p&gt;If you used ModHeader in Selenium or Playwright tests, that broke separately: its test package ships a Manifest V2 build that current Chrome won't load. &lt;a href="https://protemir.github.io/headerwise/automation.html" rel="noopener noreferrer"&gt;Here's what works now&lt;/a&gt;. And if your setup needs something none of these handle, tell me in the comments.&lt;/p&gt;

</description>
      <category>chrome</category>
      <category>webdev</category>
      <category>privacy</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Why ClickHouse® fills the disk in self-hosted Langfuse and SigNoz, and how to fix it</title>
      <dc:creator>Temir Kussainov</dc:creator>
      <pubDate>Sat, 26 Sep 2026 04:40:18 +0000</pubDate>
      <link>https://dev.to/protemir/why-clickhouser-fills-the-disk-in-self-hosted-langfuse-and-signoz-and-how-to-fix-it-3l4c</link>
      <guid>https://dev.to/protemir/why-clickhouser-fills-the-disk-in-self-hosted-langfuse-and-signoz-and-how-to-fix-it-3l4c</guid>
      <description>&lt;p&gt;&lt;em&gt;Also in &lt;a href="https://diskvet.dev/es/guide/" rel="noopener noreferrer"&gt;Español&lt;/a&gt; · &lt;a href="https://diskvet.dev/pt/guide/" rel="noopener noreferrer"&gt;Português&lt;/a&gt; · &lt;a href="https://diskvet.dev/ru/guide/" rel="noopener noreferrer"&gt;Русский&lt;/a&gt; · &lt;a href="https://diskvet.dev/ja/guide/" rel="noopener noreferrer"&gt;日本語&lt;/a&gt; · &lt;a href="https://diskvet.dev/ko/guide/" rel="noopener noreferrer"&gt;한국어&lt;/a&gt; · &lt;a href="https://diskvet.dev/zh/guide/" rel="noopener noreferrer"&gt;中文&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your Langfuse or SigNoz server is running out of disk, but your traces are small. In most public reports the space goes to ClickHouse® itself: to its own log tables, such as &lt;code&gt;system.trace_log&lt;/code&gt; and &lt;code&gt;system.text_log&lt;/code&gt;, which have no size limit by default. ClickStack has the same problem.&lt;/p&gt;

&lt;p&gt;This guide shows how to confirm it with one read-only query, free the space now, and stop it from coming back without hitting the known traps. You don't need any extra tools.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tested on stock &lt;code&gt;clickhouse/clickhouse-server&lt;/code&gt; images 24.8, 25.12 and 26.9 (the SigNoz commands on 25.5 and 25.12), with the ClickHouse service and config taken from Langfuse's and SigNoz's compose files.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check.&lt;/strong&gt; Run one read-only query on &lt;code&gt;system.parts&lt;/code&gt;. If &lt;code&gt;system.*_log&lt;/code&gt; tables are bigger than your data, ClickHouse's own logs are the problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free space now.&lt;/strong&gt; &lt;code&gt;TRUNCATE&lt;/code&gt; the big logs. For a table over 50 GB, add &lt;code&gt;SETTINGS max_table_size_to_drop = 0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop it coming back.&lt;/strong&gt; Add a &lt;code&gt;config.d&lt;/code&gt; file with a TTL for each log (for &lt;code&gt;opentelemetry_span_log&lt;/code&gt; the TTL goes inside &lt;code&gt;&amp;lt;engine&amp;gt;&lt;/code&gt;), then restart ClickHouse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean up.&lt;/strong&gt; Drop the &lt;code&gt;*_log_0&lt;/code&gt;, &lt;code&gt;*_log_1&lt;/code&gt; copies that the restart leaves behind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Still full?&lt;/strong&gt; Check Docker's container logs, deleted rows and stuck parts. On Langfuse, update Langfuse before you move ClickHouse to 26.8+.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. What is using the disk? Check ClickHouse disk usage by table
&lt;/h2&gt;

&lt;p&gt;Save this query as &lt;code&gt;size.sql&lt;/code&gt;. It lists the biggest tables by size on disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;formatReadableSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes_on_disk&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes_on_disk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Langfuse (docker compose).&lt;/strong&gt; Run this in the folder with Langfuse's &lt;code&gt;docker-compose.yml&lt;/code&gt;. Its compose file already sets &lt;code&gt;CLICKHOUSE_USER&lt;/code&gt; and &lt;code&gt;CLICKHOUSE_PASSWORD&lt;/code&gt; inside the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; clickhouse sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'clickhouse-client --user "$CLICKHOUSE_USER" --password "$CLICKHOUSE_PASSWORD" --readonly=1 --format PrettyCompact'&lt;/span&gt; &amp;lt; size.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SigNoz (docker compose).&lt;/strong&gt; The container is usually called &lt;code&gt;signoz-clickhouse&lt;/code&gt; (check with &lt;code&gt;docker ps&lt;/code&gt;). SigNoz's &lt;code&gt;users.xml&lt;/code&gt; leaves the &lt;code&gt;default&lt;/code&gt; user without a password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; signoz-clickhouse clickhouse-client &lt;span class="nt"&gt;--readonly&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;--format&lt;/span&gt; PrettyCompact &amp;lt; size.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--readonly=1&lt;/code&gt; makes the session read-only: ClickHouse refuses any change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to read it.&lt;/strong&gt; Compare the &lt;code&gt;system&lt;/code&gt; rows with your own data (&lt;code&gt;default&lt;/code&gt; for Langfuse, &lt;code&gt;signoz_*&lt;/code&gt; for SigNoz). If &lt;code&gt;system.trace_log&lt;/code&gt; or &lt;code&gt;system.text_log&lt;/code&gt; is at the top, go on to section 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. How do I free the disk space right now?
&lt;/h2&gt;

&lt;p&gt;Open a client with write access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Langfuse&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;clickhouse sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'clickhouse-client --user "$CLICKHOUSE_USER" --password "$CLICKHOUSE_PASSWORD"'&lt;/span&gt;
&lt;span class="c"&gt;# SigNoz&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; signoz-clickhouse clickhouse-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty the big logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;TRUNCATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trace_log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;TRUNCATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;TRUNCATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metric_log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;TRUNCATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This deletes only ClickHouse's own diagnostics, not your traces, and needs no restart.&lt;/p&gt;

&lt;p&gt;Langfuse reads &lt;code&gt;system.query_log&lt;/code&gt; to follow some of its own queries (&lt;a href="https://github.com/langfuse/langfuse/issues/13123" rel="noopener noreferrer"&gt;#13123&lt;/a&gt;). So empty it, but don't switch it off.&lt;/p&gt;

&lt;h3&gt;
  
  
  TRUNCATE fails with Code 359? Tables over 50 GB
&lt;/h3&gt;

&lt;p&gt;ClickHouse won't drop or truncate a table bigger than &lt;code&gt;max_table_size_to_drop&lt;/code&gt; (default 50,000,000,000 bytes, about 46.6 GiB). The limit applies to &lt;code&gt;TRUNCATE&lt;/code&gt; too, and a bigger table fails with &lt;code&gt;Code: 359 ... (TABLE_SIZE_EXCEEDS_MAX_DROP_SIZE_LIMIT)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Big logs do hit it: the &lt;code&gt;text_log&lt;/code&gt; in &lt;a href="https://github.com/orgs/langfuse/discussions/15024" rel="noopener noreferrer"&gt;Langfuse discussion #15024&lt;/a&gt; was 59.20 GiB. There are two ways around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A.&lt;/strong&gt; Lift the limit for one statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ClickHouse 23.12+: lift the limit for this one statement&lt;/span&gt;
&lt;span class="k"&gt;TRUNCATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_log&lt;/span&gt; &lt;span class="n"&gt;SETTINGS&lt;/span&gt; &lt;span class="n"&gt;max_table_size_to_drop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option B.&lt;/strong&gt; Create the one-time flag file, then run the plain &lt;code&gt;TRUNCATE&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;clickhouse sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'touch /var/lib/clickhouse/flags/force_drop_table &amp;amp;&amp;amp; chmod 666 /var/lib/clickhouse/flags/force_drop_table'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(SigNoz: &lt;code&gt;docker exec signoz-clickhouse sh -c '...'&lt;/code&gt; with the same command.) ClickHouse deletes the flag after the first &lt;code&gt;DROP&lt;/code&gt; or &lt;code&gt;TRUNCATE&lt;/code&gt; that needs it, so create it again before the next big table.&lt;/p&gt;

&lt;p&gt;The space is back now, but the logs start growing again right away. Section 4 stops that. Section 3 explains why it happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Why are trace_log and text_log so big?
&lt;/h2&gt;

&lt;p&gt;ClickHouse writes its own diagnostics into tables in the &lt;code&gt;system&lt;/code&gt; database: &lt;code&gt;query_log&lt;/code&gt;, &lt;code&gt;trace_log&lt;/code&gt;, &lt;code&gt;text_log&lt;/code&gt;, &lt;code&gt;metric_log&lt;/code&gt; and more. The &lt;a href="https://clickhouse.com/docs/reference/system-tables/overview" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; says it plainly: "By default, table growth is unlimited."&lt;/p&gt;

&lt;p&gt;Recent &lt;a href="https://github.com/ClickHouse/ClickHouse/blob/master/programs/server/config.xml" rel="noopener noreferrer"&gt;default configs&lt;/a&gt; set a TTL only for a few small logs (since 25.9, &lt;code&gt;processors_profile_log&lt;/code&gt; keeps 30 days), not for the big ones. Two defaults make the big ones grow fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the query profiler is on and writes stack samples of running queries into &lt;code&gt;trace_log&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;text_log&lt;/code&gt; stores the server log at &lt;code&gt;trace&lt;/code&gt; level.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Public examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Langfuse &lt;a href="https://github.com/langfuse/langfuse/issues/13123" rel="noopener noreferrer"&gt;#13123&lt;/a&gt;:&lt;/strong&gt; &lt;code&gt;system.trace_log&lt;/code&gt; was 66.86 GiB, while the Langfuse tables listed there (&lt;code&gt;traces&lt;/code&gt;, &lt;code&gt;observations&lt;/code&gt;, &lt;code&gt;scores&lt;/code&gt;) took about 51 MiB together. The maintainers decided not to override ClickHouse's defaults and added a &lt;a href="https://langfuse.com/faq/all/reduce-clickhouse-disk-size" rel="noopener noreferrer"&gt;FAQ page&lt;/a&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SigNoz &lt;a href="https://github.com/SigNoz/signoz/issues/12050" rel="noopener noreferrer"&gt;#12050&lt;/a&gt;:&lt;/strong&gt; more than 80 GB of system logs (including a &lt;code&gt;trace_log_0&lt;/code&gt; copy) next to less than 500 MB of telemetry. Truncating the &lt;code&gt;system.*&lt;/code&gt; tables freed about 80 GB. SigNoz's new Foundry installer sets TTLs; older docker compose installs don't have them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ClickStack Helm chart &lt;a href="https://github.com/ClickHouse/ClickStack-helm-charts/pull/275" rel="noopener noreferrer"&gt;PR #275&lt;/a&gt;:&lt;/strong&gt; a 10 Gi volume hit 100% after 10 days, with 5.2 GB of trace-level server logs and a 3.7 GB &lt;code&gt;system.text_log&lt;/code&gt; against ~70 MB of telemetry. The PR, merged in September 2026, adds a 7-day TTL.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. How do I set a TTL on ClickHouse system logs?
&lt;/h2&gt;

&lt;p&gt;A TTL makes ClickHouse delete old log rows by itself. Set it &lt;strong&gt;after&lt;/strong&gt; the &lt;code&gt;TRUNCATE&lt;/code&gt; from section 2, because of how ClickHouse applies it.&lt;/p&gt;

&lt;p&gt;When you add a TTL and restart, ClickHouse doesn't shrink the old table. It renames it to &lt;code&gt;trace_log_0&lt;/code&gt; with all its rows and creates a new one with the TTL. If you truncated first, these copies are tiny, and step 4 drops them.&lt;/p&gt;

&lt;p&gt;Run the SQL below in the same write-access client as in section 2.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: which logs have no TTL?
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;formatReadableSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine_full&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'TTL [^S]*'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'system'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%MergeTree'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'_log(_[0-9]+)?$'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_bytes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty &lt;code&gt;ttl&lt;/code&gt; means the log is kept forever. Names like &lt;code&gt;trace_log_0&lt;/code&gt; are old copies; step 4 removes them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: add a TTL file in config.d
&lt;/h3&gt;

&lt;p&gt;Save this as &lt;code&gt;clickhouse-ttl.xml&lt;/code&gt; next to &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;clickhouse&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Keep 7 days of ClickHouse's own logs. List only logs your server has. --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;query_log&amp;gt;&amp;lt;ttl&amp;gt;&lt;/span&gt;event_date + INTERVAL 7 DAY DELETE&lt;span class="nt"&gt;&amp;lt;/ttl&amp;gt;&amp;lt;/query_log&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;trace_log&amp;gt;&amp;lt;ttl&amp;gt;&lt;/span&gt;event_date + INTERVAL 7 DAY DELETE&lt;span class="nt"&gt;&amp;lt;/ttl&amp;gt;&amp;lt;/trace_log&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;text_log&amp;gt;&amp;lt;ttl&amp;gt;&lt;/span&gt;event_date + INTERVAL 7 DAY DELETE&lt;span class="nt"&gt;&amp;lt;/ttl&amp;gt;&amp;lt;/text_log&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;metric_log&amp;gt;&amp;lt;ttl&amp;gt;&lt;/span&gt;event_date + INTERVAL 7 DAY DELETE&lt;span class="nt"&gt;&amp;lt;/ttl&amp;gt;&amp;lt;/metric_log&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;asynchronous_metric_log&amp;gt;&amp;lt;ttl&amp;gt;&lt;/span&gt;event_date + INTERVAL 7 DAY DELETE&lt;span class="nt"&gt;&amp;lt;/ttl&amp;gt;&amp;lt;/asynchronous_metric_log&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;part_log&amp;gt;&amp;lt;ttl&amp;gt;&lt;/span&gt;event_date + INTERVAL 7 DAY DELETE&lt;span class="nt"&gt;&amp;lt;/ttl&amp;gt;&amp;lt;/part_log&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- The stock config defines this log with &amp;lt;engine&amp;gt;, so a separate &amp;lt;ttl&amp;gt;
         stops the server from starting (ClickHouse#88366). TTL goes inside. --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;opentelemetry_span_log&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;engine&amp;gt;&lt;/span&gt;ENGINE = MergeTree PARTITION BY toYYYYMM(finish_date) ORDER BY (finish_date, finish_time_us) TTL finish_date + INTERVAL 7 DAY DELETE&lt;span class="nt"&gt;&amp;lt;/engine&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/opentelemetry_span_log&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/clickhouse&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mount it into the &lt;code&gt;clickhouse&lt;/code&gt; service (keep your existing volume lines):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;clickhouse&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./clickhouse-ttl.xml:/etc/clickhouse-server/config.d/clickhouse-ttl.xml:ro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three traps in this step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;opentelemetry_span_log&lt;/code&gt; takes its TTL inside &lt;code&gt;&amp;lt;engine&amp;gt;&lt;/code&gt;.&lt;/strong&gt; The stock config defines this log with &lt;code&gt;&amp;lt;engine&amp;gt;&lt;/code&gt;, and per the &lt;a href="https://clickhouse.com/docs/reference/system-tables/overview" rel="noopener noreferrer"&gt;docs&lt;/a&gt; that conflicts with &lt;code&gt;&amp;lt;ttl&amp;gt;&lt;/code&gt;. The server then exits with &lt;code&gt;If 'engine' is specified for system table, TTL parameters should be specified directly inside 'engine'&lt;/code&gt; (&lt;a href="https://github.com/ClickHouse/ClickHouse/issues/88366" rel="noopener noreferrer"&gt;ClickHouse #88366&lt;/a&gt;). Copy &lt;code&gt;PARTITION BY&lt;/code&gt; and &lt;code&gt;ORDER BY&lt;/code&gt; from &lt;code&gt;SELECT engine_full FROM system.tables WHERE database = 'system' AND name = 'opentelemetry_span_log'&lt;/code&gt; (SigNoz's config also sorts by &lt;code&gt;trace_id&lt;/code&gt;). Or switch the log off with &lt;code&gt;&amp;lt;opentelemetry_span_log remove="1"/&amp;gt;&lt;/code&gt; (Langfuse's &lt;a href="https://langfuse.com/self-hosting/configuration/scaling" rel="noopener noreferrer"&gt;scaling docs&lt;/a&gt;). &lt;code&gt;remove&lt;/code&gt; doesn't delete the existing table, so drop it yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;List only the logs your server has.&lt;/strong&gt; A section in the config is what turns a log on (the stock &lt;code&gt;config.xml&lt;/code&gt; disables &lt;code&gt;session_log&lt;/code&gt; by commenting it out). SigNoz's own &lt;a href="https://github.com/SigNoz/signoz/blob/v0.129.0/deploy/common/clickhouse/config.xml" rel="noopener noreferrer"&gt;&lt;code&gt;config.xml&lt;/code&gt;&lt;/a&gt; doesn't enable &lt;code&gt;text_log&lt;/code&gt;, so delete that line there. Also add &lt;code&gt;processors_profile_log&lt;/code&gt;, which has no TTL in that file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile settings go into users.d, not config.d.&lt;/strong&gt; If you also turn off the query profiler (&lt;code&gt;query_profiler_real_time_period_ns&lt;/code&gt; and &lt;code&gt;query_profiler_cpu_time_period_ns&lt;/code&gt; = 0, as in Langfuse's scaling docs), put that &lt;code&gt;&amp;lt;profiles&amp;gt;&lt;/code&gt; block into &lt;code&gt;users.d/&lt;/code&gt;. In &lt;code&gt;config.d/&lt;/code&gt; it is silently ignored (&lt;a href="https://github.com/triggerdotdev/trigger.dev/issues/4343" rel="noopener noreferrer"&gt;trigger.dev #4343&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: restart ClickHouse
&lt;/h3&gt;

&lt;p&gt;The TTL from &lt;code&gt;config.d&lt;/code&gt; takes effect only when the server restarts (&lt;a href="https://kb.altinity.com/altinity-kb-setup-and-maintenance/altinity-kb-system-tables-eat-my-disk/" rel="noopener noreferrer"&gt;Altinity KB&lt;/a&gt;). A config reload is not enough. With compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; clickhouse     &lt;span class="c"&gt;# recreates the container with the new mount&lt;/span&gt;
docker compose ps clickhouse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If ClickHouse keeps restarting, the reason is in its error log inside the container. &lt;code&gt;docker compose logs&lt;/code&gt; may not show it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;docker compose ps &lt;span class="nt"&gt;-aq&lt;/span&gt; clickhouse&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:/var/log/clickhouse-server/clickhouse-server.err.log &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 20 clickhouse-server.err.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: drop the old trace_log_0 copies
&lt;/h3&gt;

&lt;p&gt;After the restart, every changed log has a copy like &lt;code&gt;trace_log_0&lt;/code&gt; (then &lt;code&gt;_1&lt;/code&gt;, &lt;code&gt;_2&lt;/code&gt; after later changes). The copy keeps all the old rows and has no TTL (ClickStack PR #275 upgrade note, &lt;a href="https://github.com/getsentry/snuba/issues/7311" rel="noopener noreferrer"&gt;Sentry snuba #7311&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This query writes the &lt;code&gt;DROP&lt;/code&gt; statements for you. Read them, then paste them into the client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'DROP TABLE system.'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' SETTINGS max_table_size_to_drop = 0;'&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'system'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'_log_[0-9]+$'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ClickHouse no longer writes to these tables. &lt;code&gt;DROP&lt;/code&gt; can't be undone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: check the result
&lt;/h3&gt;

&lt;p&gt;Run the step 1 query again. Every big log should show a TTL, and no &lt;code&gt;_N&lt;/code&gt; names should be left.&lt;/p&gt;

&lt;p&gt;ClickHouse removes expired rows during merges, by default at most every 4 hours (&lt;a href="https://clickhouse.com/docs/reference/settings/merge-tree-settings/merge-with#merge_with_ttl_timeout" rel="noopener noreferrer"&gt;&lt;code&gt;merge_with_ttl_timeout&lt;/code&gt;&lt;/a&gt;), so the size shrinks over the next hours.&lt;/p&gt;

&lt;p&gt;A script can do the checking for you: &lt;a href="https://github.com/Protemir/diskvet" rel="noopener noreferrer"&gt;diskvet&lt;/a&gt; (my project) runs the step 1 check read-only and prints the &lt;code&gt;TRUNCATE&lt;/code&gt; commands, the TTL file and the &lt;code&gt;DROP&lt;/code&gt; list for your tables, with the flag for tables over the drop limit (see section 7).&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Disk still full? Where else the space goes
&lt;/h2&gt;

&lt;p&gt;Compare &lt;code&gt;df -h&lt;/code&gt; with the total size of all parts (&lt;code&gt;SELECT formatReadableSize(sum(bytes_on_disk)) FROM system.parts&lt;/code&gt;). If the disk holds much more than that, the space is outside ClickHouse's tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are Docker container logs filling the disk?
&lt;/h3&gt;

&lt;p&gt;Docker's default &lt;code&gt;json-file&lt;/code&gt; log driver has no size limit (&lt;code&gt;max-size&lt;/code&gt; defaults to &lt;code&gt;-1&lt;/code&gt;, see the &lt;a href="https://docs.docker.com/engine/logging/drivers/json-file/" rel="noopener noreferrer"&gt;Docker docs&lt;/a&gt;). In &lt;a href="https://github.com/langfuse/langfuse/issues/16339" rel="noopener noreferrer"&gt;Langfuse #16339&lt;/a&gt; the ClickHouse container's log reached 89.1 GiB and filled a 244 GiB root disk.&lt;/p&gt;

&lt;p&gt;Find the biggest ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'du -h /var/lib/docker/containers/*/*-json.log | sort -h | tail -5'&lt;/span&gt;
docker ps &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--no-trunc&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.ID}} {{.Names}}'&lt;/span&gt;   &lt;span class="c"&gt;# the folder name is the container ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To fix it, add a &lt;code&gt;logging:&lt;/code&gt; block to every service in &lt;code&gt;docker-compose.yml&lt;/code&gt;. In #16339 the Langfuse maintainers recommended doing it for all containers, not only ClickHouse. SigNoz's &lt;a href="https://github.com/SigNoz/signoz/blob/v0.129.0/deploy/docker/docker-compose.yaml" rel="noopener noreferrer"&gt;compose file&lt;/a&gt; already sets &lt;code&gt;50m&lt;/code&gt; × 3.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;json-file&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;max-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;50m"&lt;/span&gt;
        &lt;span class="na"&gt;max-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;docker compose up -d --force-recreate&lt;/code&gt;: log options apply only to newly created containers. Recreating also removes the old log file; in #16339 that freed 81 GiB.&lt;/p&gt;

&lt;p&gt;Two notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your Docker daemon uses another logging driver, set retention there instead, because &lt;code&gt;driver: json-file&lt;/code&gt; overrides it.&lt;/li&gt;
&lt;li&gt;Langfuse's README (&lt;a href="https://github.com/langfuse/langfuse/pull/16363" rel="noopener noreferrer"&gt;PR #16363&lt;/a&gt;) describes the daemon-wide way: set &lt;code&gt;max-size&lt;/code&gt; and &lt;code&gt;max-file&lt;/code&gt; as daemon defaults, then restart Docker and recreate the containers. It also advises against truncating or rotating these files with external tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Are ClickHouse's own log files big?
&lt;/h3&gt;

&lt;p&gt;ClickHouse also writes plain log files to &lt;code&gt;/var/log/clickhouse-server&lt;/code&gt; (a volume in Langfuse's compose). By default they are at &lt;code&gt;trace&lt;/code&gt; level, rotated at 1000M, with up to 10 old files kept (&lt;a href="https://clickhouse.com/docs/reference/settings/server-settings/settings/other#logger" rel="noopener noreferrer"&gt;&lt;code&gt;logger&lt;/code&gt; docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;ClickStack PR #275 uses the values below. Save them as another &lt;code&gt;config.d&lt;/code&gt; file, mount it like the TTL file and restart ClickHouse (section 4, step 3):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;clickhouse&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;logger&amp;gt;&amp;lt;level&amp;gt;&lt;/span&gt;information&lt;span class="nt"&gt;&amp;lt;/level&amp;gt;&amp;lt;size&amp;gt;&lt;/span&gt;100M&lt;span class="nt"&gt;&amp;lt;/size&amp;gt;&amp;lt;count&amp;gt;&lt;/span&gt;10&lt;span class="nt"&gt;&amp;lt;/count&amp;gt;&amp;lt;/logger&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/clickhouse&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why do deleted rows still take space?
&lt;/h3&gt;

&lt;p&gt;Langfuse's data retention removes old rows with a &lt;a href="https://clickhouse.com/docs/reference/statements/delete" rel="noopener noreferrer"&gt;lightweight &lt;code&gt;DELETE&lt;/code&gt;&lt;/a&gt;. ClickHouse only marks such rows; the space comes back when the part is merged.&lt;/p&gt;

&lt;p&gt;Background merges skip parts over &lt;a href="https://clickhouse.com/docs/reference/settings/merge-tree-settings/max-bytes#max_bytes_to_merge_at_max_space_in_pool" rel="noopener noreferrer"&gt;&lt;code&gt;max_bytes_to_merge_at_max_space_in_pool&lt;/code&gt;&lt;/a&gt; (150 GiB), and old monthly partitions are rarely merged. In &lt;a href="https://github.com/orgs/langfuse/discussions/13969" rel="noopener noreferrer"&gt;Langfuse discussion #13969&lt;/a&gt; a May partition of ~802 GiB was ~85% deleted rows.&lt;/p&gt;

&lt;p&gt;Find the partitions with deleted rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;partition_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;formatReadableSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes_on_disk&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;has_lightweight_delete&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;partition_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes_on_disk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then apply the delete mask, one partition at a time (&lt;a href="https://clickhouse.com/docs/reference/statements/alter/apply-deleted-mask" rel="noopener noreferrer"&gt;APPLY DELETED MASK&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;observations&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;DELETED&lt;/span&gt; &lt;span class="n"&gt;MASK&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="s1"&gt;'202605'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Careful:&lt;/strong&gt; this is a heavyweight mutation. It rewrites the affected parts, so it needs free space for the new copies and loads the disk. Run it off-peak and watch it with &lt;code&gt;SELECT * FROM system.mutations WHERE NOT is_done&lt;/code&gt;. In #13969 it freed ~360 GiB per replica in ~17 minutes.&lt;/p&gt;

&lt;p&gt;Since v3.179.0 the Langfuse worker can do this on a schedule: &lt;code&gt;LANGFUSE_CLICKHOUSE_DELETED_MASK_CLEANER_ENABLED=true&lt;/code&gt; (off by default; see &lt;code&gt;.env.prod.example&lt;/code&gt;, &lt;a href="https://github.com/langfuse/langfuse/pull/14035" rel="noopener noreferrer"&gt;PR #14035&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Are old parts stuck on disk? (inactive and detached parts)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;formatReadableSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes_on_disk&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remove_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;oldest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;refcount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_refcount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes_on_disk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inactive parts are left over from merges. ClickHouse deletes them after &lt;a href="https://clickhouse.com/docs/reference/settings/merge-tree-settings/other#old_parts_lifetime" rel="noopener noreferrer"&gt;&lt;code&gt;old_parts_lifetime&lt;/code&gt;&lt;/a&gt; (480 s), once no query uses them (&lt;code&gt;refcount&lt;/code&gt; = 1).&lt;/p&gt;

&lt;p&gt;If they pile up (112.55 GiB of them in Langfuse discussion #15024) and &lt;code&gt;oldest&lt;/code&gt; is hours ago, look for long queries in &lt;code&gt;system.processes&lt;/code&gt; and stop them with &lt;code&gt;KILL QUERY WHERE query_id = '...'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Detached parts stay on disk until you remove them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;formatReadableSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes_on_disk&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detached_parts&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check &lt;code&gt;reason&lt;/code&gt; first. Remove a part with &lt;code&gt;ALTER TABLE &amp;lt;db&amp;gt;.&amp;lt;table&amp;gt; DROP DETACHED PART '&amp;lt;name&amp;gt;' SETTINGS allow_drop_detached = 1&lt;/code&gt;, not by deleting folders.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Upgrading ClickHouse? Check this first
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Langfuse + ClickHouse 26.8 or newer: update Langfuse first.&lt;/strong&gt; ClickHouse 26.8 changed the default of &lt;code&gt;input_format_read_datetime_number_as_raw_value&lt;/code&gt; from 1 to 0. Older Langfuse sends &lt;code&gt;DateTime64&lt;/code&gt; values as millisecond numbers, and on 26.8+ they end up stored as &lt;code&gt;9999-12-31 23:59:59&lt;/code&gt; (&lt;a href="https://github.com/langfuse/langfuse/issues/16858" rel="noopener noreferrer"&gt;Langfuse #16858&lt;/a&gt;: 0.59% to 21.43% of writes per project). The fix is &lt;a href="https://github.com/langfuse/langfuse/pull/16892" rel="noopener noreferrer"&gt;PR #16892&lt;/a&gt; (backported to v3 in &lt;a href="https://github.com/langfuse/langfuse/pull/16957" rel="noopener noreferrer"&gt;PR #16957&lt;/a&gt;), released in Langfuse v4.28.0 and v3.225.7. Langfuse partitions by month, so such rows land in partition &lt;code&gt;999912&lt;/code&gt;. Check your version and look for them:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;partition_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;partition_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'9999'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;partition_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;After any upgrade, run the step 1 query from section 4 again.&lt;/strong&gt; When a release changes a log's schema, ClickHouse renames the current table and creates a new one, so new &lt;code&gt;_N&lt;/code&gt; copies appear. Drop them as in step 4.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Check everything at once
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Protemir/diskvet" rel="noopener noreferrer"&gt;diskvet&lt;/a&gt; (my project) is an open-source (Apache-2.0) read-only script that runs most of the checks above and prints a report with the fix commands. It can't see Docker's log files, but it shows how much of the disk is outside ClickHouse's parts.&lt;/p&gt;

&lt;p&gt;On the machine with Langfuse's &lt;code&gt;docker-compose.yml&lt;/code&gt; (read &lt;code&gt;checks.sql&lt;/code&gt; before you run it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://github.com/Protemir/diskvet/releases/latest/download/diskvet.sh
curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://github.com/Protemir/diskvet/releases/latest/download/checks.sql
sh diskvet.sh report &lt;span class="nt"&gt;--docker&lt;/span&gt; auto &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; report.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For SigNoz, use &lt;code&gt;--docker signoz-clickhouse&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It reads only metadata from &lt;code&gt;system.*&lt;/code&gt; tables (&lt;code&gt;system.tables&lt;/code&gt;, &lt;code&gt;system.parts&lt;/code&gt;, &lt;code&gt;system.disks&lt;/code&gt;, &lt;code&gt;system.detached_parts&lt;/code&gt;, &lt;code&gt;system.mutations&lt;/code&gt;, &lt;code&gt;system.part_log&lt;/code&gt; and a few more), with &lt;code&gt;readonly=2&lt;/code&gt; and resource limits. It never reads rows of your tables, &lt;code&gt;system.query_log&lt;/code&gt; or query texts, and sends nothing anywhere. Nothing runs by itself: you read each fix and run it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;ClickHouse documentation and source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;System tables overview (unlimited growth, &lt;code&gt;&amp;lt;engine&amp;gt;&lt;/code&gt; vs &lt;code&gt;&amp;lt;ttl&amp;gt;&lt;/code&gt;, renaming on schema change): &lt;a href="https://clickhouse.com/docs/reference/system-tables/overview" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/system-tables/overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Default &lt;code&gt;config.xml&lt;/code&gt; (logs without TTL, &lt;code&gt;opentelemetry_span_log&lt;/code&gt; engine, &lt;code&gt;text_log&lt;/code&gt; and logger at &lt;code&gt;trace&lt;/code&gt;, &lt;code&gt;session_log&lt;/code&gt; commented out): &lt;a href="https://github.com/ClickHouse/ClickHouse/blob/master/programs/server/config.xml" rel="noopener noreferrer"&gt;https://github.com/ClickHouse/ClickHouse/blob/master/programs/server/config.xml&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max_table_size_to_drop&lt;/code&gt;, server setting and &lt;code&gt;force_drop_table&lt;/code&gt;: &lt;a href="https://clickhouse.com/docs/reference/settings/server-settings/settings/max-table#max_table_size_to_drop" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/settings/server-settings/settings/max-table#max_table_size_to_drop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max_table_size_to_drop&lt;/code&gt;, query setting: &lt;a href="https://clickhouse.com/docs/reference/settings/session-settings/max#max_table_size_to_drop" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/settings/session-settings/max#max_table_size_to_drop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Query-level override since 23.12: &lt;a href="https://github.com/ClickHouse/ClickHouse/pull/57452" rel="noopener noreferrer"&gt;https://github.com/ClickHouse/ClickHouse/pull/57452&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The flag is removed after use (&lt;code&gt;checkCanBeDropped&lt;/code&gt;): &lt;a href="https://github.com/ClickHouse/ClickHouse/blob/master/src/Interpreters/Context.cpp" rel="noopener noreferrer"&gt;https://github.com/ClickHouse/ClickHouse/blob/master/src/Interpreters/Context.cpp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TRUNCATE: &lt;a href="https://clickhouse.com/docs/reference/statements/truncate" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/statements/truncate&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;opentelemetry_span_log&lt;/code&gt; TTL error: &lt;a href="https://github.com/ClickHouse/ClickHouse/issues/88366" rel="noopener noreferrer"&gt;https://github.com/ClickHouse/ClickHouse/issues/88366&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MergeTree settings: &lt;code&gt;merge_with_ttl_timeout&lt;/code&gt; (14400 s) &lt;a href="https://clickhouse.com/docs/reference/settings/merge-tree-settings/merge-with#merge_with_ttl_timeout" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/settings/merge-tree-settings/merge-with#merge_with_ttl_timeout&lt;/a&gt;, &lt;code&gt;old_parts_lifetime&lt;/code&gt; (480 s) &lt;a href="https://clickhouse.com/docs/reference/settings/merge-tree-settings/other#old_parts_lifetime" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/settings/merge-tree-settings/other#old_parts_lifetime&lt;/a&gt;, &lt;code&gt;max_bytes_to_merge_at_max_space_in_pool&lt;/code&gt; (150 GiB) &lt;a href="https://clickhouse.com/docs/reference/settings/merge-tree-settings/max-bytes#max_bytes_to_merge_at_max_space_in_pool" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/settings/merge-tree-settings/max-bytes#max_bytes_to_merge_at_max_space_in_pool&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Server log rotation (&lt;code&gt;logger&lt;/code&gt;: &lt;code&gt;level&lt;/code&gt;, &lt;code&gt;size&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;): &lt;a href="https://clickhouse.com/docs/reference/settings/server-settings/settings/other#logger" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/settings/server-settings/settings/other#logger&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lightweight DELETE: &lt;a href="https://clickhouse.com/docs/reference/statements/delete" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/statements/delete&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;APPLY DELETED MASK: &lt;a href="https://clickhouse.com/docs/reference/statements/alter/apply-deleted-mask" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/statements/alter/apply-deleted-mask&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;system.parts: &lt;a href="https://clickhouse.com/docs/reference/system-tables/parts" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/system-tables/parts&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;system.detached_parts: &lt;a href="https://clickhouse.com/docs/reference/system-tables/detached_parts" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/system-tables/detached_parts&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;DROP DETACHED PART: &lt;a href="https://clickhouse.com/docs/reference/statements/alter/partition" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/statements/alter/partition&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allow_drop_detached&lt;/code&gt;: &lt;a href="https://clickhouse.com/docs/reference/settings/session-settings/allow#allow_drop_detached" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/settings/session-settings/allow#allow_drop_detached&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;system.mutations: &lt;a href="https://clickhouse.com/docs/reference/system-tables/mutations" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/system-tables/mutations&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;KILL QUERY: &lt;a href="https://clickhouse.com/docs/reference/statements/kill" rel="noopener noreferrer"&gt;https://clickhouse.com/docs/reference/statements/kill&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Altinity KB, "System tables ate my disk" (restart needed): &lt;a href="https://kb.altinity.com/altinity-kb-setup-and-maintenance/altinity-kb-system-tables-eat-my-disk/" rel="noopener noreferrer"&gt;https://kb.altinity.com/altinity-kb-setup-and-maintenance/altinity-kb-system-tables-eat-my-disk/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;json-file logging driver (&lt;code&gt;max-size&lt;/code&gt; defaults to -1; existing containers keep old settings): &lt;a href="https://docs.docker.com/engine/logging/drivers/json-file/" rel="noopener noreferrer"&gt;https://docs.docker.com/engine/logging/drivers/json-file/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Langfuse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FAQ, reduce ClickHouse disk size: &lt;a href="https://langfuse.com/faq/all/reduce-clickhouse-disk-size" rel="noopener noreferrer"&gt;https://langfuse.com/faq/all/reduce-clickhouse-disk-size&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Scaling docs, ClickHouse system log tables: &lt;a href="https://langfuse.com/self-hosting/configuration/scaling" rel="noopener noreferrer"&gt;https://langfuse.com/self-hosting/configuration/scaling&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;#13123, system tables grow unbounded: &lt;a href="https://github.com/langfuse/langfuse/issues/13123" rel="noopener noreferrer"&gt;https://github.com/langfuse/langfuse/issues/13123&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;#16339, unbounded Docker log: &lt;a href="https://github.com/langfuse/langfuse/issues/16339" rel="noopener noreferrer"&gt;https://github.com/langfuse/langfuse/issues/16339&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #16363, Docker log rotation in the README (daemon defaults, no external truncation): &lt;a href="https://github.com/langfuse/langfuse/pull/16363" rel="noopener noreferrer"&gt;https://github.com/langfuse/langfuse/pull/16363&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Discussion #13969, lightweight-deleted rows: &lt;a href="https://github.com/orgs/langfuse/discussions/13969" rel="noopener noreferrer"&gt;https://github.com/orgs/langfuse/discussions/13969&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Discussion #15024, inactive parts and 59 GiB text_log: &lt;a href="https://github.com/orgs/langfuse/discussions/15024" rel="noopener noreferrer"&gt;https://github.com/orgs/langfuse/discussions/15024&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #14035, deleted-mask cleaner (released in v3.179.0): &lt;a href="https://github.com/langfuse/langfuse/pull/14035" rel="noopener noreferrer"&gt;https://github.com/langfuse/langfuse/pull/14035&lt;/a&gt;, &lt;a href="https://github.com/langfuse/langfuse/releases/tag/v3.179.0" rel="noopener noreferrer"&gt;https://github.com/langfuse/langfuse/releases/tag/v3.179.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;#16858, DateTime64 on ClickHouse 26.8+: &lt;a href="https://github.com/langfuse/langfuse/issues/16858" rel="noopener noreferrer"&gt;https://github.com/langfuse/langfuse/issues/16858&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR #16892 and v3 backport PR #16957: &lt;a href="https://github.com/langfuse/langfuse/pull/16892" rel="noopener noreferrer"&gt;https://github.com/langfuse/langfuse/pull/16892&lt;/a&gt;, &lt;a href="https://github.com/langfuse/langfuse/pull/16957" rel="noopener noreferrer"&gt;https://github.com/langfuse/langfuse/pull/16957&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Releases with the fix: &lt;a href="https://github.com/langfuse/langfuse/releases/tag/v4.28.0" rel="noopener noreferrer"&gt;https://github.com/langfuse/langfuse/releases/tag/v4.28.0&lt;/a&gt;, &lt;a href="https://github.com/langfuse/langfuse/releases/tag/v3.225.7" rel="noopener noreferrer"&gt;https://github.com/langfuse/langfuse/releases/tag/v3.225.7&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SigNoz, ClickStack and others:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SigNoz #12050, 80+ GB of system logs: &lt;a href="https://github.com/SigNoz/signoz/issues/12050" rel="noopener noreferrer"&gt;https://github.com/SigNoz/signoz/issues/12050&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SigNoz v0.129.0 ClickHouse &lt;code&gt;config.xml&lt;/code&gt; and &lt;code&gt;users.xml&lt;/code&gt;: &lt;a href="https://github.com/SigNoz/signoz/blob/v0.129.0/deploy/common/clickhouse/config.xml" rel="noopener noreferrer"&gt;https://github.com/SigNoz/signoz/blob/v0.129.0/deploy/common/clickhouse/config.xml&lt;/a&gt;, &lt;a href="https://github.com/SigNoz/signoz/blob/v0.129.0/deploy/common/clickhouse/users.xml" rel="noopener noreferrer"&gt;https://github.com/SigNoz/signoz/blob/v0.129.0/deploy/common/clickhouse/users.xml&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SigNoz v0.129.0 &lt;code&gt;docker-compose.yaml&lt;/code&gt; (container &lt;code&gt;signoz-clickhouse&lt;/code&gt;, &lt;code&gt;max-size: 50m&lt;/code&gt;, &lt;code&gt;max-file: "3"&lt;/code&gt;): &lt;a href="https://github.com/SigNoz/signoz/blob/v0.129.0/deploy/docker/docker-compose.yaml" rel="noopener noreferrer"&gt;https://github.com/SigNoz/signoz/blob/v0.129.0/deploy/docker/docker-compose.yaml&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;ClickStack Helm chart PR #275: &lt;a href="https://github.com/ClickHouse/ClickStack-helm-charts/pull/275" rel="noopener noreferrer"&gt;https://github.com/ClickHouse/ClickStack-helm-charts/pull/275&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sentry snuba #7311, &lt;code&gt;*_log_N&lt;/code&gt; copies without TTL: &lt;a href="https://github.com/getsentry/snuba/issues/7311" rel="noopener noreferrer"&gt;https://github.com/getsentry/snuba/issues/7311&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;trigger.dev #4343, profile settings in config.d ignored: &lt;a href="https://github.com/triggerdotdev/trigger.dev/issues/4343" rel="noopener noreferrer"&gt;https://github.com/triggerdotdev/trigger.dev/issues/4343&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Written and tested by an AI agent: every command was run on the ClickHouse versions listed at the top. Also on &lt;a href="https://diskvet.dev/guide/" rel="noopener noreferrer"&gt;diskvet.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;ClickHouse is a registered trademark of ClickHouse, Inc. diskvet is not affiliated with ClickHouse, Inc. Langfuse, SigNoz and ClickStack are trademarks of their respective owners.&lt;/p&gt;

</description>
      <category>clickhouse</category>
      <category>devops</category>
      <category>selfhosted</category>
      <category>observability</category>
    </item>
  </channel>
</rss>
