<?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: MilkyWay008</title>
    <description>The latest articles on DEV Community by MilkyWay008 (@milkyway008).</description>
    <link>https://dev.to/milkyway008</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%2F4075791%2F36d12367-22b5-4e63-b6b6-ee85957624b5.jpg</url>
      <title>DEV Community: MilkyWay008</title>
      <link>https://dev.to/milkyway008</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/milkyway008"/>
    <language>en</language>
    <item>
      <title>uv lock says "No solution found": how to read the dependency deadlock and break it</title>
      <dc:creator>MilkyWay008</dc:creator>
      <pubDate>Tue, 22 Sep 2026 17:07:21 +0000</pubDate>
      <link>https://dev.to/milkyway008/uv-lock-says-no-solution-found-how-to-read-the-dependency-deadlock-and-break-it-3g9h</link>
      <guid>https://dev.to/milkyway008/uv-lock-says-no-solution-found-how-to-read-the-dependency-deadlock-and-break-it-3g9h</guid>
      <description>&lt;p&gt;You add one library, run &lt;code&gt;uv lock&lt;/code&gt;, and the resolver prints 40 lines of "Because ... And because ..." and quits with a non-zero exit code. You did not change any pins. The project was fine yesterday.&lt;/p&gt;

&lt;p&gt;I have run into this class of failure a few times and it usually ends the same way: the resolver is right, and one package in the graph has a stale upper bound.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that wall of text is saying
&lt;/h2&gt;

&lt;p&gt;The first line is the one that matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;× No solution found when resolving dependencies:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything after it is a derivation chain. Each line is a requirement read out of some package's metadata, and the chain ends where two of them want different things.&lt;/p&gt;

&lt;p&gt;A real example, from a LangGraph issue (&lt;a href="https://github.com/langchain-ai/langgraph/issues/8352" rel="noopener noreferrer"&gt;langchain-ai/langgraph#8352&lt;/a&gt;): &lt;code&gt;langgraph-api&lt;/code&gt; 0.11.0 pins &lt;code&gt;opentelemetry-exporter-prometheus&amp;lt;0.59&lt;/code&gt;, which forces &lt;code&gt;opentelemetry-sdk&lt;/code&gt; into an old range. Meanwhile &lt;code&gt;pydantic-ai&lt;/code&gt; 2.x pulls &lt;code&gt;logfire&amp;gt;=4.35.0&lt;/code&gt;, which wants &lt;code&gt;opentelemetry-sdk&amp;gt;=1.39.0,&amp;lt;1.43.0&lt;/code&gt;. Those two ranges never overlap, so no set of versions satisfies both.&lt;/p&gt;

&lt;p&gt;Nobody is being unreasonable here. That 0.59 bound was probably correct when someone wrote it...... it just never got revisited. The person who filed the issue could not fix it either, because the metadata ships from a private source tree, so only the maintainers can widen the pin.&lt;/p&gt;

&lt;p&gt;Two commands make the chain readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv lock &lt;span class="nt"&gt;-v&lt;/span&gt;                            &lt;span class="c"&gt;# full derivation, as the resolver sees it&lt;/span&gt;
uv tree &lt;span class="nt"&gt;--invert&lt;/span&gt; opentelemetry-sdk    &lt;span class="c"&gt;# who wants which version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the chain bottom-up, find the two packages that disagree, ignore the middle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The silent downgrade is worse than the error
&lt;/h2&gt;

&lt;p&gt;In that same issue the resolver could quietly settle on a much older &lt;code&gt;langgraph-api&lt;/code&gt; (0.7.27 was observed), and &lt;code&gt;langgraph dev&lt;/code&gt; fell back to an older build without complaining. The app boots, on code you did not ask for, and the Studio upgrade banner stays unsatisfiable.&lt;/p&gt;

&lt;p&gt;That one costs people days, because nothing looks broken. A successful resolution is not proof you got what you wanted. Check the installed version, not the requested one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constraints cannot fix a deadlock (this is the part people get wrong)
&lt;/h2&gt;

&lt;p&gt;uv has two settings for this, both in the workspace-root &lt;code&gt;pyproject.toml&lt;/code&gt; under &lt;code&gt;[tool.uv]&lt;/code&gt;. They look similar and behave nothing alike.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;constraint-dependencies&lt;/code&gt; is additive. The docs describe constraints as restricting "the versions of dependencies that are selected during resolution", so they get intersected with whatever the packages already declared.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;override-dependencies&lt;/code&gt; is absolute. The docs say overrides "force selection of a specific version of a package, regardless of the version requested by any other package, and regardless of whether choosing that version would typically constitute an invalid resolution". The resolution docs call overrides the escape hatch for erroneous upper version bounds.&lt;/p&gt;

&lt;p&gt;So when the ranges are disjoint, a constraint can never save you. Constraints only narrow the space, and the space is already empty. This matters because the reflex is to reach for a constraint when the real tool is an override.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.uv]&lt;/span&gt;
&lt;span class="c"&gt;# stops the resolver from silently picking something ancient&lt;/span&gt;
&lt;span class="py"&gt;constraint-dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;["langgraph-api&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="s"&gt;"]&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;
&lt;span class="c"&gt;# the deadlock breaker: deliberately contradicts a declared upper bound&lt;/span&gt;
&lt;span class="py"&gt;override-dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;["opentelemetry-exporter-prometheus&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.63&lt;/span&gt;&lt;span class="err"&gt;b&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s"&gt;"]&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One placement gotcha: uv reads these from the workspace root only. Putting them in &lt;code&gt;uv.toml&lt;/code&gt;, or in a member package's pyproject, does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order I would try things
&lt;/h2&gt;

&lt;p&gt;1st, pin the thing that moved back to the last version that worked. Boring, safe, usually enough to get moving today.&lt;/p&gt;

&lt;p&gt;2nd, add a floor constraint on whatever was being silently downgraded, so you find out about it instead of living with it.&lt;/p&gt;

&lt;p&gt;3rd, override the stale bound, then actually run the app. &lt;code&gt;uv lock&lt;/code&gt; exiting 0 only proves the graph resolves. The package that asked for the old SDK is now untested territory.&lt;/p&gt;

&lt;p&gt;4th, if the two libraries genuinely cannot coexist, give them separate environments. Ugly, but honest, and sometimes correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leave a trail
&lt;/h2&gt;

&lt;p&gt;Keep the override in the file with a comment, a link to the upstream issue, and the date you added it. An override without a note turns into permanent mystery debt six months later. In this case the pin was widened on the 0.13.0 dev line (&lt;code&gt;&amp;lt;0.64,&amp;gt;=0.63b1&lt;/code&gt;, landing the SDK at 1.42.1) while stable still carried the old bound, so the plan is: wait for the release, then delete the block.&lt;/p&gt;

&lt;p&gt;If you use pip instead of uv, the constraint file (&lt;code&gt;pip install --dry-run -c constraints.txt&lt;/code&gt;) is additive in the same way, so it will not break a disjoint range either. Worth knowing before spending an hour on it.&lt;/p&gt;

&lt;p&gt;I can be wrong about your particular graph, but from what I have seen the fix is rarely a clever resolver flag. It is reading the chain, working out which bound is stale, and forcing that one thing while you wait for upstream...... then writing down why.&lt;/p&gt;

&lt;p&gt;One honest gap: I could not independently confirm whether stable &lt;code&gt;langgraph-api&lt;/code&gt; still carries &lt;code&gt;&amp;lt;0.59&lt;/code&gt; at the time of writing. The issue is open. Check it before copying the override.&lt;/p&gt;

</description>
      <category>python</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>ai</category>
    </item>
    <item>
      <title>HTTP 400 from your own LLM endpoint? Check for content: null</title>
      <dc:creator>MilkyWay008</dc:creator>
      <pubDate>Mon, 21 Sep 2026 17:28:55 +0000</pubDate>
      <link>https://dev.to/milkyway008/http-400-from-your-own-llm-endpoint-check-for-content-null-1o46</link>
      <guid>https://dev.to/milkyway008/http-400-from-your-own-llm-endpoint-check-for-content-null-1o46</guid>
      <description>&lt;p&gt;Someone I was helping had a tidy little setup: a self-hosted OpenAI-compatible endpoint in front of their model, a coding agent pointed at it, months of smooth sailing. Then they bumped the agent from 7.3.42 to 7.3.44 and every request came back HTTP 400. Same endpoint, same key, same model. Only the client version changed.&lt;/p&gt;

&lt;p&gt;The error body was short, and it looked roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Bad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Request&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Type validation failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
           &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/messages/2/content"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
           &lt;/span&gt;&lt;span class="nl"&gt;"keyword"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invalid_union"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run a gateway in front of a local model (vLLM, LiteLLM, llama.cpp, Open WebUI, LM Studio, or your own FastAPI shim), this one will find you eventually. The error already tells you where to look, and the fix is usually a couple of lines on whichever side of the wire you control.&lt;/p&gt;

&lt;h2&gt;
  
  
  The field it names is the whole clue
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/messages/2/content&lt;/code&gt; means the third message in the array, its &lt;code&gt;content&lt;/code&gt; field, and a union check that failed. Validators generated from a schema (pydantic, zod, ajv and friends) hand you the JSON path for free. Read the path first, then read the raw request, and stop guessing about the model.&lt;/p&gt;

&lt;p&gt;Log the outgoing body at the gateway (raw JSON, not a redacted summary), find that index, and look at the message. In this case it was an assistant message with tool calls and no text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read_file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;a.txt&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing looks broken there. That's the trap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed, and why neither side is crazy
&lt;/h2&gt;

&lt;p&gt;On the client side, Vercel's AI SDK changed tool-only assistant messages from &lt;code&gt;content: ""&lt;/code&gt; to &lt;code&gt;content: null&lt;/code&gt; on purpose. The commit message spells out why (&lt;a href="https://github.com/vercel/ai/commit/bfb756d839d435e3fd0afa22135c59d4587c79f2" rel="noopener noreferrer"&gt;vercel/ai@bfb756d8&lt;/a&gt;, April 2026): providers backed by AWS Bedrock reject an empty text block with &lt;code&gt;ValidationException: text content blocks must be non-empty&lt;/code&gt;, so the serializer became &lt;code&gt;content: text || null&lt;/code&gt;. A lot of coding agents sit on top of that SDK, which is why the change showed up in several places at once.&lt;/p&gt;

&lt;p&gt;On the server side, the endpoint's validator insists that an assistant &lt;code&gt;content&lt;/code&gt; is a string. Null isn't a string. Union check fails, request dies.&lt;/p&gt;

&lt;p&gt;OpenAI's own schema allows null, though. In the &lt;a href="https://github.com/openai/openai-openapi/blob/master/openapi.yaml" rel="noopener noreferrer"&gt;OpenAI OpenAPI spec&lt;/a&gt;, the assistant message defines &lt;code&gt;content&lt;/code&gt; as &lt;code&gt;string | array of parts | null&lt;/code&gt;, described as "The contents of the assistant message. Required unless &lt;code&gt;tool_calls&lt;/code&gt; or &lt;code&gt;function_call&lt;/code&gt; is specified." The only required property on that object is &lt;code&gt;role&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;content: null&lt;/code&gt; on a tool-call-only assistant message is legal, and the validator is the piece that's out of step with the protocol. "My schema says no" is a different sentence from "the spec says no."&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing it
&lt;/h2&gt;

&lt;p&gt;If you own the endpoint, normalize at the edge. Accept null when tool calls are present, and rewrite it into something your backend tolerates. A small piece of Starlette middleware is enough:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.middleware.base&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseHTTPMiddleware&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;body&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;m&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_calls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;m&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;   &lt;span class="c1"&gt;# or " " if your backend also dislikes empty strings
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NullContentFix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseHTTPMiddleware&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/chat/completions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That comment about empty strings isn't hypothetical. Continue's OpenAI converter does &lt;code&gt;message.content || " "&lt;/code&gt; with the literal comment "LM Studio (and other providers) don't accept empty content." Different clients picked different workarounds for the same edge case, which is a decent hint about how long this has been rattling around. If your backend is picky about empty strings too, coerce to a single space.&lt;/p&gt;

&lt;p&gt;If you don't own the endpoint, pin the client. The actual difference between working and broken was &lt;code&gt;@ai-sdk/openai-compatible&lt;/code&gt; 2.0.41 versus 2.0.48. A sibling tool still sitting on 2.0.41 was reported working against the same endpoint, which is a reminder that "it works in tool X" says nothing about your config. Pin it in the lockfile, or put your own proxy in the middle to rewrite null.&lt;/p&gt;

&lt;p&gt;And don't wait on the upstream fix. A normalizing pull request for the client was opened and closed without merging (&lt;a href="https://github.com/Kilo-Org/kilocode/issues/12331" rel="noopener noreferrer"&gt;kilocode#12331&lt;/a&gt;; the original report is &lt;a href="https://github.com/Kilo-Org/kilocode/issues/12330" rel="noopener noreferrer"&gt;kilocode#12330&lt;/a&gt;), so as of today it's on you to pin or normalize.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same 400 in other disguises
&lt;/h2&gt;

&lt;p&gt;Once you've seen one message-shape mismatch, you spot them everywhere. The ones I keep running into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reasoning blocks that don't survive a round trip. Open WebUI dropped the Anthropic thinking-block signature when saving &lt;code&gt;reasoning_details&lt;/code&gt;, then replayed the unsigned block on the next turn. Anthropic rejects it with &lt;code&gt;Invalid signature in thinking block&lt;/code&gt;, so turn 2 dies every time, identically across five providers (&lt;a href="https://github.com/open-webui/open-webui/issues/27467" rel="noopener noreferrer"&gt;open-webui#27467&lt;/a&gt;). The fix PR is closed without merging, so stripping unsigned reasoning from prior assistant turns in a proxy is the workaround for now.&lt;/li&gt;
&lt;li&gt;Streaming reasoning where &lt;code&gt;delta.content&lt;/code&gt; isn't a string. n8n 2.36.8 with the Mistral Chat Model: content arrives as an array of thinking blocks, the node's schema wants a string, and you get &lt;code&gt;invalid_union&lt;/code&gt;. Routing through a proxy that strips thinking deltas worked (&lt;a href="https://github.com/n8n-io/n8n/issues/37352" rel="noopener noreferrer"&gt;n8n#37352&lt;/a&gt;, which n8n closed on Sep 8).&lt;/li&gt;
&lt;li&gt;Extra parameters the client started injecting. One client began sending &lt;code&gt;prompt_cache_breakpoint&lt;/code&gt; to a custom Responses endpoint and got &lt;code&gt;400 invalid_parameter&lt;/code&gt; on every request, reproducing on a clean VM (&lt;a href="https://github.com/Kilo-Org/kilocode/issues/13285" rel="noopener noreferrer"&gt;kilocode#13285&lt;/a&gt;). Strip the unknown field at the proxy, or pin the client back.&lt;/li&gt;
&lt;li&gt;Role remapping that breaks the model's chat template. LM Studio maps each developer message to its own system message, so the second one lands mid-prompt and Qwen's Jinja template throws &lt;code&gt;System message must be at the beginning&lt;/code&gt; (&lt;a href="https://github.com/lmstudio-ai/lmstudio-bug-tracker/issues/2298" rel="noopener noreferrer"&gt;lmstudio#2298&lt;/a&gt;). Consolidate developer messages client-side, or fall back to &lt;code&gt;/v1/chat/completions&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The habit that saves the evening
&lt;/h2&gt;

&lt;p&gt;When a proxy or router sits in the middle, the 400 is often produced by the translation layer, not the model. Test with tools disabled, try the same model through a different route, and read the JSON path in the error before touching anything else.&lt;/p&gt;

&lt;p&gt;Then decide who owns the field, fix the side you control, and pin the version of the thing that changed instead of the whole stack.&lt;/p&gt;

&lt;p&gt;And if you happen to be the one writing the validator: accept null on assistant messages that carry tool calls. It's in the spec.&lt;/p&gt;

&lt;p&gt;I've only hit this on a handful of stacks, so treat it as a starting point rather than gospel, and test it against your own gateway. I keep notes on this class of failure in a small KB repo, Windows-flavoured more often than not: &lt;a href="https://github.com/MilkyWay008/hermes-kb-hack-fix" rel="noopener noreferrer"&gt;hermes-kb-hack-fix&lt;/a&gt;. Hope it saves you an evening.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your agent's SQLite state DB keeps corrupting: what actually causes it, and how to recover the data</title>
      <dc:creator>MilkyWay008</dc:creator>
      <pubDate>Sun, 20 Sep 2026 17:20:16 +0000</pubDate>
      <link>https://dev.to/milkyway008/your-agents-sqlite-state-db-keeps-corrupting-what-actually-causes-it-and-how-to-recover-the-data-55hd</link>
      <guid>https://dev.to/milkyway008/your-agents-sqlite-state-db-keeps-corrupting-what-actually-causes-it-and-how-to-recover-the-data-55hd</guid>
      <description>&lt;h1&gt;
  
  
  Your agent's SQLite state DB keeps corrupting: what actually causes it, and how to recover the data
&lt;/h1&gt;

&lt;p&gt;I run an agent that keeps its entire brain in one local SQLite file. Three weeks ago it came back with &lt;code&gt;database disk image is malformed&lt;/code&gt;, and I did the first thing most of us do: deleted the &lt;code&gt;-shm&lt;/code&gt; sidecar, re-ran the repair, moved on. It came back about twenty hours later, same table, same error.&lt;/p&gt;

&lt;p&gt;So I finally went and read SQLite's own list of ways you can corrupt a database file. It's a good list. Nearly everything on it is something we do to the database, not something the database does to itself.&lt;/p&gt;

&lt;p&gt;Here is the order I wish I had done this in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the error properly first
&lt;/h2&gt;

&lt;p&gt;Two different messages get called "corruption" and they are not the same thing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;database disk image is malformed&lt;/code&gt; is SQLITE_CORRUPT (11): page-level damage inside a file that is still recognisably SQLite.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;file is not a database&lt;/code&gt; is SQLITE_NOTADB (26): the header isn't SQLite at all, so it's the wrong path, a zero-length file, or something else wrote over it.&lt;/p&gt;

&lt;p&gt;If you only read one error, read the first one. By the time you reach the last error in a stack trace, the real cause is usually several layers down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the file before you touch it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sqlite3 state.db &lt;span class="s2"&gt;"PRAGMA integrity_check;"&lt;/span&gt;
sqlite3 state.db &lt;span class="s2"&gt;"PRAGMA quick_check;"&lt;/span&gt;
sqlite3 state.db &lt;span class="s2"&gt;".dbinfo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;integrity_check&lt;/code&gt; returns &lt;code&gt;ok&lt;/code&gt;, or a list of problems. It stops after 100 of them, so a long list means "lots", not "exactly 100". &lt;code&gt;quick_check&lt;/code&gt; runs the same test minus the table-versus-index comparison. It's fast and it can miss damage. And &lt;code&gt;PRAGMA foreign_key_check&lt;/code&gt; is not a corruption test at all, it only reports FK violations.&lt;/p&gt;

&lt;p&gt;If the output mentions &lt;code&gt;ptrmap&lt;/code&gt;, that's the pointer map that autovacuum keeps, and it now disagrees with what is actually stored on the pages. That is structural damage from a stray, short or duplicated write, which in practice usually means two writers on one file. It is not an index logic bug, and no amount of &lt;code&gt;REINDEX&lt;/code&gt; will clear it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually causes it
&lt;/h2&gt;

&lt;p&gt;SQLite keeps a canonical list at &lt;a href="https://www.sqlite.org/howtocorrupt.html" rel="noopener noreferrer"&gt;https://www.sqlite.org/howtocorrupt.html&lt;/a&gt;. Ranked by how often I've seen each one show up in agent and desktop app trackers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Deleting or renaming &lt;code&gt;-wal&lt;/code&gt; / &lt;code&gt;-shm&lt;/code&gt; while something still has the database open. The &lt;code&gt;-shm&lt;/code&gt; is the wal-index, the shared memory that coordinates readers and writers. Unlink it under a live connection and that coordination is gone. This is the one I did.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Putting the state file on a network or synced folder. NFS, SMB, OneDrive, Dropbox, iCloud. SQLite's locking is advisory and assumes a real local filesystem with working locks. A sync client rewriting pages underneath it is a corruption machine.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Two processes writing the same file without agreeing on locking. A gateway plus a CLI plus a second server pointed at one state directory. The app should own one writer, or funnel everything through a single process. That's an application bug, not a SQLite one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Copying a live database with a file copy instead of the backup API or &lt;code&gt;VACUUM INTO&lt;/code&gt;. Same category: deleting a hot journal, or restoring a backup while a transaction is still open.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PRAGMA synchronous=OFF&lt;/code&gt;, or a disk that reports a write as synced when it isn't.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Real SQLite bugs do exist, but they are narrow and old: a WAL race writing to a WAL-mode database (§8.1), and corruption after switching between rollback and WAL mode with a VACUUM in between (§8.4). If your database corrupts on a local disk, with one writer, no sidecar fiddling and a sane &lt;code&gt;busy_timeout&lt;/code&gt;, you're in that territory, and it's worth collecting a repro instead of re-reading your own code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovering the data
&lt;/h2&gt;

&lt;p&gt;There is no in-place repair. Anyone telling you to "fix" the file is guessing. The steps below are what worked for me on an agent that had two processes pointed at the same state directory, and I can't promise they map one-to-one onto your setup, but there's nothing destructive in them. Try them before you delete anything.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Stop every writer. Kill the app, the gateway, the CLI. If a process still holds the file, everything below is theatre.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Copy the whole set to scratch: &lt;code&gt;state.db&lt;/code&gt; plus &lt;code&gt;state.db-wal&lt;/code&gt; plus &lt;code&gt;state.db-shm&lt;/code&gt;. Copying the &lt;code&gt;.db&lt;/code&gt; on its own throws away committed data that only lives in the WAL, and it can make a healthy database look corrupt.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dump and reload into a new file:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sqlite3 corrupt.db &lt;span class="s2"&gt;".recover"&lt;/span&gt; | sqlite3 new.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.dump&lt;/code&gt; stops at the first sign of corruption. &lt;code&gt;.recover&lt;/code&gt; keeps going, reassembles what it can from the surviving pages, and parks rows it cannot attribute into a &lt;code&gt;lost_and_found&lt;/code&gt; table.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Validate before you swap. Run &lt;code&gt;PRAGMA integrity_check;&lt;/code&gt; and &lt;code&gt;PRAGMA foreign_key_check;&lt;/code&gt; against &lt;code&gt;new.db&lt;/code&gt;, then &lt;code&gt;REINDEX;&lt;/code&gt;. Keep the old file around until you're sure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Count what you lost. Rows on damaged pages are gone. Uncommitted WAL transactions are gone. Check the &lt;code&gt;lost_and_found&lt;/code&gt; row count, and diff the schema against what you expect, because a table whose root page went missing can disappear silently rather than error out.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Stopping it from coming back
&lt;/h2&gt;

&lt;p&gt;Set &lt;code&gt;PRAGMA journal_mode=WAL;&lt;/code&gt;, &lt;code&gt;PRAGMA synchronous=NORMAL;&lt;/code&gt;, &lt;code&gt;PRAGMA busy_timeout=5000;&lt;/code&gt; and actually handle &lt;code&gt;SQLITE_BUSY&lt;/code&gt; instead of swallowing it. Ignoring busy is on SQLite's own corruption list.&lt;/p&gt;

&lt;p&gt;Never delete or rename &lt;code&gt;-wal&lt;/code&gt; or &lt;code&gt;-shm&lt;/code&gt; while any connection is open. The checkpoint and the cleanup happen when the last connection closes. Let it happen.&lt;/p&gt;

&lt;p&gt;Keep one writer per state directory, enforced with a lock file if the app can't guarantee it, or do all the writing in one process.&lt;/p&gt;

&lt;p&gt;Keep the state directory out of OneDrive, Dropbox, network shares, and aggressive antivirus realtime scanning. Exclude it explicitly rather than hoping.&lt;/p&gt;

&lt;p&gt;Don't churn journal modes, and don't schedule &lt;code&gt;VACUUM&lt;/code&gt; against a live, high-churn database. Back up with &lt;code&gt;VACUUM INTO backup.db&lt;/code&gt; or the backup API, nightly, before you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;I'm not going to pretend this explains every case. If a freshly created database corrupts on local disk with one writer, one process and no sidecar deletion, that's a genuine bug and it deserves a repro with the file attached, not a checklist. Watch for one more trap there: &lt;code&gt;VACUUM INTO&lt;/code&gt; is transactional for the source, but an unplanned shutdown can still leave the output file incomplete. Validate the backup you just made, don't assume it.&lt;/p&gt;

&lt;p&gt;The rest of it is mostly ours. The file was fine, and we kept pulling the rug out from under it. One writer, local disk, hands off the sidecars, and a backup you have actually restored at least once. That's how I run it now, and the malformed errors stopped.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>The tool is installed and on PATH. Your app still can't find it.</title>
      <dc:creator>MilkyWay008</dc:creator>
      <pubDate>Sat, 19 Sep 2026 17:10:17 +0000</pubDate>
      <link>https://dev.to/milkyway008/the-tool-is-installed-and-on-path-your-app-still-cant-find-it-eb8</link>
      <guid>https://dev.to/milkyway008/the-tool-is-installed-and-on-path-your-app-still-cant-find-it-eb8</guid>
      <description>&lt;p&gt;You install a tool. You open a fresh terminal, run &lt;code&gt;where ffmpeg&lt;/code&gt;, and it prints a path right away. Then you go back to your IDE, or your desktop app, or your agent, and it tells you the tool isn't installed. Same machine, same PATH, two different answers.&lt;/p&gt;

&lt;p&gt;I've hit this twice in the last month: once with git inside a code agent's worktree manager, and once with ffmpeg inside an MCP subprocess. Both people had already checked PATH, so they figured the app was broken. Most of the time it isn't. Windows handed that app a copy of the environment from an earlier point in time, and nothing ever told it things changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every process gets a frozen copy of the environment
&lt;/h2&gt;

&lt;p&gt;Windows builds an environment block when it creates a process. Microsoft's docs are blunt about it: "By default, a child process inherits the environment variables of its parent process." So your app, the helper processes it spawns, the extension host it loads and every tool it shells out to all keep the block they were born with. Editing PATH afterwards doesn't reach back into a running process.&lt;/p&gt;

&lt;p&gt;When you add a directory to PATH, Windows writes it to the registry (machine-wide under &lt;code&gt;HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment&lt;/code&gt;, per-user under &lt;code&gt;HKCU\Environment&lt;/code&gt;) and broadcasts a &lt;code&gt;WM_SETTINGCHANGE&lt;/code&gt; message with &lt;code&gt;lParam&lt;/code&gt; set to the string &lt;code&gt;Environment&lt;/code&gt;. The docs say that broadcast is what lets "applications, such as the shell, to pick up your updates." The word to notice there is shell. Explorer refreshes its environment, and anything Explorer launches after that inherits the fresh copy. Anything already running keeps the old one, and so does everything it spawns from then on.&lt;/p&gt;

&lt;p&gt;That's the whole mechanism. There's no cache to clear and no config to reload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosing it in about a minute
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open a new terminal window and run &lt;code&gt;where ffmpeg&lt;/code&gt; (or &lt;code&gt;where git&lt;/code&gt;). If that fails too, the app isn't your problem. Your PATH edit didn't stick.&lt;/li&gt;
&lt;li&gt;Check when the app actually started:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;cherry-studio&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ProcessName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;StartTime&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that start time is older than your PATH edit, you can stop here.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ask the app what it saw. Plenty of tools log the lookup they performed: an IDE extension's output channel, or an MCP server's stderr line like &lt;code&gt;FFmpeg not found at: ffmpeg&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Look at both halves of PATH, because Windows combines them when it builds the effective value for a new process:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Path'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Machine'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Path'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'User'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Machine entries come first in the combined list, so if two directories contain a copy of the tool, the earlier one wins. It isn't always the copy you meant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Restart the process, not the window
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Quit the app completely, then check Task Manager for leftovers: extension hosts, tray helpers, node or electron children, sidecars. A lot of desktop apps are single-instance, so reopening while the old process is still alive just shows you the same stale window with the same stale environment. Kill the leftovers first.&lt;/li&gt;
&lt;li&gt;If you launched the app from a terminal, close that terminal as well. It took its environment when it opened, and the app inherited it from there. A shell started from another shell has the same problem one level up.&lt;/li&gt;
&lt;li&gt;Log off and back on if the change was machine-wide and the app is stubborn. You get a fresh Explorer and a fresh session environment instead of fighting the old one.&lt;/li&gt;
&lt;li&gt;Services and scheduled tasks get none of this. Their host processes started at boot, so they keep the PATH from boot until you restart the service or reboot. Logging off doesn't touch them.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When the restart doesn't help
&lt;/h2&gt;

&lt;p&gt;The git thread above is the awkward version: git worked in VS Code's integrated terminal, worked in Visual Studio, worked when typed by hand, and the extension still said "Git is not installed or found in PATH." Restarts and a reboot changed nothing.&lt;/p&gt;

&lt;p&gt;When the environment is definitely fresh and the tool still isn't found, the app isn't using PATH the way you think it is. The usual reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It runs its own detection with a hardcoded list of locations instead of consulting PATH, and your install isn't on that list.&lt;/li&gt;
&lt;li&gt;It resolves the wrong name. Git for Windows ships &lt;code&gt;git.exe&lt;/code&gt; under both &lt;code&gt;cmd\&lt;/code&gt; and &lt;code&gt;bin\&lt;/code&gt;, and a tool pointed at one of them while looking for the other will fail even though &lt;code&gt;git&lt;/code&gt; works fine for you. On Windows a spawn without a shell doesn't apply PATHEXT either, so a bare &lt;code&gt;git&lt;/code&gt; can fail where &lt;code&gt;git.exe&lt;/code&gt; succeeds.&lt;/li&gt;
&lt;li&gt;There's more than one copy. Run &lt;code&gt;where git&lt;/code&gt; and count the lines. A PortableGit bundled inside another client, a WSL git and your own install are all candidates, and the first one in the combined PATH wins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The durable fix is to stop relying on inheritance. Point the app at the absolute path in its own settings (a &lt;code&gt;git.path&lt;/code&gt; style key, or whatever the equivalent is), or give it an explicit environment block. For an MCP server that's normally an &lt;code&gt;env&lt;/code&gt; entry in the config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"PATH"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;ffmpeg&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;bin;&amp;lt;your existing PATH here&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some clients expand &lt;code&gt;${PATH}&lt;/code&gt; for you and some don't, so check yours before assuming either way.&lt;/p&gt;

&lt;p&gt;One thing to watch if you edit PATH with &lt;code&gt;setx&lt;/code&gt;: it crops values at 1024 characters, silently, so a long PATH loses entries off the end. The documented maximum size of a single environment variable is 32,767 characters, and if you're anywhere near that, something else in your setup needs a look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this usually lands
&lt;/h2&gt;

&lt;p&gt;Restarting an app solves this more often than it sounds like it should, but restart means the process, not the window. If a full restart and a reboot both fail, stop suspecting the environment. The tool is probably fine, and the app's own lookup logic is what to read next.&lt;/p&gt;

&lt;p&gt;Both cases came from real threads: the git one is &lt;a href="https://github.com/Kilo-Org/kilocode/issues/13452" rel="noopener noreferrer"&gt;Kilo-Org/kilocode#13452&lt;/a&gt; and the ffmpeg one is &lt;a href="https://github.com/CherryHQ/cherry-studio/issues/18528" rel="noopener noreferrer"&gt;CherryHQ/cherry-studio#18528&lt;/a&gt;. I've only had eyes on those two, so your mileage may vary. Hope this helps.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Socket error 10013 on Windows: your port is reserved, not blocked</title>
      <dc:creator>MilkyWay008</dc:creator>
      <pubDate>Fri, 18 Sep 2026 17:08:26 +0000</pubDate>
      <link>https://dev.to/milkyway008/socket-error-10013-on-windows-your-port-is-reserved-not-blocked-1kja</link>
      <guid>https://dev.to/milkyway008/socket-error-10013-on-windows-your-port-is-reserved-not-blocked-1kja</guid>
      <description>&lt;p&gt;If a dev tool on Windows has ever failed on you with this line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;An attempt was made to access a socket in a way forbidden by its access permissions. (os error 10013)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...your first instinct is probably the firewall, or antivirus, or "I need to run this as admin". I've gone down that road. Turned Defender off, opened an elevated terminal, got the exact same error. Most of the time it's something more boring: Windows has already given that port away, and nothing in the error message says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two errors that look alike and mean opposite things
&lt;/h2&gt;

&lt;p&gt;If you spend any time in Windows socket logs, you'll keep meeting two of them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;10048&lt;/code&gt; (WSAEADDRINUSE) means something is already listening on that port. &lt;code&gt;netstat -ano | findstr :47311&lt;/code&gt; gives you the PID, and killing that process fixes it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;10013&lt;/code&gt; (WSAEACCES) means you aren't allowed to bind there at all. Nothing is listening. The firewall isn't involved, and running as admin usually changes nothing, because the port sits inside a range Windows has reserved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On any machine running Hyper-V, WSL2 or Docker Desktop, the Host Network Service (WinNAT) books blocks of TCP ports for itself when it starts. Those blocks aren't occupied, they're reserved. A tool that grabs a random port for a local listener, like an OAuth callback, a preview server or a test runner, can land inside one. And since the port gets picked at random each run, the failure comes and goes. That's the part that makes it so annoying to chase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: look at the reservations
&lt;/h2&gt;

&lt;p&gt;No admin needed for this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netsh int ipv4 show excludedportrange &lt;span class="nv"&gt;protocol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a table of start ports and the number of ports in each block. On a box with WSL2 or Docker Desktop I usually see a few blocks of 100 somewhere up in the 49000-51000 region, but which bases you get depends on the machine and can shift between boots. Run it fresh instead of trusting output you saved last month.&lt;/p&gt;

&lt;p&gt;If you want the ephemeral range as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netsh int ipv4 show dynamicport tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check whether your port falls inside one of the blocks. Everything from &lt;code&gt;startport&lt;/code&gt; up to &lt;code&gt;startport + numberofports - 1&lt;/code&gt; is covered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: move the port, if you're allowed to
&lt;/h2&gt;

&lt;p&gt;This is the fix that actually stuck for the person in the thread I was reading (&lt;a href="https://github.com/github/copilot-cli/issues/4463" rel="noopener noreferrer"&gt;github/copilot-cli#4463&lt;/a&gt;). Copilot CLI picks its OAuth callback port automatically, and the listener blew up with 10013 before the browser even opened, which is a fun way to spend an afternoon. He listed the excluded ranges, pinned the port to 47000, restarted, and the auth flow completed.&lt;/p&gt;

&lt;p&gt;Most tools that do a loopback OAuth flow have a setting for this somewhere. For Copilot CLI it's the &lt;code&gt;auth.redirectPort&lt;/code&gt; key in the config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"auth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"redirectPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;47000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a plain dev server it's whatever your framework gives you: &lt;code&gt;--port&lt;/code&gt;, &lt;code&gt;PORT=&lt;/code&gt;, a &lt;code&gt;listen()&lt;/code&gt; argument. Pick something clear of every block, re-run &lt;code&gt;show excludedportrange&lt;/code&gt;, and confirm nothing covers it.&lt;/p&gt;

&lt;p&gt;One thing worth saying out loud. If the tool worked yesterday and fails today, that doesn't mean the problem fixed itself. It picked a different port. It'll be back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: if the port has to stay
&lt;/h2&gt;

&lt;p&gt;Two options here, one temporary and one that sticks. Both need an elevated prompt.&lt;/p&gt;

&lt;p&gt;The temporary one releases the NAT service's holds around your bind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;net stop winnat
&lt;span class="c"&gt;# start your service, or finish the bind, here&lt;/span&gt;
net start winnat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Restart-Service winnat&lt;/code&gt; does it in a single step if you only need the reservations dropped and re-taken. While winnat is stopped, outbound NAT and port forwarding for WSL2, Hyper-V VMs and Docker Desktop containers degrade, so don't do this on a machine that's actively serving containers. The reservations also come back the next time the service starts, reboot included, and on newer builds the Host Network Service may re-take them almost immediately, in which case this buys you nothing.&lt;/p&gt;

&lt;p&gt;The durable option is to make the reservation yours instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netsh interface ipv4 add excludedportrange &lt;span class="nv"&gt;protocol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tcp &lt;span class="nv"&gt;startport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;47311 &lt;span class="nv"&gt;numberofports&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nv"&gt;store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;persistent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Windows now keeps that port out of dynamic allocation, so nothing else claims it and your explicit bind keeps working across restarts. &lt;code&gt;store=persistent&lt;/code&gt; is the part that survives a reboot. To undo it later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netsh interface ipv4 delete excludedportrange &lt;span class="nv"&gt;protocol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tcp &lt;span class="nv"&gt;startport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;47311 &lt;span class="nv"&gt;numberofports&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nv"&gt;store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;persistent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You are now the owner of a reserved port. That's the whole point, but it's worth remembering you did it, for the day months from now when something else can't have that port.&lt;/p&gt;

&lt;h2&gt;
  
  
  If the excluded list is empty and you still get 10013
&lt;/h2&gt;

&lt;p&gt;Then it isn't the NAT service. The usual suspects at that point are endpoint security and VPN filter drivers. Some EDR and AV products, plus a few corporate VPN clients, install a filter that refuses binds for specific programs, and the error surfaces as the same 10013. The tell is a binary that fails on one machine and works on another with the same port, along with a block event in the security product's own log. I haven't hit that one often, so I'd read the vendor log before guessing at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;I treated 10013 as a permissions problem for longer than I'd like to admit before it occurred to me to just look at the reserved ranges first. It's a two second command, and it tells you straight away whether you're chasing a port collision or something else entirely. Run it before you touch the firewall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netsh int ipv4 show excludedportrange &lt;span class="nv"&gt;protocol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Topic came from a stuck-user thread over on the Copilot CLI repo. The port-pinning fix there is the reporter's, not mine, and it's the one that got him unblocked.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>devops</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Docker EACCES after an image upgrade: check the container's uid against the mount</title>
      <dc:creator>MilkyWay008</dc:creator>
      <pubDate>Thu, 17 Sep 2026 17:06:59 +0000</pubDate>
      <link>https://dev.to/milkyway008/docker-eacces-after-an-image-upgrade-check-the-containers-uid-against-the-mount-4h2k</link>
      <guid>https://dev.to/milkyway008/docker-eacces-after-an-image-upgrade-check-the-containers-uid-against-the-mount-4h2k</guid>
      <description>&lt;p&gt;A container that worked yesterday doesn't start today. The image tag moved by one patch version, nothing on your side changed, and the logs end with something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EACCES: permission denied, mkdir '/root/.flowise'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or you get the friendlier version. The web UI comes up, the API answers 502, and further up the log there's a Python &lt;code&gt;PermissionError&lt;/code&gt; from the entrypoint plus &lt;code&gt;sqlite3.OperationalError: unable to open database file&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Same failure, different outfit. The image changed which user it runs as (or which HOME it uses, or which subfolder of the mounted directory it writes into), and the directory on your host is still owned by somebody else. I've run into this class a few times now...... it's worth knowing the 2-minute diagnosis instead of rolling the tag back and hoping. Rolling back does work, by the way, and it's a fine stopgap. It just comes back on the next upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the uid matters
&lt;/h2&gt;

&lt;p&gt;A bind mount is your host directory, handed to the container as is. No translation layer, no magic.&lt;/p&gt;

&lt;p&gt;When the container process runs as uid X and tries to &lt;code&gt;mkdir&lt;/code&gt; inside a directory owned by uid Y, the kernel does the normal thing: it checks X against the owner, group and "other" bits of that host inode. Not the owner, not in the group, and no write bit for others, and you get &lt;code&gt;EACCES&lt;/code&gt;. That's the entire bug.&lt;/p&gt;

&lt;p&gt;Two details that bite people.&lt;/p&gt;

&lt;p&gt;Root inside the container is a special case. Linux lets uid 0 bypass a lot of permission checks through capabilities like &lt;code&gt;CAP_DAC_OVERRIDE&lt;/code&gt;, so running a container as root often hides this problem instead of fixing it. Then a new image switches to a non-root &lt;code&gt;USER&lt;/code&gt;, or you move to rootless Docker or Podman where container root maps to a host subuid, and the bypass that was covering for you is gone.&lt;/p&gt;

&lt;p&gt;A mode &lt;code&gt;700&lt;/code&gt; directory is the ugly case. Only the host owner has any access at all, so every other uid in the container is locked out completely and you get a hard crash instead of a partial one.&lt;/p&gt;

&lt;p&gt;Why does an upgrade trigger it? Because the new image changed &lt;code&gt;USER&lt;/code&gt; (root to 1000, or 1000 to 10001), changed &lt;code&gt;HOME&lt;/code&gt;, or started writing to a path inside the mount that the old image never touched. Nothing on your side moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnose it in a couple of minutes
&lt;/h2&gt;

&lt;p&gt;All of these are read-only, safe on a running box.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs &amp;lt;container&amp;gt; &lt;span class="nt"&gt;--tail&lt;/span&gt; 50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for the failing path, not just the error. That path is the directory that has to be writable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Config.User}}'&lt;/span&gt; flowiseai/flowise:3.1.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty output means the image runs as root. &lt;code&gt;1000&lt;/code&gt; or &lt;code&gt;10001:10001&lt;/code&gt; means it doesn't. Run the same command against the old tag and compare. This is usually the moment it clicks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--entrypoint&lt;/span&gt; sh flowiseai/flowise:3.1.4 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'id; echo $HOME; ls -ln /root'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That prints the uid/gid, the HOME the image sets, and who owns the target directory. Worth knowing an image can set &lt;code&gt;USER&lt;/code&gt; and forget &lt;code&gt;HOME&lt;/code&gt;; then tools write somewhere odd and the error points at the wrong place.&lt;/p&gt;

&lt;p&gt;Then the host side:&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;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ln&lt;/span&gt; /path/to/host/mount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the image runs as 1000 and the mount is owned by 1000, permissions are not your problem. Go look at SELinux (more below) or at the app's own config. If the numbers differ, you found it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix ladder
&lt;/h2&gt;

&lt;p&gt;1st, and usually right: make the host directory owned by the uid the container runs as.&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 chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 1000:1000 /path/to/host/mount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works everywhere, takes seconds. The catch is you've now hardcoded one uid, so the next image that changes its uid breaks you again. Which is the trap you're standing in right now.&lt;/p&gt;

&lt;p&gt;2nd, and more durable: use a named volume instead of a bind mount.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume create flowise_data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;flowise&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;flowise_data:/root/.flowise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Docker first creates a named volume and the image has content at that path, it copies the image's own directory into the volume, ownership included. So the volume starts out owned by whatever uid the image expects, and the container can write to it. The costs: your data isn't at a host path you can scp from, and if the volume already exists with a different uid you may have to remove and recreate it (back it up first, please).&lt;/p&gt;

&lt;p&gt;3rd: override the user, if you want the container to match your host user.&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;flowise&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1000:1000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handy for local dev where you want files to land as you. It backfires when the image's own files belong to a different uid (a home directory owned by &lt;code&gt;node&lt;/code&gt; is still not writable by uid 1000), and on Docker Desktop the uid model isn't your host's anyway.&lt;/p&gt;

&lt;p&gt;4th: PUID/PGID, when the image implements it. linuxserver-style images read &lt;code&gt;PUID&lt;/code&gt; and &lt;code&gt;PGID&lt;/code&gt; from the environment and chown their data directories at startup. Only works where it's supported, so check the docs for the image you're actually running.&lt;/p&gt;

&lt;p&gt;5th: pin the previous tag. &lt;code&gt;image: flowiseai/flowise:3.1.3&lt;/code&gt;. Two minutes of work, unblocks you, fixes nothing, and shows up again on the next upgrade. Fine as a stopgap while an upstream issue gets attention. Not a plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas that will cost you an afternoon
&lt;/h2&gt;

&lt;p&gt;SELinux on Fedora and RHEL. If &lt;code&gt;EACCES&lt;/code&gt; survives a &lt;em&gt;correct&lt;/em&gt; chown, the directory is missing the container file label. Add &lt;code&gt;:z&lt;/code&gt; or &lt;code&gt;:Z&lt;/code&gt; to the mount. A correct chown that changes nothing is the SELinux tell.&lt;/p&gt;

&lt;p&gt;Docker Desktop on Windows and macOS. Containers there run in a Linux VM with its own uid space, so everything above is for Linux hosts. Desktop layers its own file-sharing quirks on top.&lt;/p&gt;

&lt;p&gt;It isn't always the mount root that needs fixing. If the app writes to &lt;code&gt;$HOME/.appname&lt;/code&gt;, that subdirectory is the one that has to be writable, and it may be the one that was just created and failed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;chown -R&lt;/code&gt; on a big or shared directory is slow, and it re-owns data belonging to other users and apps without asking. On a 200 GB media mount, don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  One honest caveat
&lt;/h2&gt;

&lt;p&gt;I went and read two of these threads. The one with a proper A/B proof is an agent-canvas image running as uid 10001 against a &lt;code&gt;700&lt;/code&gt; mount, which booted fine at &lt;code&gt;777&lt;/code&gt;...... that one nails the mechanism. The other, Flowise 3.1.4 failing on &lt;code&gt;mkdir '/root/.flowise'&lt;/code&gt;, has the exact symptom and a confirmed "revert to 3.1.3 fixes it", but the issue itself has zero comments, so the uid theory there is plausible rather than confirmed. If chown doesn't do it for you on that build, you're probably looking at a different startup bug wearing the same error message.&lt;/p&gt;

&lt;p&gt;Either way: next time a container throws &lt;code&gt;EACCES&lt;/code&gt; after an upgrade, run the inspect commands before you touch anything. Comparing &lt;code&gt;{{.Config.User}}&lt;/code&gt; between two tags takes ten seconds, and it tells you whether you have a permissions problem at all.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>tutorial</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why your MCP server randomly shows no tools: the npx startup tax</title>
      <dc:creator>MilkyWay008</dc:creator>
      <pubDate>Wed, 16 Sep 2026 17:16:18 +0000</pubDate>
      <link>https://dev.to/milkyway008/why-your-mcp-server-randomly-shows-no-tools-the-npx-startup-tax-2l0e</link>
      <guid>https://dev.to/milkyway008/why-your-mcp-server-randomly-shows-no-tools-the-npx-startup-tax-2l0e</guid>
      <description>&lt;p&gt;If you've wired an MCP server into an agent client with &lt;code&gt;"command": "npx"&lt;/code&gt;, you've probably met this one. The server runs fine when you launch it by hand. The agent insists the tools don't exist. No error, nothing useful in the log, and maybe it works again tomorrow.&lt;/p&gt;

&lt;p&gt;I've been digging through a pile of these reports lately, and I'm fairly sure about the cause: it's usually not your server...... it's the npx layer you launched it with.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the failure looks like
&lt;/h2&gt;

&lt;p&gt;Three flavors of the same thing, from the threads I read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent announces that the server's tools aren't available, as if you never configured them.&lt;/li&gt;
&lt;li&gt;One session works, the next is dead, with nothing changed.&lt;/li&gt;
&lt;li&gt;The same server connects fine in the MCP Inspector and fails in the desktop app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of that sounds familiar, the server is probably fine. The spawn is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why npx makes it worse
&lt;/h2&gt;

&lt;p&gt;MCP's stdio transport works by the client launching your server as a subprocess and talking JSON-RPC over stdin/stdout. The spec is blunt about it: "the client launches the MCP server as a subprocess." So startup time is the client's problem, and clients wait with a deadline.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx -y &amp;lt;package&amp;gt;&lt;/code&gt; is not a shortcut to a binary. It boots npm (itself a Node program), resolves the package, consults the npx cache, and only then spawns your server as yet another child process. On a cold cache there's a download in there too. Every step of that happens inside the client's handshake window.&lt;/p&gt;

&lt;p&gt;Then the deadline expires. In Copilot CLI's case the handshake budget is reported as a fixed 60,000 ms with no retry. The reporter measured 22 of 76 handshakes blowing past it in a single day (29%). Once a server misses the budget it's marked failed for the entire session, and the agent just says the tools don't exist. That's the silent-no-tools symptom.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;From the reporter's measurements, same machine, three ways to launch the same server:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;How it was launched&lt;/th&gt;
&lt;th&gt;Average&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npx -y @azure-devops/mcp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;15.39s (runs: 27.91 / 8.31 / 9.96)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;global &lt;code&gt;.cmd&lt;/code&gt; shim&lt;/td&gt;
&lt;td&gt;3.32s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;direct &lt;code&gt;node .../dist/index.js&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;3.21s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's one person, one machine, one day, so read it as a range rather than a law of nature. But the direction matches what I see locally.&lt;/p&gt;

&lt;p&gt;My own check, Windows 11, Node 22.23.2, with the package already warm in the npx cache and &lt;code&gt;--offline&lt;/code&gt; so nothing was downloaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;npx --offline -y @playwright/mcp@0.0.81 --version            1.54s / 1.59s / 1.64s
&lt;/span&gt;&lt;span class="gp"&gt;node &amp;lt;npx-cache&amp;gt;&lt;/span&gt;/node_modules/@playwright/mcp/cli.js &lt;span class="nt"&gt;--version&lt;/span&gt;   0.44s / 0.44s / 0.48s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;--help&lt;/code&gt;, which loads the whole CLI, it was 7.56s versus 1.74s. Booting npm on its own costs about 0.6s on this box; booting node about 0.1s. So even with a warm cache you're paying seconds for nothing...... and a cold cache is a different animal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your own setup first
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Time the command your client actually runs. Compare &lt;code&gt;time npx -y &amp;lt;your-server&amp;gt; --help&lt;/code&gt; against &lt;code&gt;time node &amp;lt;path-to-entrypoint&amp;gt; --help&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run the server outside the client. &lt;code&gt;npx @modelcontextprotocol/inspector&lt;/code&gt;, then paste the same command in. Works in the Inspector, fails in the app? You have a spawn problem, not a server problem.&lt;/li&gt;
&lt;li&gt;Read the client's MCP logs. Claude Desktop writes to &lt;code&gt;%APPDATA%\Claude\logs\mcp*.log&lt;/code&gt; on Windows and &lt;code&gt;~/Library/Logs/Claude/&lt;/code&gt; on macOS. Other clients mostly just say the server failed and stop there.&lt;/li&gt;
&lt;li&gt;Ask whether the client retries at all. Most don't. One miss means dead tools for that session.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The fix, in the order I'd trust it
&lt;/h2&gt;

&lt;p&gt;1st, install the server globally and point &lt;code&gt;command&lt;/code&gt; at the resolved binary instead of npx. This is the fix the reporter landed on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @playwright/mcp
npm prefix &lt;span class="nt"&gt;-g&lt;/span&gt;                      &lt;span class="c"&gt;# where global packages land&lt;/span&gt;
where.exe playwright-mcp           &lt;span class="c"&gt;# Windows: full path to the shim&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; playwright-mcp          &lt;span class="c"&gt;# macOS / Linux&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"playwright"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Users&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;you&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;AppData&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Roaming&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;npm&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;playwright-mcp.cmd"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2nd, skip the shim and hand the entrypoint to node. This was the fastest of the three measured paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"C:/Users/you/AppData/Roaming/npm/node_modules/@playwright/mcp/cli.js"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3rd, if you have to keep npx, at least warm the cache. Run the exact command once by hand (&lt;code&gt;npx -y &amp;lt;server&amp;gt; --help&lt;/code&gt;) so the package is fetched, then restart the client. It helps, but you still pay the npm boot on every session.&lt;/p&gt;

&lt;p&gt;4th, raise the timeout if your client has one. The Copilot CLI issue body says the budget is hard-coded, while a commenter on that same issue says there's a per-entry &lt;code&gt;timeout&lt;/code&gt; in the config. I can't settle that from here, so check your own client's docs and version before trusting either claim.&lt;/p&gt;

&lt;p&gt;5th, use a remote transport when the server supports it. An HTTP/SSE server is already running, so there's no per-session spawn to be slow. It's not a coincidence that one vendor's resolution was rewriting the desktop app's MCP client instead of fixing anyone's config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Windows gotchas that masquerade as the same bug
&lt;/h2&gt;

&lt;p&gt;A raw absolute path in &lt;code&gt;command&lt;/code&gt; can kill every cold start in Claude Desktop: &lt;code&gt;Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'c:'&lt;/code&gt;. Use &lt;code&gt;node&lt;/code&gt; plus a file path, or an &lt;code&gt;.exe&lt;/code&gt;/&lt;code&gt;.cmd&lt;/code&gt;, and remember the config is read only at startup, so quit from the tray rather than closing the window.&lt;/p&gt;

&lt;p&gt;Also, &lt;code&gt;"command": "C:\\Program Files\\nodejs\\npx.cmd"&lt;/code&gt; gets split at the space by the cmd.exe handoff and tries to run &lt;code&gt;C:\Program&lt;/code&gt;. The 8.3 short path (&lt;code&gt;C:\\PROGRA~1\\nodejs\\npx.cmd&lt;/code&gt;) sidesteps it.&lt;/p&gt;

&lt;p&gt;One more config-shape trap, from a different toolkit: if the client defaults to SSE, a stdio-shaped config (command/args) throws &lt;code&gt;URL is required for SSE transport&lt;/code&gt;. Declare the transport explicitly and the error goes away.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it isn't npx
&lt;/h2&gt;

&lt;p&gt;If the server connects and then dies, or the process exits immediately, spawn timing is irrelevant...... go read the server's stderr. A subprocess that hangs past the budget gets treated exactly like a slow one, so a wedged server and a slow launcher look identical from the outside.&lt;/p&gt;

&lt;p&gt;None of this is exotic. It's a handshake deadline most clients keep private plus a launcher that's slower than most of us assume. Ten minutes moving your config off npx and you can stop thinking about it.&lt;/p&gt;

&lt;p&gt;I built Smart-MCP-Proxy for the adjacent itch, running a lot of servers without restarting the client every time: &lt;a href="https://github.com/MilkyWay008/Smart-MCP-Proxy" rel="noopener noreferrer"&gt;https://github.com/MilkyWay008/Smart-MCP-Proxy&lt;/a&gt;. Worth a look if you've got a wall of MCP entries. Hope this helps somebody.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your Windows agent CLI pauses 15 seconds before every command: read the Procmon trace</title>
      <dc:creator>MilkyWay008</dc:creator>
      <pubDate>Tue, 15 Sep 2026 17:12:07 +0000</pubDate>
      <link>https://dev.to/milkyway008/your-windows-agent-cli-pauses-15-seconds-before-every-command-read-the-procmon-trace-429k</link>
      <guid>https://dev.to/milkyway008/your-windows-agent-cli-pauses-15-seconds-before-every-command-read-the-procmon-trace-429k</guid>
      <description>&lt;p&gt;I keep a list of complaints that sound like vibes and turn out to be real bugs. One of them was "the agent CLI on Windows sits there thinking for 15 seconds before it starts". Not slow streaming...... just dead air between Enter and the first byte of actual work, every single command.&lt;/p&gt;

&lt;p&gt;Turns out that one has a trace behind it. There's an open issue on the Codex repo (openai/codex#41351) with the kind of evidence I wish every bug report had. Numbers first: about 15.6 seconds per command in the unelevated Windows sandbox, and roughly 122ms for the same command with the sandbox set to &lt;code&gt;danger-full-access&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Same machine. Same binary. A 128x difference.&lt;/p&gt;

&lt;p&gt;That is not your CPU, and it's not your disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the trace actually showed
&lt;/h2&gt;

&lt;p&gt;Procmon, filtered to the process and to &lt;code&gt;CreateFile&lt;/code&gt;, sorted by duration, puts almost the whole delay in one row: about 15.42 seconds on a single &lt;code&gt;CreateFile&lt;/code&gt;, result &lt;code&gt;OBJECT PATH INVALID&lt;/code&gt;, on a path that starts with two backslashes and names the NUL device. The stack runs &lt;code&gt;CreateFileW&lt;/code&gt; -&amp;gt; &lt;code&gt;GetDriveTypeW&lt;/code&gt; -&amp;gt; &lt;code&gt;ZwCreateFile&lt;/code&gt; -&amp;gt; &lt;code&gt;FLTMGR.SYS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two things stand out. 1st, the path is a device path that isn't spelled the way it needs to be. 2nd, &lt;code&gt;GetDriveTypeW&lt;/code&gt; is sitting in the middle of it, and that function is a known hazard on paths it can't classify. Both of those matter, so here's the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  The path rules that get missed
&lt;/h2&gt;

&lt;p&gt;Microsoft's naming files, paths and namespaces page is worth the 10 minutes if you ever build paths by hand in Windows code. The short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NUL&lt;/code&gt; is a reserved device name. It works in any directory, which is why &lt;code&gt;&amp;gt; NUL&lt;/code&gt; works from anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;\\.\NUL&lt;/code&gt; uses the Win32 &lt;strong&gt;device namespace&lt;/strong&gt;. That's the explicit way to open a device.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;\\NUL&lt;/code&gt; is not a device path. A path that starts with two backslashes is a &lt;strong&gt;UNC name&lt;/strong&gt;, that is &lt;code&gt;\\server\share&lt;/code&gt;. So Windows routes it to the network provider chain (&lt;code&gt;LanmanWorkstation&lt;/code&gt;, &lt;code&gt;mrxsmb&lt;/code&gt;) and goes looking for a server called NUL. There isn't one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;\\?\C:\...&lt;/code&gt; is the extended-length prefix. It tells the API layer to skip string parsing and normalization, and it disables device-name translation too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the hazard on top: &lt;code&gt;GetDriveTypeW&lt;/code&gt; asks the provider layer what kind of drive a path refers to, and on an unavailable UNC path that call can block for a long time. This isn't theoretical. wxWidgets issue #8859 is the same shape from 2007: &lt;code&gt;wxFSVolume&lt;/code&gt; hung for about a minute on an unreachable &lt;code&gt;\\server\drive&lt;/code&gt; because its internal &lt;code&gt;FilteredAdd&lt;/code&gt; called &lt;code&gt;GetDriveType&lt;/code&gt;, and the reporter noted that drive-letter availability gets cached but UNC paths don't, so it hangs on every call. That one got patched in 2022.&lt;/p&gt;

&lt;p&gt;So a bogus double-backslash path, plus a function that queries the network provider, equals a stall. The path is the bug. The function is where the time goes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual defect is one over-escaped string
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;codex-rs/windows-sandbox-rs/src/acl.rs&lt;/code&gt;, the sandbox's &lt;code&gt;allow_null_device()&lt;/code&gt; passes this to &lt;code&gt;CreateFileW&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nf"&gt;to_wide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;r"\\\\.\\NUL"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.as_ptr&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks like ordinary Windows escaping. It isn't, because &lt;code&gt;r"..."&lt;/code&gt; in Rust is a raw string and raw strings don't process escapes. The bytes are literal: four leading backslashes, a dot, then two more backslashes, then &lt;code&gt;NUL&lt;/code&gt;. The correct literal for the device namespace has two leading backslashes and one after the dot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nf"&gt;to_wide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;r"\\.\NUL"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.as_ptr&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm not picking on Rust here, the trap exists everywhere verbatim strings do. C# &lt;code&gt;@"..."&lt;/code&gt;, PowerShell single quotes, raw string literals in most modern languages. You add a layer of escaping to be safe, and the path you ship means something different from the path you meant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest caveat: 15 seconds is not universal
&lt;/h2&gt;

&lt;p&gt;I nearly wrote a wrong paragraph about this, so here it is properly.&lt;/p&gt;

&lt;p&gt;On my own Windows 11 box, a malformed device path does not stall. It fails immediately. &lt;code&gt;\\NUL&lt;/code&gt; throws out of &lt;code&gt;GetFullPath&lt;/code&gt;, &lt;code&gt;CreateFileW&lt;/code&gt; returns error 161 (&lt;code&gt;ERROR_BAD_PATHNAME&lt;/code&gt;) in under a millisecond, and &lt;code&gt;GetDriveTypeW&lt;/code&gt; just says &lt;code&gt;DRIVE_NO_ROOT_DIR&lt;/code&gt;. Microseconds, not seconds.&lt;/p&gt;

&lt;p&gt;The reporter's machine behaves differently, and their own WPA trace explains why: a third-party filesystem minifilter, &lt;code&gt;360FsFlt.sys&lt;/code&gt;, sits in the path of that failing call and adds about 15.4s of its own. So the malformed path is a real defect either way, but the price of it depends on what is filtering I/O on your machine. Which is also why this is hard to search for. Fast on a clean VM, miserable on a real desk.&lt;/p&gt;

&lt;p&gt;What they did confirm with a controlled change is the direction of the fix. Byte-patching that one literal in &lt;code&gt;codex.exe&lt;/code&gt; and &lt;code&gt;codex-command-runner.exe&lt;/code&gt;, with nothing else changed, took &lt;code&gt;spawn_ready&lt;/code&gt; from 23.4s down to 0.6s, then 0.5s and 0.47s on repeats. Same versions, same machine, only the string fixed.&lt;/p&gt;

&lt;p&gt;Status while I'm writing this: the issue is still open, no merged upstream fix, and the same literal is still there on &lt;code&gt;main&lt;/code&gt; and in the 0.155 alpha I checked. So treat the correction as confirmed by inspection, not shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosing it on your own machine
&lt;/h2&gt;

&lt;p&gt;This recipe transfers to any "my tool pauses N seconds before doing anything" complaint, which is the main reason I wanted to write it up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run Procmon as admin, stop capture (Ctrl+E), clear (Ctrl+X), then start capture just before you reproduce.&lt;/li&gt;
&lt;li&gt;Ctrl+L to filter: &lt;code&gt;Process Name&lt;/code&gt; is your tool, and &lt;code&gt;Operation&lt;/code&gt; is &lt;code&gt;CreateFile&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Options -&amp;gt; Select Columns -&amp;gt; add &lt;code&gt;Duration&lt;/code&gt; and &lt;code&gt;Result&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Sort by Duration, descending. One row usually owns the whole delay.&lt;/li&gt;
&lt;li&gt;Right-click that row -&amp;gt; Properties -&amp;gt; Stack. If &lt;code&gt;FLTMGR.SYS&lt;/code&gt; shows up, you're not looking at app logic anymore, you're looking at a filesystem filter driver.&lt;/li&gt;
&lt;li&gt;To name the filter, capture with WPR and read Mini-Filter Delays in WPA, matching the PID and TID from Procmon.&lt;/li&gt;
&lt;li&gt;If the stack went through the network provider, correlate the registry and network activity: filter &lt;code&gt;Process Name&lt;/code&gt; is &lt;code&gt;System&lt;/code&gt; and &lt;code&gt;Path&lt;/code&gt; contains &lt;code&gt;LanmanWorkstation&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your tool stalls and the stack is pure application code, you have a different bug and this writeup won't help you.&lt;/p&gt;

&lt;h2&gt;
  
  
  A probe you can run in 30 seconds
&lt;/h2&gt;

&lt;p&gt;This demonstrates the path semantics, not the 15 seconds. Add the warm-up call, cold-start noise is real:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Add-Type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Namespace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;W&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-MemberDefinition&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sh"&gt;@'
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
public static extern uint GetDriveTypeW(string p);
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
public static extern IntPtr CreateFileW(string p, uint a, uint s, IntPtr sa, uint d, uint f, IntPtr t);
'@&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'NUL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'\\.\NUL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'\\.\\NUL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'\\NUL'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;W.K&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;CreateFileW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;x60000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IntPtr&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IntPtr&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;# warm up&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Diagnostics.Stopwatch&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;StartNew&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;W.K&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;CreateFileW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;x60000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IntPtr&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IntPtr&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Runtime.InteropServices.Marshal&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;GetLastWin32Error&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"{0,-10} {1,7:N2} ms  GetDriveType={2}  err={3}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Elapsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TotalMilliseconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;W.K&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;GetDriveTypeW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my box that prints err 161 for the over-escaped form and for &lt;code&gt;\\NUL&lt;/code&gt;, and err 5 for the two device-namespace forms. Err 5 is not a path failure, it means the path resolved and my probe just didn't ask for the right permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take away from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If you build Windows paths by hand, count the backslashes. &lt;code&gt;\\.\&lt;/code&gt; for devices, &lt;code&gt;\\?\&lt;/code&gt; only for extended-length paths, and never a leading &lt;code&gt;\\&lt;/code&gt; unless you actually mean a server.&lt;/li&gt;
&lt;li&gt;Don't hand constructed paths to &lt;code&gt;GetDriveTypeW&lt;/code&gt;. It asks the provider layer, and that can block.&lt;/li&gt;
&lt;li&gt;Stuck on Codex specifically? &lt;code&gt;sandbox = "danger-full-access"&lt;/code&gt; under &lt;code&gt;[windows]&lt;/code&gt; is the measured escape hatch, about 122ms. It weakens the sandbox, so that's a workaround, not a fix.&lt;/li&gt;
&lt;li&gt;Chasing a stall you can't explain? Trace before you theorize. One Procmon row sorted by duration beats an hour of guessing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I could be wrong about the exact cost on your machine, since all I have is the reporter's trace and my own clean box to compare against. But the path spelling is checkable, and it is wrong upstream, so that part I'm fairly confident about. The trace and the before/after numbers came from the people in that thread, not from me. I just read it and went, huh, that's the trap I've tripped over before.&lt;/p&gt;

&lt;p&gt;Docs I leaned on: &lt;a href="https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file" rel="noopener noreferrer"&gt;Naming Files, Paths, and Namespaces&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getdrivetyper" rel="noopener noreferrer"&gt;GetDriveTypeW&lt;/a&gt;, &lt;a href="https://github.com/openai/codex/issues/41351" rel="noopener noreferrer"&gt;the Codex issue&lt;/a&gt;, and &lt;a href="https://github.com/wxWidgets/wxWidgets/issues/8859" rel="noopener noreferrer"&gt;wxWidgets #8859&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>JavaScript heap out of memory: read the GC line before you raise the heap size</title>
      <dc:creator>MilkyWay008</dc:creator>
      <pubDate>Mon, 14 Sep 2026 17:20:34 +0000</pubDate>
      <link>https://dev.to/milkyway008/javascript-heap-out-of-memory-read-the-gc-line-before-you-raise-the-heap-size-3edp</link>
      <guid>https://dev.to/milkyway008/javascript-heap-out-of-memory-read-the-gc-line-before-you-raise-the-heap-size-3edp</guid>
      <description>&lt;p&gt;You know the moment. You reopen a long-running session in your AI coding CLI and instead of your conversation you get a wall of garbage-collector spam that ends like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
Aborted (core dumped)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code 134, nothing partial to salvage, session unusable. The standard internet answer is "raise &lt;code&gt;--max-old-space-size&lt;/code&gt;." I've watched that advice fail more than once, so here's what the message is really saying, and how to tell which of a few very different problems you're actually looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the crash means
&lt;/h2&gt;

&lt;p&gt;Node aborts when V8's old generation can't grow any further. There are two versions of the message and they mean slightly different things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Ineffective mark-compacts near heap limit&lt;/code&gt; means full mark-compacts are running and reclaiming almost nothing, so V8 stops trying.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Reached heap limit&lt;/code&gt; means an allocation failed and the old generation cannot expand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are terminal. Neither tells you why the heap filled up, which is the only part worth knowing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ceiling is not 4 GB
&lt;/h2&gt;

&lt;p&gt;If you've read that "Node caps the V8 heap at around 4 GB," that's only half true. V8 derives the default from your physical memory: roughly half of RAM, with a 2 GB floor value as the base default, and the cap only doubles to 4 GB on 64-bit builds with about 16 GB of RAM or more.&lt;/p&gt;

&lt;p&gt;Print yours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"console.log(require('v8').getHeapStatistics().heap_size_limit/1024/1024 + ' MB')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the 13 GB Windows box I'm typing this on, that prints &lt;code&gt;2096 MB&lt;/code&gt;. So on a normal laptop the wall is 2 GB, not 4. (The number includes the young generation, so it comes out slightly above whatever &lt;code&gt;--max-old-space-size&lt;/code&gt; value is in effect.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The GC line is the actual diagnosis
&lt;/h2&gt;

&lt;p&gt;When Node dies this way it prints the last few compactions first. Here's a real one from a Copilot CLI crash report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;Mark-Compact 4062.4 (4098.2) -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;4061.5 &lt;span class="o"&gt;(&lt;/span&gt;4097.5&lt;span class="o"&gt;)&lt;/span&gt; MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the arrow. 4062 MB in, 4061 MB out. About a megabyte reclaimed out of four gigabytes. That is not a ceiling problem. Those objects are still referenced by the running process, and raising the ceiling just moves the wall a few gigabytes to the right. In that same thread someone tried &lt;code&gt;NODE_OPTIONS=--max-old-space-size=8192&lt;/code&gt; and got the other message instead: &lt;code&gt;Reached heap limit&lt;/code&gt;. Same crash, later.&lt;/p&gt;

&lt;p&gt;One more thing worth knowing: not every Node CLI honors that variable. Single-executable builds have been reported ignoring &lt;code&gt;NODE_OPTIONS&lt;/code&gt; outright, so you can apply the popular fix and change nothing at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three problems wearing the same error message
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Retained objects.&lt;/strong&gt; Reclaim per compact is near zero and &lt;code&gt;heapUsed&lt;/code&gt; sits right at the limit. The tool is holding onto history, transcripts, or cached file contents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A genuinely huge working set.&lt;/strong&gt; The compacts do reclaim real memory and you still reach the ceiling. Here, raising the limit is the honest answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Something that isn't the JavaScript heap at all.&lt;/strong&gt; &lt;code&gt;rss&lt;/code&gt; climbs while &lt;code&gt;heapUsed&lt;/code&gt; stays flat. That's native or FFI memory, and the heap flag is irrelevant to it. There's a Kilo CLI report with exactly this shape: RSS at 3.19 GB, peak around 4 GB, then a segfault inside an FFI trampoline after roughly 56 minutes. On Windows you might see the machine go down instead of the process: one Claude Code report has &lt;code&gt;node.exe&lt;/code&gt; climbing to 45 GB and hard-locking the box, and raising the pagefile from 2 GB to 16 GB only stretched the crash from 7-13 minutes to about 3 hours. That's memory pressure, not a V8 cap.&lt;/p&gt;

&lt;p&gt;Desktop apps add their own multiplier. Base64-inlining an attachment copies the file several times over. One report had a 238 MB PDF turn into about 317 MB of base64, then get copied again by the buffer and the JSON serialization, blowing the heap on an 8 GB machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to try, in order
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Read the compact line and your &lt;code&gt;heapUsed&lt;/code&gt; before changing any flag. Near-zero reclaim means stop resuming that session.&lt;/li&gt;
&lt;li&gt;If it's retained state: start a fresh session and read the old transcript instead of restoring it. Prune or rotate the stored session-history JSON so the next resume loads less. Keep large files out of the process by passing a path rather than inlining the content.&lt;/li&gt;
&lt;li&gt;If the working set is genuinely large, raise the cap with headroom: &lt;code&gt;NODE_OPTIONS=--max-old-space-size=6144&lt;/code&gt;. The host needs the RAM to back that up.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;rss&lt;/code&gt; is the number climbing, stop tuning V8 and go looking for a native leak or a Windows commit-charge problem.&lt;/li&gt;
&lt;li&gt;If you're filing an issue, bring evidence with it: run with &lt;code&gt;--heapsnapshot-near-heap-limit=1&lt;/code&gt;, open the snapshot in Chrome DevTools, and paste the exact FATAL line, the GC lines, the version, the OS, and whether the session was resumed. That's usually the difference between a report that gets fixed and one that gets closed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One honest caveat about version bumps. In the Copilot CLI thread the maintainer pointed at a prerelease, a second user reproduced the crash on that prerelease anyway, and the changelog for a later build says long-running sessions "return freed memory to the system instead of holding gigabytes of it." So yes, upstream fixes for this class do exist and ship eventually. But the pointer alone wasn't enough, and there were about a dozen other issues in that repo carrying the same error string.&lt;/p&gt;

&lt;p&gt;None of this needs a profiler to start. Read the arrow in the GC line first, because most of these crashes turn out not to be a cap problem at all.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>The MCP Python SDK 2.0 broke the wrapper libraries. Here's how to spot it and pin back</title>
      <dc:creator>MilkyWay008</dc:creator>
      <pubDate>Sun, 13 Sep 2026 17:22:32 +0000</pubDate>
      <link>https://dev.to/milkyway008/the-mcp-python-sdk-20-broke-the-wrapper-libraries-heres-how-to-spot-it-and-pin-back-346n</link>
      <guid>https://dev.to/milkyway008/the-mcp-python-sdk-20-broke-the-wrapper-libraries-heres-how-to-spot-it-and-pin-back-346n</guid>
      <description>&lt;p&gt;If your MCP tools started failing a few weeks ago and nothing in your own code changed, you're not imagining it.&lt;/p&gt;

&lt;p&gt;The official Python &lt;code&gt;mcp&lt;/code&gt; package shipped 2.0.0 on 2026-07-28. It's a real major version, with public APIs removed and renamed. Latest on PyPI right now is 2.2.0, and a plain &lt;code&gt;pip install mcp&lt;/code&gt; resolves to 2.x. Any library that depends on &lt;code&gt;mcp&lt;/code&gt; without an upper bound quietly floated forward and broke at install time.&lt;/p&gt;

&lt;p&gt;I spent a while reproducing this in throwaway venvs with 1.29.1 and 2.2.0 side by side. Here's what actually changed, what the errors look like, and how to get moving again.&lt;/p&gt;

&lt;h2&gt;
  
  
  1st, one name collision to get out of the way
&lt;/h2&gt;

&lt;p&gt;Two SDKs share the name. Python &lt;code&gt;mcp&lt;/code&gt; is the one on PyPI. The Rust one is &lt;code&gt;rmcp&lt;/code&gt; on crates.io, which is what Goose and some other Rust agents use, and it's on 3.x now. If you're debugging a Rust agent, the Python 2.x story is not your story. I nearly chased the wrong version myself...... glad I checked the crate name first.&lt;/p&gt;

&lt;h2&gt;
  
  
  1st break: streamablehttp_client is gone
&lt;/h2&gt;

&lt;p&gt;In 1.x:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.client.streamable_http&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamablehttp_client&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;streamablehttp_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sse_read_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;as &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_session_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In 2.x that import raises ImportError. The replacement is:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.client.streamable_http&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamable_http_client&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;streamable_http_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;as &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two changes there. The name has underscores now, and the context manager yields a 2-tuple. The 3rd element, &lt;code&gt;get_session_id&lt;/code&gt;, is gone.&lt;/p&gt;

&lt;p&gt;That 2nd part is the one that hurts, because it doesn't fail at import. The wrapper imports fine, then blows up at runtime, and you get the error a lot of people are probably googling right now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValueError: not enough values to unpack (expected 3, got 2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I reproduced that against a live streamable-HTTP server on 2.2.0. In the installed package it's &lt;code&gt;mcp/client/streamable_http.py&lt;/code&gt;, around line 753: &lt;code&gt;yield read_stream, write_stream&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2nd break: the transport kwargs are gone
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;StreamableHTTPTransport.__init__&lt;/code&gt; in 2.x is just &lt;code&gt;(self, url)&lt;/code&gt;. The &lt;code&gt;timeout&lt;/code&gt;, &lt;code&gt;sse_read_timeout&lt;/code&gt;, &lt;code&gt;headers&lt;/code&gt; and &lt;code&gt;auth&lt;/code&gt; keywords aren't accepted there anymore; set them on the httpx2 &lt;code&gt;AsyncClient&lt;/code&gt; instead. Pass them and you get &lt;code&gt;TypeError: unexpected keyword argument 'timeout'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Honesty note in the other direction: &lt;code&gt;sse_client&lt;/code&gt; still takes &lt;code&gt;timeout&lt;/code&gt; and &lt;code&gt;sse_read_timeout&lt;/code&gt;. So the "all the transport kwargs got dropped" version of this story is wrong. It's the streamable-HTTP path that changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  3rd break: field names went snake_case
&lt;/h2&gt;

&lt;p&gt;This is the quiet one. The Pydantic protocol models renamed their attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tool.inputSchema&lt;/code&gt; goes to &lt;code&gt;tool.input_schema&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;result.isError&lt;/code&gt; goes to &lt;code&gt;result.is_error&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;structuredContent&lt;/code&gt; goes to &lt;code&gt;structured_content&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nextCursor&lt;/code&gt; goes to &lt;code&gt;next_cursor&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The JSON on the wire is still camelCase (the aliases keep that), so servers and clients still agree on the protocol. It's your Python that breaks, with an AttributeError. Bonus trap: &lt;code&gt;model_dump()&lt;/code&gt; without &lt;code&gt;by_alias=True&lt;/code&gt; now hands you snake_case dicts without complaint, which can poison anything downstream that expects the protocol shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is actually broken right now
&lt;/h2&gt;

&lt;p&gt;Two examples I verified by downloading the packages rather than trusting a changelog. autogen-ext 0.7.5 declares &lt;code&gt;mcp&amp;gt;=1.11.0&lt;/code&gt; with no upper bound, imports &lt;code&gt;streamablehttp_client&lt;/code&gt;, unpacks the 3-tuple, and reads &lt;code&gt;inputSchema&lt;/code&gt; / &lt;code&gt;isError&lt;/code&gt;, so its &lt;code&gt;[mcp]&lt;/code&gt; extra is broken against current &lt;code&gt;mcp&lt;/code&gt;, and as of mid-September that's still the case on main. llama-index-tools-mcp 0.5.0 is the same failure with a twist: it declared &lt;code&gt;mcp&amp;gt;=2.0.0&lt;/code&gt; but still unpacked the 3-tuple, so it was broken against its own dependency floor. Fixed in 0.5.1 in late August; 0.6.0 is current.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pin that fixes it
&lt;/h2&gt;

&lt;p&gt;If you don't need 2.x features, pin back. The official 2.0.0 release notes tell library authors to keep a &lt;code&gt;&amp;lt;2&lt;/code&gt; upper bound, and the 1.x line is still getting security fixes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"mcp&amp;gt;=1.28,&amp;lt;2"&lt;/span&gt;     &lt;span class="c"&gt;# resolves to 1.30.0 today&lt;/span&gt;
uv add &lt;span class="s2"&gt;"mcp&amp;lt;2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the wrapper already fixed it (llama-index-tools-mcp &amp;gt;= 0.5.1, for example), upgrade the wrapper instead of downgrading &lt;code&gt;mcp&lt;/code&gt;. Read the release notes before you assume which side is fixed.&lt;/p&gt;

&lt;p&gt;Before guessing, check what you're actually running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import importlib.metadata as m, inspect; print(m.version('mcp')); import mcp.client.streamable_http as sh; print(hasattr(sh,'streamablehttp_client')); print(inspect.signature(sh.streamable_http_client))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And check the wrapper's &lt;code&gt;pyproject.toml&lt;/code&gt; for an unbounded &lt;code&gt;mcp&lt;/code&gt; dependency. That missing upper bound is the whole bug, and it'll happen again at the next major.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things that are not this bug
&lt;/h2&gt;

&lt;p&gt;I saw a couple of claims circulating while digging that can cost you hours if you chase them.&lt;/p&gt;

&lt;p&gt;An empty list from a tool producing zero content blocks is not a 2.x regression. I measured the same numbers on both 1.29.1 and 2.2.0: &lt;code&gt;[]&lt;/code&gt; gives 0 blocks, &lt;code&gt;''&lt;/code&gt; gives 1, &lt;code&gt;json.dumps([])&lt;/code&gt; gives 1, &lt;code&gt;['a','b']&lt;/code&gt; gives 2. It's long-standing FastMCP behavior in &lt;code&gt;_convert_to_content&lt;/code&gt;, which flattens a list item by item, and an empty list flattens to nothing. &lt;code&gt;structured_content&lt;/code&gt; is still correct. If your tool returns a list, return something wrapper-friendly instead of a bare &lt;code&gt;[]&lt;/code&gt; so the model gets one text block to read. That one matters if an agent keeps re-running your search instead of concluding "no match".&lt;/p&gt;

&lt;p&gt;Separately, 2.x removed automatic return-value wrapping in the low-level &lt;code&gt;Server&lt;/code&gt;, and &lt;code&gt;mcp.types&lt;/code&gt; moved out into a &lt;code&gt;mcp-types&lt;/code&gt; package. Both are in the migration guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;A dependency major plus an unbounded requirement in the wrapper equals silent breakage at install time, with a stack trace pointing at your code instead of the dependency. Pin it, then skim the migration guide at py.sdk.modelcontextprotocol.io/migration. It's a long read, but the naming section alone saves the afternoon.&lt;/p&gt;

&lt;p&gt;I run a pile of MCP servers locally for agent work, and pinning is the boring habit that keeps it quiet. If you want to see how I aggregate and hot-swap those servers, that's in my Smart-MCP-Proxy repo, but the pin is the only part you need today.&lt;/p&gt;

&lt;p&gt;And I could be wrong about your specific stack, so test the pin in a throwaway venv first. That's what I did before writing any of this down.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why your Python tool corrupts text on Windows, and why chcp 65001 doesn't fix it</title>
      <dc:creator>MilkyWay008</dc:creator>
      <pubDate>Sat, 12 Sep 2026 17:08:37 +0000</pubDate>
      <link>https://dev.to/milkyway008/why-your-python-tool-corrupts-text-on-windows-and-why-chcp-65001-doesnt-fix-it-4j2k</link>
      <guid>https://dev.to/milkyway008/why-your-python-tool-corrupts-text-on-windows-and-why-chcp-65001-doesnt-fix-it-4j2k</guid>
      <description>&lt;p&gt;I lost an afternoon to a config file last week. The graph name was right there in the JSON, spelled exactly the way the CLI wanted it, and the CLI kept saying the graph didn't exist.&lt;/p&gt;

&lt;p&gt;Turns out the file wasn't being read wrong...... it was being &lt;em&gt;decoded&lt;/em&gt; wrong. The name that came out the other end had a couple of extra characters in it, and nothing raised an error to tell me.&lt;/p&gt;

&lt;p&gt;Then the same class of bug turned up in an updater. It printed "Up to date" while two reader threads died with &lt;code&gt;charmap codec can't decode byte 0x81&lt;/code&gt;. The check thought everything was fine, which is the worst part of it.&lt;/p&gt;

&lt;p&gt;Both are one thing: Python's default text encoding on Windows is not UTF-8. Not for files, and not for subprocess pipes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Python actually uses by default
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;open("config.json")&lt;/code&gt; with no &lt;code&gt;encoding=&lt;/code&gt; argument decodes with whatever &lt;code&gt;locale.getpreferredencoding(False)&lt;/code&gt; returns. On Windows that's the ANSI code page. cp1252 on most en-US machines, cp936/GBK on zh-CN, cp932 on ja-JP, cp1251 on ru-RU.&lt;/p&gt;

&lt;p&gt;Pipes are the same story. &lt;code&gt;subprocess.run(cmd, text=True)&lt;/code&gt; with no &lt;code&gt;encoding=&lt;/code&gt; hands the pipe to &lt;code&gt;TextIOWrapper&lt;/code&gt;, which falls back to that same locale codec. So the parent process decodes the child's UTF-8 output with cp1252 and hopes for the best.&lt;/p&gt;

&lt;p&gt;If your files are UTF-8 (most config, most code, most modern tooling), you have a codec mismatch. What happens next depends on the bytes, and there are four flavors.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four ways it breaks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Mojibake, no exception
&lt;/h3&gt;

&lt;p&gt;I encoded &lt;code&gt;café—&lt;/code&gt; to UTF-8, which gives &lt;code&gt;63 61 66 c3 a9 e2 80 94&lt;/code&gt;, then decoded those bytes as cp1252 and got &lt;code&gt;cafÃ©â€”&lt;/code&gt;. cp1252 maps almost every byte to &lt;em&gt;something&lt;/em&gt;, so nothing raises. Your dict key is now &lt;code&gt;cafÃ©&lt;/code&gt; and the lookup for &lt;code&gt;café&lt;/code&gt; misses. That was my graph-not-found bug, and silent corruption is worse than a crash because you get no pointer to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A hard UnicodeDecodeError
&lt;/h3&gt;

&lt;p&gt;Same bytes, decoded as GBK: &lt;code&gt;'gbk' codec can't decode byte 0x94 in position 7: incomplete multibyte sequence&lt;/code&gt;. GBK wants a valid lead byte followed by a valid trail byte, and a UTF-8 sequence is neither.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The single-byte crash
&lt;/h3&gt;

&lt;p&gt;cp1252 defines no character for &lt;code&gt;0x81&lt;/code&gt;, &lt;code&gt;0x8D&lt;/code&gt;, &lt;code&gt;0x8F&lt;/code&gt;, &lt;code&gt;0x90&lt;/code&gt;, &lt;code&gt;0x9D&lt;/code&gt;. One of those lands in the stream and you get &lt;code&gt;'charmap' codec can't decode byte 0x81 in position 0: character maps to &amp;lt;undefined&amp;gt;&lt;/code&gt;. Small detail that explains why this shows up in normal files: &lt;code&gt;0x81&lt;/code&gt; is a UTF-8 &lt;em&gt;continuation&lt;/em&gt; byte, so it appears in the middle of ordinary characters (&lt;code&gt;U+2041&lt;/code&gt; is &lt;code&gt;E2 81 81&lt;/code&gt;). You don't need exotic text to hit it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. A text file gets called "binary"
&lt;/h3&gt;

&lt;p&gt;Plenty of tools sniff the first N bytes to guess whether a file is binary, and 1000 is a popular sample size. Cut a UTF-8 file at byte 1000 and you can split a multibyte character in half: &lt;code&gt;'utf-8' codec can't decode byte 0xe6 in position 999: unexpected end of data&lt;/code&gt;. The rest of the file decodes cleanly and there is no NUL byte anywhere, but the tool decides it's binary and refuses to display it. I've had that happen on a plain CJK text file.&lt;/p&gt;

&lt;h2&gt;
  
  
  chcp 65001 won't save you
&lt;/h2&gt;

&lt;p&gt;I tried it. Console code page was 437, I ran &lt;code&gt;chcp 65001&lt;/code&gt;, and &lt;code&gt;locale.getencoding()&lt;/code&gt; still returned cp1252. &lt;code&gt;getpreferredencoding(False)&lt;/code&gt; still returned cp1252. The &lt;code&gt;Popen(text=True)&lt;/code&gt; reader still handed me &lt;code&gt;cafÃ©&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;chcp&lt;/code&gt; sets the &lt;em&gt;console&lt;/em&gt; code page. Python's default text encoding comes from the &lt;em&gt;system ANSI&lt;/em&gt; code page (&lt;code&gt;GetACP&lt;/code&gt;), which is a different thing. And for pipes the console isn't involved at all...... there's no console anywhere in that path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix it in the source
&lt;/h2&gt;

&lt;p&gt;Pin the encoding at every point where bytes become text. That's the whole fix, and it's boring on purpose:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="c1"&gt;# text files: say what they are
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;README.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# child processes: text mode, an explicit encoding, and a forgiving handler
&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&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;log&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;--oneline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# if it's your own stdout that's misbehaving
&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reconfigure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&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;A couple of details that bite later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;encoding="locale"&lt;/code&gt; when the file really is in the platform encoding (3.10+). Don't guess.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;errors="replace"&lt;/code&gt; never raises, which is also why it can hide a real mismatch. Reach for &lt;code&gt;errors="surrogateescape"&lt;/code&gt; if you need to round-trip arbitrary bytes without losing any.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To find the sites in code you already have, run with &lt;code&gt;PYTHONWARNDEFAULTENCODING=1&lt;/code&gt; (or &lt;code&gt;python -X warn_default_encoding&lt;/code&gt;). Python 3.10+ emits an &lt;code&gt;EncodingWarning&lt;/code&gt; everywhere the default is being relied on. Run your tests once with it enabled, fix the hits, done. That's the PEP 597 answer to "how do I find these", and it beats grepping for &lt;code&gt;open(&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you can't patch the tool
&lt;/h2&gt;

&lt;p&gt;Third-party CLI, vendor binary, something you don't own: set &lt;code&gt;PYTHONUTF8=1&lt;/code&gt; in that process's environment. That's UTF-8 mode (PEP 540, available since 3.7). It makes &lt;code&gt;open()&lt;/code&gt; default to UTF-8, switches the filesystem encoding to UTF-8, and, the part that matters here, makes &lt;code&gt;locale.getpreferredencoding()&lt;/code&gt; return &lt;code&gt;utf-8&lt;/code&gt; so pipe decoding gets the right codec.&lt;/p&gt;

&lt;p&gt;Same script, both settings, on Windows 11 with Python 3.11:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PYTHONUTF8=0  getpreferredencoding: cp1252  |  Popen(text=True): 'cafÃ©â€”'
PYTHONUTF8=1  getpreferredencoding: utf-8   |  Popen(text=True): 'café—'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;locale.getencoding()&lt;/code&gt; stays cp1252 in both rows. UTF-8 mode doesn't change the machine's ANSI code page, it changes what Python asks for.&lt;/p&gt;

&lt;p&gt;Two honest caveats, because this is where people get hurt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UTF-8 mode is not a codec fix.&lt;/strong&gt; If the child process actually emits GBK bytes, you still fail. You just fail with utf-8 named in the traceback instead of cp1252. &lt;code&gt;errors="replace"&lt;/code&gt; is the crash-proof option. UTF-8 mode covers the common case, it doesn't make you right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I wouldn't set it machine-wide.&lt;/strong&gt; It changes the default for every Python process on the box and flips &lt;code&gt;os.fsencode&lt;/code&gt;/&lt;code&gt;os.fsdecode&lt;/code&gt; to UTF-8, so anything that legitimately exchanges ANSI or OEM encoded data with a Windows component can start producing mojibake. Per process, per venv, or per CI job is the sane scope. Also, &lt;code&gt;PYTHONIOENCODING&lt;/code&gt; does not cover pipes: I had it set to utf-8 and the pipe still decoded as cp1252.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For what it's worth, this is going away. PEP 686 makes UTF-8 the default in 3.15, and the same PEP warns that the change can surface &lt;code&gt;UnicodeError&lt;/code&gt; and mojibake in code that was leaning on the old behavior without saying so. Expect a wave of these reports when it lands.&lt;/p&gt;

&lt;h2&gt;
  
  
  The binary-sniff false positive
&lt;/h2&gt;

&lt;p&gt;Don't decide "binary" from a truncated sample. Check for a NUL byte (that's what git does in its 8000-byte scan) and let an incremental decoder deal with the edge, so you never split a character:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;codecs&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;looks_binary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sniff&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&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;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sniff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;dec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;codecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getincrementaldecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;dec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# a partial char at the edge simply isn't flushed
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;UnicodeDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns &lt;code&gt;False&lt;/code&gt; for UTF-8 text no matter where the boundary lands, CJK included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;I'm fairly sure this is the whole class of the bug, but I've only hit it on Windows, so your mileage may vary on a Linux box with a non-UTF-8 locale (same mechanism, much rarer default). If a tool prints a name that looks correct and still can't find it, hexdump the config and hexdump the value it's comparing against. The extra bytes will be sitting right there. Hope this saves somebody the afternoon it cost me.&lt;/p&gt;

&lt;p&gt;Where I ran into it: langchain-ai/langgraph#8665 (config reads on a CJK locale), NousResearch/hermes-agent#97322 (the cp1252 pipe reader thread), NousResearch/hermes-agent#86187 (valid UTF-8 flagged as binary).&lt;/p&gt;

</description>
      <category>ai</category>
      <category>windows</category>
      <category>tutorial</category>
      <category>python</category>
    </item>
    <item>
      <title>Two writers, one database: why your self-hosted app breaks the moment it runs twice</title>
      <dc:creator>MilkyWay008</dc:creator>
      <pubDate>Fri, 11 Sep 2026 17:09:24 +0000</pubDate>
      <link>https://dev.to/milkyway008/two-writers-one-database-why-your-self-hosted-app-breaks-the-moment-it-runs-twice-26eh</link>
      <guid>https://dev.to/milkyway008/two-writers-one-database-why-your-self-hosted-app-breaks-the-moment-it-runs-twice-26eh</guid>
      <description>&lt;p&gt;I keep running into the same failure in the self-hosted world, and it took me an embarrassingly long time to name it the first time round. The app works fine as one container. You add a second replica, or a second backend process, and everything goes strange: logins stop sticking, requests hang, or the logs fill up with &lt;code&gt;database is locked&lt;/code&gt;. Nothing crashes loudly. It just quietly stops making sense.&lt;/p&gt;

&lt;p&gt;Two separate mistakes tend to get made at once when you run a second copy of something that was built as one instance, and both of them look like somebody else's bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your old session dies, but a fresh one works
&lt;/h2&gt;

&lt;p&gt;This is the one that fools people. You log in, the page works, you click something and you get bounced back to the login screen. Open an incognito window, log in again, and it behaves (at least for a while).&lt;/p&gt;

&lt;p&gt;What's happening is that the instance that answered your login is not the instance answering your next request. If the app auto-generates a signing secret on first start, and Open WebUI calls this &lt;code&gt;WEBUI_SECRET_KEY&lt;/code&gt;, each copy invents its own. Instance A signs your session cookie, instance B tries to verify it with a different key, and the honest answer it gives you is 401. A fresh login works because it gets minted and used by whichever pod you landed on.&lt;/p&gt;

&lt;p&gt;On Kubernetes this sometimes arrives as an Envoy message instead of a clean 401: &lt;code&gt;upstream connect error or disconnect/reset before headers&lt;/code&gt;. That's transport-level noise sitting on top of a state-level problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or the logs say database is locked
&lt;/h2&gt;

&lt;p&gt;The other half of this is a file that only tolerates one writer. SQLite allows exactly one writer at a time. If a writer can't take the lock it returns &lt;code&gt;SQLITE_BUSY&lt;/code&gt; and the app reports &lt;code&gt;database is locked&lt;/code&gt;, unless a busy handler is configured to wait. Write-ahead logging lets readers and writers share on the same machine, but the wal-index lives in an mmapped &lt;code&gt;-shm&lt;/code&gt; file, so WAL does not work across machines, and the SQLite docs say it plainly: the WAL implementation will not work on a network filesystem. Rollback journals are no better, they use &lt;code&gt;fcntl()&lt;/code&gt; locks that are broken on plenty of NFS implementations.&lt;/p&gt;

&lt;p&gt;So the tempting fix, "put the &lt;code&gt;.db&lt;/code&gt; on a shared volume and scale the replicas", isn't a fix at all. It's a faster route to corrupt data.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, count your writers
&lt;/h2&gt;

&lt;p&gt;Before changing anything, prove how many copies are running and who owns the state file.&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;# processes and pods&lt;/span&gt;
pgrep &lt;span class="nt"&gt;-af&lt;/span&gt; &lt;span class="s2"&gt;"serve|gateway|uvicorn"&lt;/span&gt;
docker ps &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Names}}\t{{.Ports}}'&lt;/span&gt;
kubectl get pods &lt;span class="nt"&gt;-o&lt;/span&gt; wide

&lt;span class="c"&gt;# who holds the file and the port&lt;/span&gt;
lsof /path/to/webui.db          &lt;span class="c"&gt;# or: fuser -v /path/to/webui.db&lt;/span&gt;
ss &lt;span class="nt"&gt;-ltnp&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rows where you expected one is the whole diagnosis. Also look for stale PID or lock files in the app's data directory. A process that died badly leaves its lock behind, and the next start either refuses to boot or quietly becomes a second writer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, in order
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Get back to one writer first. On macOS actually quit the app (&lt;code&gt;Cmd+Q&lt;/code&gt;, not just closing the window), or run the app's own &lt;code&gt;gateway stop&lt;/code&gt;, or &lt;code&gt;kubectl scale deployment open-webui --replicas=1&lt;/code&gt;. Recovery before redesign.&lt;/li&gt;
&lt;li&gt;Give every replica the same secret. One &lt;code&gt;openssl rand -base64 32&lt;/code&gt; value, injected into all pods as &lt;code&gt;WEBUI_SECRET_KEY&lt;/code&gt; (and &lt;code&gt;OAUTH_SESSION_TOKEN_ENCRYPTION_KEY&lt;/code&gt; if you use OAuth). This is the step people skip, because everything boots fine without it.&lt;/li&gt;
&lt;li&gt;Move the database out of the container: &lt;code&gt;DATABASE_URL=postgresql://user:pass@db-host:5432/openwebui&lt;/code&gt;. One warning the docs are clear about, Open WebUI does not migrate your existing SQLite data into Postgres for you. Do this before you have production data, or plan the export.&lt;/li&gt;
&lt;li&gt;Externalize coordination as well: &lt;code&gt;REDIS_URL=redis://redis-host:6379/0&lt;/code&gt;, plus &lt;code&gt;WEBSOCKET_MANAGER=redis&lt;/code&gt; and &lt;code&gt;ENABLE_WEBSOCKET_SUPPORT=true&lt;/code&gt;. Without Redis, websocket handling and config sync stay in-process, and multi-instance users get 403s and intermittent auth weirdness that looks nothing like a database problem.&lt;/li&gt;
&lt;li&gt;Check the side databases too. The default ChromaDB vector store is SQLite-backed and not fork-safe, so vector search falling over on two replicas is the same disease in a different organ. Swapping to &lt;code&gt;VECTOR_DB=pgvector&lt;/code&gt; puts it in the Postgres you already have.&lt;/li&gt;
&lt;li&gt;Orchestrator hygiene: keep &lt;code&gt;UVICORN_WORKERS=1&lt;/code&gt; per container, and let exactly one replica run migrations (&lt;code&gt;ENABLE_DB_MIGRATIONS=false&lt;/code&gt; on the rest).&lt;/li&gt;
&lt;li&gt;Verify instead of hoping. Compare &lt;code&gt;kubectl exec &amp;lt;pod&amp;gt; -- printenv WEBUI_SECRET_KEY&lt;/code&gt; across both pods, then log in and roll the deployment while keeping that session alive. Watch &lt;code&gt;grep -c "database is locked"&lt;/code&gt; in the logs stop climbing.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Things that don't fix it
&lt;/h2&gt;

&lt;p&gt;A shared NFS or PVC volume holding the SQLite file, with two replicas on top, doesn't fix anything. The Open WebUI docs warn you'll get &lt;code&gt;database is locked&lt;/code&gt; and data corruption, and the SQLite WAL docs explain why the shared-memory file can't cross hosts anyway.&lt;/p&gt;

&lt;p&gt;Sticky sessions don't fix it either...... &lt;code&gt;sessionAffinity: ClientIP&lt;/code&gt; only hides the problem until a rollout moves your pod, and then everyone is logged out at once.&lt;/p&gt;

&lt;p&gt;Copying a live &lt;code&gt;.db&lt;/code&gt; around with &lt;code&gt;docker cp&lt;/code&gt; or plain &lt;code&gt;cp&lt;/code&gt; is worse than it looks. A copy taken mid-transaction is a mix of old and new pages, and if there's a &lt;code&gt;-wal&lt;/code&gt; or &lt;code&gt;-journal&lt;/code&gt; file sitting next to it, that has to travel with it or your copy is unusable. Use &lt;code&gt;VACUUM INTO&lt;/code&gt;, the backup API, or &lt;code&gt;sqlite3_rsync&lt;/code&gt; on 3.47 and newer.&lt;/p&gt;

&lt;p&gt;Raising the busy timeout buys you nothing. No pragma makes SQLite safe on network storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest caveats
&lt;/h2&gt;

&lt;p&gt;The Kubernetes issue I started from is still open, and the maintainers there haven't confirmed a root cause (fwiw, treat that particular attribution as community reasoning rather than gospel). The SQLite documentation is firm about network filesystems but never says the word Docker, so the bind-mount case is inference on my part, not a quote. And if your app ships a single-instance lease or lock, use it. The lease is the real fix, everything above is a workaround.&lt;/p&gt;

&lt;p&gt;I could be wrong about the specifics of your stack, but if you're seeing session weirdness that only shows up once a second copy is running, this is the first place I'd look. It cost me a couple of hours of staring at logs the first time.&lt;/p&gt;

&lt;p&gt;Sources worth reading: &lt;a href="https://www.sqlite.org/wal.html" rel="noopener noreferrer"&gt;SQLite WAL&lt;/a&gt;, &lt;a href="https://www.sqlite.org/faq.html" rel="noopener noreferrer"&gt;SQLite FAQ&lt;/a&gt;, &lt;a href="https://www.sqlite.org/lockingv3.html" rel="noopener noreferrer"&gt;locking&lt;/a&gt;, &lt;a href="https://www.sqlite.org/howtocorrupt.html" rel="noopener noreferrer"&gt;how to corrupt&lt;/a&gt;, and Open WebUI's &lt;a href="https://docs.openwebui.com/getting-started/advanced-topics/scaling/" rel="noopener noreferrer"&gt;scaling&lt;/a&gt; and &lt;a href="https://docs.openwebui.com/getting-started/advanced-topics/hardening/" rel="noopener noreferrer"&gt;hardening&lt;/a&gt; pages.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
