<?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: ThryLox</title>
    <description>The latest articles on DEV Community by ThryLox (@thrylox).</description>
    <link>https://dev.to/thrylox</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%2F4005948%2Fa6d2f699-1821-4464-af9b-453f59b781e8.png</url>
      <title>DEV Community: ThryLox</title>
      <link>https://dev.to/thrylox</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thrylox"/>
    <language>en</language>
    <item>
      <title>How I Debugged and Fixed Memory &amp; Goroutine Leaks in ProjectDiscovery Nuclei Engine 🚀</title>
      <dc:creator>ThryLox</dc:creator>
      <pubDate>Sun, 28 Jun 2026 01:20:58 +0000</pubDate>
      <link>https://dev.to/thrylox/how-i-debugged-and-fixed-memory-goroutine-leaks-in-projectdiscovery-nuclei-engine-4794</link>
      <guid>https://dev.to/thrylox/how-i-debugged-and-fixed-memory-goroutine-leaks-in-projectdiscovery-nuclei-engine-4794</guid>
      <description>&lt;p&gt;If you work in cloud security or vulnerability scanning, chances are high that you rely on &lt;strong&gt;ProjectDiscovery Nuclei&lt;/strong&gt;—the gold standard open-source vulnerability scanner powered by YAML templates.&lt;/p&gt;

&lt;p&gt;While Nuclei performs exceptionally well as a standalone CLI tool, embedding it as an underlying SDK engine inside long-running microservices or continuous scanning workers introduces unique architectural challenges: &lt;strong&gt;memory bloat&lt;/strong&gt; and &lt;strong&gt;goroutine leaks&lt;/strong&gt; over extended execution loops.&lt;/p&gt;

&lt;p&gt;Recently, I investigated and resolved these exact engine lifecycle leaks in &lt;strong&gt;&lt;a href="https://github.com/projectdiscovery/nuclei/issues/7503" rel="noopener noreferrer"&gt;Nuclei Issue #7503&lt;/a&gt;&lt;/strong&gt; and submitted &lt;strong&gt;&lt;a href="https://github.com/projectdiscovery/nuclei/pull/7508" rel="noopener noreferrer"&gt;Pull Request #7508&lt;/a&gt;&lt;/strong&gt;. Here is a breakdown of what I discovered under the hood and how I fixed it in Go.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 The Problem: Unbounded State &amp;amp; Orphaned Goroutines
&lt;/h2&gt;

&lt;p&gt;When embedding &lt;code&gt;NucleiEngine&lt;/code&gt; into a long-running application loop (where engines are instantiated and closed dynamically per scan target), I noticed that memory consumption climbed steadily over time, and orphaned goroutines remained active long after calling &lt;code&gt;engine.Close()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Upon profiling the engine lifecycle in Go, I identified three primary memory leaks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unbounded &lt;code&gt;sync.Map&lt;/code&gt; in HTTP-to-HTTPS Port Tracker:&lt;/strong&gt; The &lt;code&gt;HTTPToHTTPSPortTracker&lt;/code&gt; stored host port mapping states in an unbounded &lt;code&gt;sync.Map&lt;/code&gt;. Over thousands of target scans, this map grew infinitely without eviction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orphaned Per-Host Rate Limiter Goroutines:&lt;/strong&gt; Global protocol state maintained per-execution rate limit pools (&lt;code&gt;PerHostRateLimitPool&lt;/code&gt;). When an engine execution finished, worker background routines were not cleanly shut down or purged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cached Template Parsers:&lt;/strong&gt; Compiled template ASTs (&lt;code&gt;parsedTemplatesCache&lt;/code&gt; and &lt;code&gt;compiledTemplatesCache&lt;/code&gt;) retained parsed representations in memory between engine instances without an explicit cache purging mechanism during engine teardown.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🛠️ The Solution: Architecture &amp;amp; Code Fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Bounded Expirable LRU Caching
&lt;/h3&gt;

&lt;p&gt;Instead of holding unbounded host entries in a &lt;code&gt;sync.Map&lt;/code&gt;, I replaced the storage structure with an expirable LRU (Least Recently Used) cache configured with a strict capacity bound (4,096 entries) and a 24-hour TTL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Replacing unbounded sync.Map with bounded expirable LRU cache&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;HTTPToHTTPSPortTracker&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;expirable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LRU&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewHTTPToHTTPSPortTracker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;HTTPToHTTPSPortTracker&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;HTTPToHTTPSPortTracker&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;expirable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewLRU&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}](&lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hour&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This guarantees that host mappings automatically expire and memory remains strictly bounded regardless of how many millions of URLs are scanned.&lt;/p&gt;


&lt;h3&gt;
  
  
  2. Lifecycle Cleanup in &lt;code&gt;protocolstate.Close()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I updated the global protocol state tear-down procedure in &lt;code&gt;pkg/protocols/common/protocolstate/state.go&lt;/code&gt; to release rate limiter worker routines and purge trackers upon &lt;code&gt;Close()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executionID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;stateLock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;stateLock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&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;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;globalStateMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;executionID&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Release per-host rate limiters and background goroutines&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PerHostRateLimitPool&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PerHostRateLimitPool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c"&gt;// Purge HTTP to HTTPS tracker entries&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPToHTTPSPortTracker&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPToHTTPSPortTracker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Purge&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;globalStateMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;executionID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Engine Cache Purging Interface
&lt;/h3&gt;

&lt;p&gt;Finally, I added a thread-safe &lt;code&gt;Purge()&lt;/code&gt; method to the template parser struct and invoked interface type assertions during &lt;code&gt;NucleiEngine.Close()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Safely purge compiled template caches on engine close&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;NucleiEngine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;closeInternal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&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;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Purge&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;purger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executerOpts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Purge&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;purger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Purge&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ⚖️ Technical Trade-offs &amp;amp; Potential Criticisms
&lt;/h2&gt;

&lt;p&gt;When designing solutions for large open-source codebases, evaluating architectural trade-offs is essential:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fixed LRU Capacity vs. Configuration:&lt;/strong&gt; Setting a hardcoded 4,096 capacity works as a balanced default for standard worker memory limits. However, in enterprise environments scanning millions of domains concurrently, exposing this bound as a configurable parameter (&lt;code&gt;Options.HTTPToHTTPSCacheSize&lt;/code&gt;) would be a clean future addition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime Interface Assertion:&lt;/strong&gt; Using runtime type assertions (&lt;code&gt;interface{ Purge() }&lt;/code&gt;) keeps the codebase decoupled and preserves backward compatibility for third-party SDK consumers using custom parsers without breaking their implementations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory Reclamation vs. Re-parsing Overhead:&lt;/strong&gt; Purging compiled template caches on engine teardown prioritizes memory stability over template compilation caching across separate engine instances.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  🧪 Results &amp;amp; Verification
&lt;/h2&gt;

&lt;p&gt;I validated these fixes across Nuclei unit test packages (&lt;code&gt;httpclientpool&lt;/code&gt;, &lt;code&gt;protocolstate&lt;/code&gt;, &lt;code&gt;templates&lt;/code&gt;, and &lt;code&gt;lib&lt;/code&gt;), verifying &lt;code&gt;100%&lt;/code&gt; success with zero memory accumulation between consecutive engine shutdowns.&lt;/p&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/projectdiscovery/nuclei/pull/7508" rel="noopener noreferrer"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        fix(engine): resolve memory and goroutine leaks in embedded engine usage (#7503)
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#7508&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/ThryLox" rel="noopener noreferrer"&gt;
        &lt;img class="github-liquid-tag-img" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F83971708%3Fv%3D4" alt="ThryLox avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/ThryLox" rel="noopener noreferrer"&gt;ThryLox&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/projectdiscovery/nuclei/pull/7508" rel="noopener noreferrer"&gt;&lt;time&gt;Jun 27, 2026&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Summary&lt;/h3&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;Fixes #7503 by implementing the required leak-prevention cleanup mechanisms outlined in #7502 for long-running embedded engines.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Key Changes&lt;/h3&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Size-Bounded HTTP-to-HTTPS Tracker:&lt;/strong&gt; Replaced the unbounded &lt;code&gt;sync.Map&lt;/code&gt; in &lt;code&gt;HTTPToHTTPSPortTracker&lt;/code&gt; (&lt;code&gt;pkg/protocols/http/httpclientpool/http_to_https_tracker.go&lt;/code&gt;) with a size-bounded expirable LRU cache (4096 entries max, 24h TTL) and added &lt;code&gt;Purge()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-Host Rate Limiter Pool Cleanup:&lt;/strong&gt; Updated &lt;code&gt;protocolstate.Close()&lt;/code&gt; (&lt;code&gt;pkg/protocols/common/protocolstate/state.go&lt;/code&gt;) to release per-host rate-limit pool goroutines and purge the HTTP-to-HTTPS tracker on shutdown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Template Cache Purging:&lt;/strong&gt; Updated &lt;code&gt;NucleiEngine.Close()&lt;/code&gt; / &lt;code&gt;closeInternal()&lt;/code&gt; (&lt;code&gt;lib/sdk.go&lt;/code&gt;) and &lt;code&gt;Parser&lt;/code&gt; (&lt;code&gt;pkg/templates/parser.go&lt;/code&gt;) to purge parsed and compiled template caches on engine close.&lt;/li&gt;
&lt;/ol&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/projectdiscovery/nuclei/pull/7508" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/projectdiscovery/nuclei/issues/7503" rel="noopener noreferrer"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        Memory and goroutine leaks in long-running embedded engine usage
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#7503&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/apps/coderabbitai" rel="noopener noreferrer"&gt;
        &lt;img class="github-liquid-tag-img" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars.githubusercontent.com%2Fin%2F347564%3Fv%3D4" alt="coderabbitai[bot] avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/apps/coderabbitai" rel="noopener noreferrer"&gt;coderabbitai[bot]&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/projectdiscovery/nuclei/issues/7503" rel="noopener noreferrer"&gt;&lt;time&gt;Jun 24, 2026&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Summary&lt;/h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;The embedded engine can leak memory and goroutines over time during long-running usage.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Required changes&lt;/h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;Implement the leak-prevention work described in #7502:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;bound the &lt;code&gt;HTTPToHTTPS&lt;/code&gt; tracker with an LRU&lt;/li&gt;
&lt;li&gt;release the per-host rate-limit pool goroutines on close&lt;/li&gt;
&lt;li&gt;purge the template caches on engine close&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Rationale&lt;/h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;Without explicit cleanup and bounded caching, long-running embedders can accumulate memory usage and leave background goroutines running indefinitely.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Affected areas&lt;/h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;HTTPToHTTPS&lt;/code&gt; tracking / redirect bookkeeping&lt;/li&gt;
&lt;li&gt;per-host rate limit pool lifecycle and shutdown&lt;/li&gt;
&lt;li&gt;template cache lifecycle during engine close&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Acceptance criteria&lt;/h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;HTTPToHTTPS&lt;/code&gt; tracker is size-bounded and evicts old entries.&lt;/li&gt;
&lt;li&gt;Per-host rate-limit pool goroutines are released when the engine closes.&lt;/li&gt;
&lt;li&gt;Template caches are purged on engine close.&lt;/li&gt;
&lt;li&gt;Long-running embedded usage no longer shows continued growth from these resources.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Backlinks&lt;/h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Pull request: &lt;a href="https://github.com/projectdiscovery/nuclei/pull/7502" rel="noopener noreferrer"&gt;https://github.com/projectdiscovery/nuclei/pull/7502&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Request comment: &lt;a href="https://github.com/projectdiscovery/nuclei/pull/7502#issuecomment-4794470056" rel="noopener noreferrer"&gt;https://github.com/projectdiscovery/nuclei/pull/7502#issuecomment-4794470056&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Requested by: @Mzack9999&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Additional context&lt;/h2&gt;
&lt;span class="octicon octicon-link"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;PR title: fix leaks&lt;/p&gt;

    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/projectdiscovery/nuclei/issues/7503" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  💡 Key Takeaways for Go Developers
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Beware of Unbounded &lt;code&gt;sync.Map&lt;/code&gt; in Long-Running Apps:&lt;/strong&gt; While &lt;code&gt;sync.Map&lt;/code&gt; is convenient, it lacks eviction policies. Use LRU caches with TTLs for dynamic lookup tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit Teardown Interfaces:&lt;/strong&gt; When building Go SDKs meant to be embedded, always provide clean &lt;code&gt;Close()&lt;/code&gt; / &lt;code&gt;Purge()&lt;/code&gt; methods to release background channels and goroutines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decoupled Lifecycle Hooks:&lt;/strong&gt; Interface checks like &lt;code&gt;if purger, ok := obj.(interface{ Purge() }); ok&lt;/code&gt; enable clean resource cleanup without introducing rigid package dependencies.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://github.com/Thrylox" rel="noopener noreferrer"&gt;@Thrylox&lt;/a&gt;. Connect with me on GitHub!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>cybersecurity</category>
      <category>opensource</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
