<?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: w0i</title>
    <description>The latest articles on DEV Community by w0i (@gonnafaraway).</description>
    <link>https://dev.to/gonnafaraway</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%2F1556965%2Fc964013c-c960-4222-8628-40e33516a527.png</url>
      <title>DEV Community: w0i</title>
      <link>https://dev.to/gonnafaraway</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gonnafaraway"/>
    <language>en</language>
    <item>
      <title>Uncontrolled heap retention in Go: how to tell a leak from a healthy cache</title>
      <dc:creator>w0i</dc:creator>
      <pubDate>Sat, 10 Oct 2026 22:13:03 +0000</pubDate>
      <link>https://dev.to/gonnafaraway/uncontrolled-heap-retention-in-go-how-to-tell-a-leak-from-a-healthy-cache-1ii5</link>
      <guid>https://dev.to/gonnafaraway/uncontrolled-heap-retention-in-go-how-to-tell-a-leak-from-a-healthy-cache-1ii5</guid>
      <description>&lt;p&gt;Growing memory is not automatically a leak.&lt;/p&gt;

&lt;p&gt;In a real Go service, caches warm up, pools expand, buffers batch work, and connection managers hold state on purpose. If you only watch RSS or &lt;code&gt;alloc_space&lt;/code&gt;, everything looks suspicious. The useful question is sharper:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After GC, does &lt;strong&gt;retained&lt;/strong&gt; heap keep climbing without a ceiling — and &lt;strong&gt;which call stacks&lt;/strong&gt; are holding it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the problem &lt;a href="https://github.com/gonnafaraway/godrain" rel="noopener noreferrer"&gt;godrain&lt;/a&gt; is built for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Often intentional&lt;/th&gt;
&lt;th&gt;Looks uncontrolled&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Metric&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;inuse_*&lt;/code&gt; after GC&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shape&lt;/td&gt;
&lt;td&gt;rises, then plateaus&lt;/td&gt;
&lt;td&gt;sustained positive slope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;known cache/pool stacks&lt;/td&gt;
&lt;td&gt;not allowlisted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load&lt;/td&gt;
&lt;td&gt;grows with RPS / queue depth&lt;/td&gt;
&lt;td&gt;load is flat, heap still climbs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;alloc_space&lt;/code&gt; will grow on a busy service forever.&lt;br&gt;&lt;br&gt;
&lt;code&gt;inuse_space&lt;/code&gt; after a forced GC is much closer to "what is still alive".&lt;/p&gt;
&lt;h2&gt;
  
  
  A tiny demo
&lt;/h2&gt;

&lt;p&gt;godrain watches a live &lt;code&gt;net/http/pprof&lt;/code&gt; endpoint, samples heap with &lt;code&gt;gc=1&lt;/code&gt;, aggregates retention by stack, then classifies growth.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/gonnafaraway/godrain/cmd/godrain@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unbounded map growth:&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;# terminal 1&lt;/span&gt;
git clone https://github.com/gonnafaraway/godrain.git
&lt;span class="nb"&gt;cd &lt;/span&gt;godrain
go run ./testdata/leaky

&lt;span class="c"&gt;# terminal 2&lt;/span&gt;
godrain watch &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; http://127.0.0.1:6061 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--warmup&lt;/span&gt; 5s &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--duration&lt;/span&gt; 45s &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--interval&lt;/span&gt; 5s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see something like &lt;code&gt;suspect&lt;/code&gt; / &lt;code&gt;leak&lt;/code&gt; attributed to the goroutine stuffing a global map.&lt;/p&gt;

&lt;p&gt;Now the opposite case — a bounded LRU that should plateau:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go run ./testdata/cached
godrain watch &lt;span class="nt"&gt;--url&lt;/span&gt; http://127.0.0.1:6062 &lt;span class="nt"&gt;--warmup&lt;/span&gt; 5s &lt;span class="nt"&gt;--duration&lt;/span&gt; 45s &lt;span class="nt"&gt;--interval&lt;/span&gt; 5s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same sampling pipeline. Different shape. Different verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the classifier works
&lt;/h2&gt;

&lt;p&gt;Rough pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sample &lt;code&gt;/debug/pprof/heap?gc=1&lt;/code&gt; on an interval&lt;/li&gt;
&lt;li&gt;Aggregate &lt;code&gt;inuse_space&lt;/code&gt; by call stack&lt;/li&gt;
&lt;li&gt;Drop a warmup window (cache fill)&lt;/li&gt;
&lt;li&gt;Score &lt;strong&gt;slope&lt;/strong&gt;, &lt;strong&gt;plateau&lt;/strong&gt;, optional &lt;strong&gt;allowlist&lt;/strong&gt;, optional &lt;strong&gt;load correlation&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Emit &lt;code&gt;ok&lt;/code&gt; / &lt;code&gt;expected&lt;/code&gt; / &lt;code&gt;suspect&lt;/code&gt; / &lt;code&gt;leak&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service (pprof)
  → heap snapshots with gc=1
  → inuse by stack
  → drop warmup
  → slope / plateau / allowlist / load
  → verdict + top stacks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Allowlists matter
&lt;/h3&gt;

&lt;p&gt;Without an allowlist, a growing cache during warm-up looks guilty. After the first report, suppress known intentional allocators:&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;patterns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;internal/cache"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lru."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Load correlation helps even more
&lt;/h3&gt;

&lt;p&gt;If heap grows with RPS, that may be expected. If RPS is flat and retained heap still climbs, that is much more interesting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;godrain watch &lt;span class="nt"&gt;--url&lt;/span&gt; http://127.0.0.1:6060 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--load-url&lt;/span&gt; http://127.0.0.1:9090/metrics &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--load-metric&lt;/span&gt; http_requests_total &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--warmup&lt;/span&gt; 1m &lt;span class="nt"&gt;--duration&lt;/span&gt; 5m &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using it on a real service
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Expose pprof &lt;strong&gt;privately&lt;/strong&gt; (&lt;code&gt;127.0.0.1&lt;/code&gt; / internal network only)&lt;/li&gt;
&lt;li&gt;Warm the process under realistic traffic&lt;/li&gt;
&lt;li&gt;Hold roughly constant load during the watch window&lt;/li&gt;
&lt;li&gt;Run:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;godrain watch &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; http://127.0.0.1:6060 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--warmup&lt;/span&gt; 1m &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--duration&lt;/span&gt; 5m &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--interval&lt;/span&gt; 15s &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the signal is messy, split surfaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API only&lt;/li&gt;
&lt;li&gt;workers only&lt;/li&gt;
&lt;li&gt;both together&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That usually shows which contour is retaining memory.&lt;/p&gt;

&lt;p&gt;For CI, non-zero exit on &lt;code&gt;suspect&lt;/code&gt;/&lt;code&gt;leak&lt;/code&gt; is intentional. Use &lt;code&gt;--no-exit-code&lt;/code&gt; if you only want the report.&lt;/p&gt;

&lt;h2&gt;
  
  
  What godrain is not
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;not a replacement for interactive &lt;code&gt;go tool pprof&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;not a static analyzer&lt;/li&gt;
&lt;li&gt;not a proof of a leak — it is a heuristic&lt;/li&gt;
&lt;li&gt;not a reason to expose &lt;code&gt;/debug/pprof&lt;/code&gt; to the internet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat pprof as sensitive. Saved heap profiles can leak internals too.&lt;/p&gt;

&lt;h2&gt;
  
  
  False positives / false negatives
&lt;/h2&gt;

&lt;p&gt;You may get &lt;strong&gt;false &lt;code&gt;suspect&lt;/code&gt;&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the window is shorter than cache fill&lt;/li&gt;
&lt;li&gt;load is ramping and you did not pass &lt;code&gt;--load-url&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;intentional growth is not allowlisted yet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You may get &lt;strong&gt;false &lt;code&gt;ok&lt;/code&gt;&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the leak is slower than your thresholds&lt;/li&gt;
&lt;li&gt;the leaking path was not exercised during the watch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Longer soaks beat clever guesses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;I got tired of the loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"memory is up"&lt;/li&gt;
&lt;li&gt;open one heap profile&lt;/li&gt;
&lt;li&gt;argue whether the cache is "supposed to do that"&lt;/li&gt;
&lt;li&gt;repeat tomorrow&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;godrain automates the boring part: &lt;strong&gt;compare retained growth over time under steady load, then point at stacks&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/gonnafaraway/godrain" rel="noopener noreferrer"&gt;github.com/gonnafaraway/godrain&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Release: &lt;a href="https://github.com/gonnafaraway/godrain/releases/tag/v0.1.0" rel="noopener noreferrer"&gt;v0.1.0&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it on a staging service, I especially want reports of false positives — that feedback is more valuable than stars.&lt;/p&gt;

</description>
      <category>go</category>
      <category>debugging</category>
      <category>observability</category>
      <category>opensource</category>
    </item>
    <item>
      <title>go-schemathesis v1.0.0: property-based tests from an OpenAPI spec, as one Go binary</title>
      <dc:creator>w0i</dc:creator>
      <pubDate>Sat, 10 Oct 2026 17:33:40 +0000</pubDate>
      <link>https://dev.to/gonnafaraway/go-schemathesis-v100-property-based-tests-from-an-openapi-spec-as-one-go-binary-28kk</link>
      <guid>https://dev.to/gonnafaraway/go-schemathesis-v100-property-based-tests-from-an-openapi-spec-as-one-go-binary-28kk</guid>
      <description>&lt;p&gt;I tagged v1.0.0 of go-schemathesis: a CLI that loads an OpenAPI 3.x document, generates HTTP cases from the schema, runs them against a live API, and checks the responses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/gonnafaraway/go-schemathesis" rel="noopener noreferrer"&gt;https://github.com/gonnafaraway/go-schemathesis&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The approach is the one Schemathesis uses: the spec is the source of cases, not a hand-written fixture list. This build is a static Go binary (kin-openapi and cobra, no cgo). I wanted that check in a Go CI job without a Python environment.&lt;/p&gt;

&lt;p&gt;A run has three phases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;examples sends example / examples already written in the spec&lt;/li&gt;
&lt;li&gt;coverage walks declared bounds (minLength, maxLength, minimum, maximum, minItems, maxItems, enum, both booleans, omitted required body)&lt;/li&gt;
&lt;li&gt;fuzzing draws random cases from a seeded RNG (--seed replays the same draw)&lt;/li&gt;
&lt;li&gt;--mode is positive, negative, or all (the default). Six checks: 5xx, undocumented status, Content-Type, response schema, acceptance of valid data, rejection of invalid data. Each failure prints a shell-quoted curl.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/gonnafaraway/go-schemathesis/cmd/schemathesis@latest
schemathesis run openapi.yaml &lt;span class="nt"&gt;--url&lt;/span&gt; http://127.0.0.1:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--phases&lt;/span&gt; examples,coverage &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mode&lt;/span&gt; positive &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
The v1.0.0 release has binaries &lt;span class="k"&gt;for &lt;/span&gt;Linux, macOS, and Windows &lt;span class="o"&gt;(&lt;/span&gt;amd64 and arm64&lt;span class="o"&gt;)&lt;/span&gt; plus SHA256SUMS.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What a green run actually covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAPI 3.0 and 3.1. Swagger 2.0 is out.&lt;/li&gt;
&lt;li&gt;Response schema checks cover type, enum, required, properties, items, length and numeric bounds, and nullable. They do not cover pattern, format, additionalProperties, const, or allOf / anyOf / oneOf / not.&lt;/li&gt;
&lt;li&gt;Cases are independent. A created id is not reused on the next request. Real ids belong in spec examples; the tool does not read your database.&lt;/li&gt;
&lt;li&gt;Transport failures show up as Errors: and do not change the exit code. An unreachable host can exit 0. In CI, assert Errors: 0 on the summary line.&lt;/li&gt;
&lt;li&gt;Report is console text. No JUnit, JSON, or SARIF yet.&lt;/li&gt;
&lt;li&gt;The binary is named schemathesis. If the Python CLI is also on PATH, install this one to an explicit directory.&lt;/li&gt;
&lt;li&gt;negative and fuzzing send schema-violating input. Point them only at an API you are allowed to test, and confirm the target with --phases examples --mode positive first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Useful feedback is a minimal spec where a check was wrong or stayed silent. The repo has a bug-report template; tokens should be redacted.&lt;/p&gt;

&lt;p&gt;Same author, separate tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/gonnafaraway/go-arch-template" rel="noopener noreferrer"&gt;go-arch-template&lt;/a&gt; — Go service skeleton: Clean Architecture, HTTP and gRPC, Postgres and Mongo, generated OpenAPI, Zap / OpenTelemetry / Prometheus.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/gonnafaraway/archgen" rel="noopener noreferrer"&gt;archgen&lt;/a&gt; — Markdown architecture notes, plus optional C4 files, generated from // archgen: comments in Go.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/gonnafaraway/kaiban" rel="noopener noreferrer"&gt;kaiban&lt;/a&gt; — local Kanban where each column is an LLM role (product through QA). A card moves forward only after a person approves the column report.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>go</category>
      <category>openapi</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
