{"article":{"slug":"go-1-28-http-2-parking-measuring-memory-savings","title":"Go 1.28 HTTP/2 Parking: Measuring Memory Savings","subtitle":null,"summary":"Mohammad Abdolirad benchmarks the HTTP/2 connection parking change in Go 1.28 with 5,000 idle connections, finding the same server binary used 85 to 93 MiB less RSS with parking enabled, with fewer goroutines and less stack memory, and discusses why HeapAlloc told a less simple story and the limits of the test.","content_type":"research","language":"en","canonical_url":"https://abdolirad.me/articles/go-1-28-http2-parking-memory/","author":{"name":"Mohammad Abdolirad","url":"https://abdolirad.me/","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Mohammad Abdolirad","url":"https://abdolirad.me/","listing_slug":null,"listing":null},"topics":[{"name":"performance","slug":"performance","url":"https://listedarticles.com/topics/performance"},{"name":"benchmarks","slug":"benchmarks","url":"https://listedarticles.com/topics/benchmarks"},{"name":"programming-languages","slug":"programming-languages","url":"https://listedarticles.com/topics/programming-languages"},{"name":"networking","slug":"networking","url":"https://listedarticles.com/topics/networking"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":1204,"reading_minutes":5,"published_at":"2026-10-23T23:22:00.000Z","added_at":"2026-10-30T20:11:04.147Z","updated_at":"2026-10-30T20:11:04.147Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/go-1-28-http-2-parking-measuring-memory-savings","markdown_url":"https://listedarticles.com/articles/go-1-28-http-2-parking-measuring-memory-savings.md","example":false,"citation":"Mohammad Abdolirad, Mohammad Abdolirad. \"Go 1.28 HTTP/2 Parking: Measuring Memory Savings.\" 23 Oct 2026. https://abdolirad.me/articles/go-1-28-http2-parking-memory/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://abdolirad.me/articles/go-1-28-http2-parking-memory/"},"body_markdown":"While reading the draft Go 1.28 release notes, I noticed a change in the HTTP/2 server. With this change, the goroutine responsible for serving a connection can exit when there is no work to do, instead of waiting. A new goroutine takes over when needed. At first, it seemed like a small change, but I wondered how much memory it could save on a server with thousands of open connections.\n\nI decided to measure it rather than guess. I wanted to see how it affected process memory usage, goroutine counts, and Go runtime memory metrics.\n\n## What is HTTP/2 parking?[#](#what-is-http2-parking)\n\nAn HTTP/2 connection can stay open even when it has no active streams. In the older server behavior, a goroutine runs the connection’s `serveLoop` and waits there when nothing new is ready. The Go 1.28 development code adds an internal mechanism called *parking*. When `serveLoop` has no work ready to process, and the implementation’s safety conditions are met, that goroutine can exit. When new work arrives, a new goroutine runs `serveLoop` again.\n\nParking is not limited to connections with no active streams. A handler can still be running while `serveLoop` has nothing immediate to process. If those safety conditions are met, the serving goroutine can be parked. That does not mean every active connection, or every running handler, can be parked.\n\nThe word *parking* comes from the Go implementation, including names such as `tryPark` and the temporary `GODEBUG` setting `http2serveparking`. It is not a public API, and the draft release notes do not use this name for the change. The change is how the server manages the goroutine for a connection, not how application code handles requests.\n\nThis also gave me a useful way to test the change. I could run the same Go 1.28 server binary with parking enabled and disabled, instead of relying only on a Go 1.27 versus Go 1.28 comparison.\n\n## How I tested it[#](#how-i-tested-it)\n\nI used the same benchmark code and machine for three configurations:\n\n1. **Go 1.27.2**, as an older-version reference.\n2. **Go 1.28 development build, parking enabled**, as the default behavior of the tested revision.\n3. **The same Go 1.28 binary, parking disabled**, using `GODEBUG=http2serveparking=0`.\n\nThe tested Go 1.28 revision was `5c51011e82ed754af072baaf4338ff35e76fe705`. The benchmark source was pinned to commit `86753bd4d1f79621a4f3c6927c8239419e54bde2`.\n\nFor each configuration, I measured the server with **0, 1,000, and 5,000 idle HTTP/2 connections**. Each complete run used ten rounds for every case and connection count. I repeated the full experiment three times on the same Linux machine. The benchmark collected short sample windows after the connections had settled, and the published results use medians rather than a single sample.\n\nThe test machine used an Intel Core i7-1265U, Linux 6.18.56, and about 32 GiB of RAM. The benchmark configuration set `GOMAXPROCS=12` and `GOGC=100`. I kept the natural garbage collection behavior rather than forcing a collection before each measurement.\n\nI looked at four metrics. **RSS** measures resident memory for the server process as reported by Linux. **Goroutines** shows how many goroutines the Go runtime reports. **StackInuse** describes memory in use by goroutine stacks. **HeapAlloc** is the amount of allocated heap memory reported by the runtime. These values are related, but they do not measure the same thing.\n\n## RSS: the clearest change[#](#rss-the-clearest-change)\n\nThe largest difference appeared at 5,000 idle connections. The table below shows median RSS from each complete run, in MiB:\n\n| Run | Go 1.27.2 | Go 1.28, parking off | Go 1.28, parking on | Saved by parking |\n| --- | --- | --- | --- | --- |\n| 1 | 249.89 | 218.28 | 129.19 | 89.08 |\n| 2 | 247.18 | 223.73 | 131.11 | 92.62 |\n| 3 | 245.60 | 217.94 | 133.25 | 84.70 |\n\nCompared with the **same Go 1.28 binary with parking disabled**, enabling parking reduced RSS by **84.7 to 92.6 MiB**, or about **38.9% to 41.4%**, across the three runs. At 1,000 idle connections, the reduction was smaller but still clear, at about **15.7 to 16.6 MiB**.\n\n![Median server RSS at different numbers of idle HTTP/2 connections](https://abdolirad.me/articles/go-1-28-http2-parking-memory/01-rss-scaling.svg)\n\nThe zero-connection baseline stayed close to 8.5 to 8.7 MiB across the configurations. I also calculated baseline-adjusted RSS by subtracting each configuration’s zero-connection median. That helps show the growth associated with idle connections, but the main comparison above uses the observed process RSS.\n\n![RSS reduction with parking enabled](https://abdolirad.me/articles/go-1-28-http2-parking-memory/02-rss-savings.svg)\n\n## Fewer goroutines, less stack memory[#](#fewer-goroutines-less-stack-memory)\n\nThe goroutine counts helped explain the shape of the result. With 5,000 idle connections, Go 1.28 reported about **10,007 goroutines with parking disabled** and **5,007 with parking enabled**. That is a difference of exactly **5,000 goroutines** in this benchmark. At 1,000 idle connections, the difference was 1,000 goroutines.\n\n![Goroutine counts by idle connection count](https://abdolirad.me/articles/go-1-28-http2-parking-memory/03-goroutines.svg)\n\nThe stack numbers moved in the same direction. At 5,000 connections, **StackInuse** was about **99 MiB with parking disabled** and about **21 MiB with parking enabled**. The reduction was close to **78 MiB** in all three complete runs.\n\n![Stack and heap memory at 5,000 idle connections](https://abdolirad.me/articles/go-1-28-http2-parking-memory/04-memory-breakdown.svg)\n\nThis does not mean that every byte saved in RSS came from stacks. RSS includes more than Go stacks, and changes in heap allocation, runtime state, and the operating system can affect it. It does show that the lower goroutine and stack counts are consistent with the parking mechanism.\n\n## HeapAlloc did not tell the same simple story[#](#heapalloc-did-not-tell-the-same-simple-story)\n\nHeapAlloc was less consistent. In some runs, parking enabled used less heap than parking disabled. In another run, it used slightly more. This is a good reminder that one memory metric cannot stand in for another.\n\nFor this experiment, **RSS, goroutine count, and StackInuse** provide a clearer picture of the difference. I would not describe the change as a fixed heap saving or claim that a certain percentage of total memory always disappears.\n\n## Limits of this benchmark[#](#limits-of-this-benchmark)\n\nThese measurements come from one machine, one benchmark design, and a specific **development revision** of Go 1.28. They are not results from a final Go 1.28 release. I tested mostly idle connections, not a mix of real traffic, active streams, and application workloads.\n\nThe benchmark kept the Go 1.28 server binary the same when switching parking on and off. That makes the parking comparison more useful, but it still does not remove all system noise. The three full runs show a repeatable direction and a range, not a universal guarantee.\n\nThe `GODEBUG=http2serveparking=0` setting I used for this comparison is temporary and undocumented. I would not treat it as a stable option for future releases.\n\nI have kept the [benchmark source at a fixed commit](https://github.com/atkrad/benchmarks/tree/86753bd4d1f79621a4f3c6927c8239419e54bde2) and the [tested Go revision](https://github.com/golang/go/commit/5c51011e82ed754af072baaf4338ff35e76fe705) linked here so the details can be checked later.\n\n## Conclusion[#](#conclusion)\n\nIn this benchmark, HTTP/2 parking made a clear difference in memory usage. With 5,000 idle connections, the same Go 1.28 server binary used around 85 to 93 MiB less RSS when parking was enabled. It also had 5,000 fewer goroutines and used about 78 MiB less stack memory.\n\nWhat I found most interesting was how much memory an otherwise quiet connection can keep around. Changing what the server does while it waits can matter as much as optimizing the work it performs.\n\nI would not apply these numbers to every Go service. Still, for servers that keep many idle HTTP/2 connections open, this is a change worth understanding and measuring with a real workload.\n","body_html":"<p>While reading the draft Go 1.28 release notes, I noticed a change in the HTTP/2 server. With this change, the goroutine responsible for serving a connection can exit when there is no work to do, instead of waiting. A new goroutine takes over when needed. At first, it seemed like a small change, but I wondered how much memory it could save on a server with thousands of open connections.</p>\n<p>I decided to measure it rather than guess. I wanted to see how it affected process memory usage, goroutine counts, and Go runtime memory metrics.</p>\n<h2 id=\"what-is-http-2-parking\">What is HTTP/2 parking?<a href=\"#what-is-http2-parking\">#</a></h2>\n<p>An HTTP/2 connection can stay open even when it has no active streams. In the older server behavior, a goroutine runs the connection’s <code>serveLoop</code> and waits there when nothing new is ready. The Go 1.28 development code adds an internal mechanism called <em>parking</em>. When <code>serveLoop</code> has no work ready to process, and the implementation’s safety conditions are met, that goroutine can exit. When new work arrives, a new goroutine runs <code>serveLoop</code> again.</p>\n<p>Parking is not limited to connections with no active streams. A handler can still be running while <code>serveLoop</code> has nothing immediate to process. If those safety conditions are met, the serving goroutine can be parked. That does not mean every active connection, or every running handler, can be parked.</p>\n<p>The word <em>parking</em> comes from the Go implementation, including names such as <code>tryPark</code> and the temporary <code>GODEBUG</code> setting <code>http2serveparking</code>. It is not a public API, and the draft release notes do not use this name for the change. The change is how the server manages the goroutine for a connection, not how application code handles requests.</p>\n<p>This also gave me a useful way to test the change. I could run the same Go 1.28 server binary with parking enabled and disabled, instead of relying only on a Go 1.27 versus Go 1.28 comparison.</p>\n<h2 id=\"how-i-tested-it\">How I tested it<a href=\"#how-i-tested-it\">#</a></h2>\n<p>I used the same benchmark code and machine for three configurations:</p>\n<ol><li><strong>Go 1.27.2</strong>, as an older-version reference.</li><li><strong>Go 1.28 development build, parking enabled</strong>, as the default behavior of the tested revision.</li><li><strong>The same Go 1.28 binary, parking disabled</strong>, using <code>GODEBUG=http2serveparking=0</code>.</li></ol>\n<p>The tested Go 1.28 revision was <code>5c51011e82ed754af072baaf4338ff35e76fe705</code>. The benchmark source was pinned to commit <code>86753bd4d1f79621a4f3c6927c8239419e54bde2</code>.</p>\n<p>For each configuration, I measured the server with <strong>0, 1,000, and 5,000 idle HTTP/2 connections</strong>. Each complete run used ten rounds for every case and connection count. I repeated the full experiment three times on the same Linux machine. The benchmark collected short sample windows after the connections had settled, and the published results use medians rather than a single sample.</p>\n<p>The test machine used an Intel Core i7-1265U, Linux 6.18.56, and about 32 GiB of RAM. The benchmark configuration set <code>GOMAXPROCS=12</code> and <code>GOGC=100</code>. I kept the natural garbage collection behavior rather than forcing a collection before each measurement.</p>\n<p>I looked at four metrics. <strong>RSS</strong> measures resident memory for the server process as reported by Linux. <strong>Goroutines</strong> shows how many goroutines the Go runtime reports. <strong>StackInuse</strong> describes memory in use by goroutine stacks. <strong>HeapAlloc</strong> is the amount of allocated heap memory reported by the runtime. These values are related, but they do not measure the same thing.</p>\n<h2 id=\"rss-the-clearest-change\">RSS: the clearest change<a href=\"#rss-the-clearest-change\">#</a></h2>\n<p>The largest difference appeared at 5,000 idle connections. The table below shows median RSS from each complete run, in MiB:</p>\n<div class=\"table-wrap\"><table><thead><tr><th>Run</th><th>Go 1.27.2</th><th>Go 1.28, parking off</th><th>Go 1.28, parking on</th><th>Saved by parking</th></tr></thead><tbody><tr><td>1</td><td>249.89</td><td>218.28</td><td>129.19</td><td>89.08</td></tr><tr><td>2</td><td>247.18</td><td>223.73</td><td>131.11</td><td>92.62</td></tr><tr><td>3</td><td>245.60</td><td>217.94</td><td>133.25</td><td>84.70</td></tr></tbody></table></div>\n<p>Compared with the <strong>same Go 1.28 binary with parking disabled</strong>, enabling parking reduced RSS by <strong>84.7 to 92.6 MiB</strong>, or about <strong>38.9% to 41.4%</strong>, across the three runs. At 1,000 idle connections, the reduction was smaller but still clear, at about <strong>15.7 to 16.6 MiB</strong>.</p>\n<figure><img src=\"https://abdolirad.me/articles/go-1-28-http2-parking-memory/01-rss-scaling.svg\" alt=\"Median server RSS at different numbers of idle HTTP/2 connections\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>The zero-connection baseline stayed close to 8.5 to 8.7 MiB across the configurations. I also calculated baseline-adjusted RSS by subtracting each configuration’s zero-connection median. That helps show the growth associated with idle connections, but the main comparison above uses the observed process RSS.</p>\n<figure><img src=\"https://abdolirad.me/articles/go-1-28-http2-parking-memory/02-rss-savings.svg\" alt=\"RSS reduction with parking enabled\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<h2 id=\"fewer-goroutines-less-stack-memory\">Fewer goroutines, less stack memory<a href=\"#fewer-goroutines-less-stack-memory\">#</a></h2>\n<p>The goroutine counts helped explain the shape of the result. With 5,000 idle connections, Go 1.28 reported about <strong>10,007 goroutines with parking disabled</strong> and <strong>5,007 with parking enabled</strong>. That is a difference of exactly <strong>5,000 goroutines</strong> in this benchmark. At 1,000 idle connections, the difference was 1,000 goroutines.</p>\n<figure><img src=\"https://abdolirad.me/articles/go-1-28-http2-parking-memory/03-goroutines.svg\" alt=\"Goroutine counts by idle connection count\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>The stack numbers moved in the same direction. At 5,000 connections, <strong>StackInuse</strong> was about <strong>99 MiB with parking disabled</strong> and about <strong>21 MiB with parking enabled</strong>. The reduction was close to <strong>78 MiB</strong> in all three complete runs.</p>\n<figure><img src=\"https://abdolirad.me/articles/go-1-28-http2-parking-memory/04-memory-breakdown.svg\" alt=\"Stack and heap memory at 5,000 idle connections\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>This does not mean that every byte saved in RSS came from stacks. RSS includes more than Go stacks, and changes in heap allocation, runtime state, and the operating system can affect it. It does show that the lower goroutine and stack counts are consistent with the parking mechanism.</p>\n<h2 id=\"heapalloc-did-not-tell-the-same-simple-story\">HeapAlloc did not tell the same simple story<a href=\"#heapalloc-did-not-tell-the-same-simple-story\">#</a></h2>\n<p>HeapAlloc was less consistent. In some runs, parking enabled used less heap than parking disabled. In another run, it used slightly more. This is a good reminder that one memory metric cannot stand in for another.</p>\n<p>For this experiment, <strong>RSS, goroutine count, and StackInuse</strong> provide a clearer picture of the difference. I would not describe the change as a fixed heap saving or claim that a certain percentage of total memory always disappears.</p>\n<h2 id=\"limits-of-this-benchmark\">Limits of this benchmark<a href=\"#limits-of-this-benchmark\">#</a></h2>\n<p>These measurements come from one machine, one benchmark design, and a specific <strong>development revision</strong> of Go 1.28. They are not results from a final Go 1.28 release. I tested mostly idle connections, not a mix of real traffic, active streams, and application workloads.</p>\n<p>The benchmark kept the Go 1.28 server binary the same when switching parking on and off. That makes the parking comparison more useful, but it still does not remove all system noise. The three full runs show a repeatable direction and a range, not a universal guarantee.</p>\n<p>The <code>GODEBUG=http2serveparking=0</code> setting I used for this comparison is temporary and undocumented. I would not treat it as a stable option for future releases.</p>\n<p>I have kept the <a href=\"https://github.com/atkrad/benchmarks/tree/86753bd4d1f79621a4f3c6927c8239419e54bde2\" rel=\"nofollow ugc noopener\">benchmark source at a fixed commit</a> and the <a href=\"https://github.com/golang/go/commit/5c51011e82ed754af072baaf4338ff35e76fe705\" rel=\"nofollow ugc noopener\">tested Go revision</a> linked here so the details can be checked later.</p>\n<h2 id=\"conclusion\">Conclusion<a href=\"#conclusion\">#</a></h2>\n<p>In this benchmark, HTTP/2 parking made a clear difference in memory usage. With 5,000 idle connections, the same Go 1.28 server binary used around 85 to 93 MiB less RSS when parking was enabled. It also had 5,000 fewer goroutines and used about 78 MiB less stack memory.</p>\n<p>What I found most interesting was how much memory an otherwise quiet connection can keep around. Changing what the server does while it waits can matter as much as optimizing the work it performs.</p>\n<p>I would not apply these numbers to every Go service. Still, for servers that keep many idle HTTP/2 connections open, this is a change worth understanding and measuring with a real workload.</p>","headings":[{"level":2,"text":"What is HTTP/2 parking?#","id":"what-is-http-2-parking"},{"level":2,"text":"How I tested it#","id":"how-i-tested-it"},{"level":2,"text":"RSS: the clearest change#","id":"rss-the-clearest-change"},{"level":2,"text":"Fewer goroutines, less stack memory#","id":"fewer-goroutines-less-stack-memory"},{"level":2,"text":"HeapAlloc did not tell the same simple story#","id":"heapalloc-did-not-tell-the-same-simple-story"},{"level":2,"text":"Limits of this benchmark#","id":"limits-of-this-benchmark"},{"level":2,"text":"Conclusion#","id":"conclusion"}]}}