<?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: GAGAN R</title>
    <description>The latest articles on DEV Community by GAGAN R (@gaganr80).</description>
    <link>https://dev.to/gaganr80</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%2F4158313%2F03f656b4-13ef-4ea3-a492-c717a3e2dd5c.png</url>
      <title>DEV Community: GAGAN R</title>
      <link>https://dev.to/gaganr80</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gaganr80"/>
    <language>en</language>
    <item>
      <title>Why your NestJS Temporal worker ignores SIGTERM</title>
      <dc:creator>GAGAN R</dc:creator>
      <pubDate>Fri, 02 Oct 2026 18:34:59 +0000</pubDate>
      <link>https://dev.to/gaganr80/why-your-nestjs-temporal-worker-ignores-sigterm-43bf</link>
      <guid>https://dev.to/gaganr80/why-your-nestjs-temporal-worker-ignores-sigterm-43bf</guid>
      <description>&lt;p&gt;I sent SIGTERM to a Temporal worker. The worker shut down. The process kept running for an hour.&lt;/p&gt;

&lt;p&gt;This was in the &lt;code&gt;nestjs-exchange-rates&lt;/code&gt; sample in &lt;code&gt;temporalio/samples-typescript&lt;/code&gt;, but the same shape of bug will show up in any NestJS app that creates a Temporal worker and also calls &lt;code&gt;app.listen()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I expected to find
&lt;/h2&gt;

&lt;p&gt;There was an open issue on the repo, #377, filed in July 2024. It said the NestJS sample calls &lt;code&gt;await this.worker.close()&lt;/code&gt;, and &lt;code&gt;close()&lt;/code&gt; doesn't exist on &lt;code&gt;Worker&lt;/code&gt; from &lt;code&gt;@temporalio/worker&lt;/code&gt;. Looked like a one line fix. Change &lt;code&gt;close()&lt;/code&gt; to &lt;code&gt;shutdown()&lt;/code&gt;, wire it to a Nest lifecycle hook, done.&lt;/p&gt;

&lt;p&gt;So I ran the sample and hit Ctrl+C to watch it fail.&lt;/p&gt;

&lt;p&gt;It didn't fail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Worker state changed { state: 'STOPPING' }
Worker state changed { state: 'DRAINING' }
Worker state changed { state: 'DRAINED' }
Worker state changed { state: 'STOPPED' }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the complete shutdown sequence. Polling stops, in flight tasks drain, cached workflows evict, and &lt;code&gt;run()&lt;/code&gt; resolves. Nothing was broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Temporal shuts the worker down for you
&lt;/h2&gt;

&lt;p&gt;The reason is in the SDK. When you create a Runtime it installs its own signal handlers. The default &lt;code&gt;shutdownSignals&lt;/code&gt; is &lt;code&gt;['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGUSR2']&lt;/code&gt;, and any of those calls &lt;code&gt;shutdown()&lt;/code&gt; on every worker in the process.&lt;/p&gt;

&lt;p&gt;So the broken &lt;code&gt;close()&lt;/code&gt; method in the sample never runs. It isn't wired to &lt;code&gt;onModuleDestroy&lt;/code&gt; or &lt;code&gt;onApplicationShutdown&lt;/code&gt;, and &lt;code&gt;main.ts&lt;/code&gt; never calls &lt;code&gt;app.enableShutdownHooks()&lt;/code&gt;, so Nest wouldn't call it even if it were. Nothing calls it, which is why nobody noticed it calls a method that doesn't exist on &lt;code&gt;Worker&lt;/code&gt; in 1.24.0.&lt;/p&gt;

&lt;p&gt;Dead code. Worth deleting, but that wasn't the bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  SIGTERM is different
&lt;/h2&gt;

&lt;p&gt;Ctrl+C is not the test. When you press Ctrl+C in a terminal, SIGINT goes to the entire foreground process group. That includes the &lt;code&gt;nest&lt;/code&gt; CLI wrapper that spawned your worker. The wrapper dies, the child goes with it, and your prompt comes back. Everything looks clean.&lt;/p&gt;

&lt;p&gt;Containers don't do that. Kubernetes sends SIGTERM to one process.&lt;/p&gt;

&lt;p&gt;So I did the same thing by hand:&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="nv"&gt;$ &lt;/span&gt;lsof &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; :3001
34333
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;-TERM&lt;/span&gt; 34333
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker logged all four states and reached STOPPED. Then:&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="nv"&gt;$ &lt;/span&gt;lsof &lt;span class="nt"&gt;-i&lt;/span&gt; :3001
node 34333 gaganr 31u IPv6 ... TCP &lt;span class="k"&gt;*&lt;/span&gt;:redwood-broker &lt;span class="o"&gt;(&lt;/span&gt;LISTEN&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;ps &lt;span class="nt"&gt;-p&lt;/span&gt; 34333
34333 ttys003 0:01.34 node &lt;span class="nt"&gt;--enable-source-maps&lt;/span&gt; .../dist/apps/worker/.../main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still there. I checked again an hour later and it was still there, still holding the port. The only thing that moved it was SIGKILL. I ran the whole thing again on a fresh process and got the same result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it hangs
&lt;/h2&gt;

&lt;p&gt;Two lifecycles, no wire between them.&lt;/p&gt;

&lt;p&gt;Temporal's signal handler stops the worker. That is the only thing it does. It has no idea a NestJS application exists.&lt;/p&gt;

&lt;p&gt;Meanwhile &lt;code&gt;apps/worker/src/main.ts&lt;/code&gt; has this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;NestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ExchangeRatesWorkerModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3001&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HTTP server on 3001 is still bound. Node keeps the event loop alive as long as a server is listening, and nobody ever calls &lt;code&gt;app.close()&lt;/code&gt;. So the worker is dead, the app is alive, and the process sits there.&lt;/p&gt;

&lt;p&gt;There was a third thing wrong, in the provider factory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;worker&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Started worker!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;run()&lt;/code&gt; returns a promise that resolves once the worker has finished shutting down. The sample threw it away. Nothing to await during shutdown, and if the worker ever failed at runtime the rejection went nowhere.&lt;/p&gt;

&lt;p&gt;In Kubernetes this costs you the full termination grace period on every deploy. The pod gets SIGTERM, ignores it, waits for SIGKILL. Default is 30 seconds.&lt;/p&gt;

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

&lt;p&gt;Let Temporal keep its signal handling. Give the &lt;code&gt;run()&lt;/code&gt; promise somewhere to live, and close the Nest app when it resolves.&lt;/p&gt;

&lt;p&gt;The provider factory stops starting the worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;taskQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;workflowOption&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;activities&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;The service exposes it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExchangeRatesWorkerService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EXCHANGE_RATES_WORKER&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;)&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;worker&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="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;And &lt;code&gt;main.ts&lt;/code&gt; owns the lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;NestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ExchangeRatesWorkerModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3001&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&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="nx"&gt;ExchangeRatesWorkerService&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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;&lt;code&gt;run()&lt;/code&gt; blocks until the SDK's signal handler shuts the worker down. Then &lt;code&gt;finally&lt;/code&gt; closes Nest, the HTTP server releases 3001, the event loop empties, and the process exits.&lt;/p&gt;

&lt;p&gt;Typing the injected worker as &lt;code&gt;Worker&lt;/code&gt; is what would have caught the original &lt;code&gt;close()&lt;/code&gt; bug. The constructor parameter had no type, so TypeScript had nothing to check it against.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to test this properly
&lt;/h2&gt;

&lt;p&gt;Don't use Ctrl+C. Get the PID and signal it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run start:worker
&lt;span class="c"&gt;# in another terminal&lt;/span&gt;
&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;-TERM&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;lsof &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; :3001&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;# wait a few seconds, then&lt;/span&gt;
lsof &lt;span class="nt"&gt;-i&lt;/span&gt; :3001
ps &lt;span class="nt"&gt;-p&lt;/span&gt; &amp;lt;pid&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both should come back empty. If either returns something, your process is ignoring SIGTERM and your deploys are paying for it.&lt;/p&gt;

&lt;p&gt;The fix is in &lt;code&gt;temporalio/samples-typescript&lt;/code&gt; PR #525.&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>node</category>
      <category>temporal</category>
    </item>
  </channel>
</rss>
