<?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: Jancer Lima</title>
    <description>The latest articles on DEV Community by Jancer Lima (@jancera).</description>
    <link>https://dev.to/jancera</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3769134%2Fafdb00bc-a64c-4120-b1c4-47e20f41a780.png</url>
      <title>DEV Community: Jancer Lima</title>
      <link>https://dev.to/jancera</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jancera"/>
    <language>en</language>
    <item>
      <title>Why worker pools beat clustering for CPU-Heavy tasks on Node.js</title>
      <dc:creator>Jancer Lima</dc:creator>
      <pubDate>Tue, 05 May 2026 00:15:24 +0000</pubDate>
      <link>https://dev.to/jancera/why-worker-pools-beat-clustering-for-cpu-heavy-tasks-on-nodejs-2eoe</link>
      <guid>https://dev.to/jancera/why-worker-pools-beat-clustering-for-cpu-heavy-tasks-on-nodejs-2eoe</guid>
      <description>&lt;p&gt;Imagine you have a Nodejs server with endpoint that performs heavy CPU operations.&lt;/p&gt;

&lt;p&gt;By default your server runs on a single thread. This means it will freeze depending on the CPU load. If your server has other asynchronous endpoints, for example, to execute database operations, those endpoints would become unresponsive while the heavy load endpoint is processing.&lt;/p&gt;

&lt;p&gt;Our first idea is to create more threads, sending the heavy tasks to be processed in parallel by another CPU core. Once finished, we send the output back to the main thread and return the answer to the client.&lt;/p&gt;

&lt;p&gt;The problem now is that we can not have more threads than available CPU cores (technically we can but it does not make much sense) so we start thinking about using worker pools where we instantiate an fixed amount of workers and reuse them to our desired tasks.&lt;/p&gt;

&lt;p&gt;Now we have a stable structure where we offload CPU intensive tasks to other threads to make the main thread free and available for new requests.&lt;/p&gt;

&lt;p&gt;I've setup a test case where we run our server into a docker container with 2 CPUs and 2Gb of memory. Our server has a root endpoint &lt;code&gt;/&lt;/code&gt; which returns an OK response and a &lt;code&gt;/blocking/:n&lt;/code&gt; endpoint that runs a Fibonacci algorithm. &lt;/p&gt;

&lt;p&gt;I've let &lt;code&gt;n&lt;/code&gt; as a parameter so we can customize how much work we want our server to do &lt;em&gt;(n is the Fibonacci input)&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;All the source code for the server and the benchmark can be found &lt;a href="https://github.com/Jancera/worker-pools-lab" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I've setup 20k requests on &lt;code&gt;/&lt;/code&gt; and 30 requests on &lt;code&gt;/blocking/35&lt;/code&gt;. It takes approximately 10 seconds to execute and we can analyze the output. &lt;em&gt;Note: The tests were hitting both endpoints simultaneously.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One test was against a server with 1 instance with 2 threads.&lt;/p&gt;

&lt;p&gt;The other test was against a server with 2 instances with 1 thread each.&lt;/p&gt;

&lt;p&gt;The server clustering were made using node &lt;code&gt;cluster&lt;/code&gt; module, the worker pool was using &lt;code&gt;Piscina&lt;/code&gt; which internally uses &lt;code&gt;worker_threads&lt;/code&gt; module and the tests were executed with &lt;code&gt;autocannon&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;2 processes, 1 thread each&lt;/strong&gt;&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;Results for http://localhost:3000/

┌─────────┬──────┬──────┬───────┬───────┬─────────┬─────────┬───────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%   │ Avg     │ Stdev   │ Max   │
├─────────┼──────┼──────┼───────┼───────┼─────────┼─────────┼───────┤
│ Latency │ 0 ms │ 2 ms │ 43 ms │ 50 ms │ 4.17 ms │ 9.12 ms │ 94 ms │
└─────────┴──────┴──────┴───────┴───────┴─────────┴─────────┴───────┘
┌───────────┬────────┬────────┬────────┬────────┬─────────┬────────┬────────┐
│ Stat      │ 1%     │ 2.5%   │ 50%    │ 97.5%  │ Avg     │ Stdev  │ Min    │
├───────────┼────────┼────────┼────────┼────────┼─────────┼────────┼────────┤
│ Req/Sec   │ 1,162  │ 1,162  │ 1,926  │ 3,761  │ 2,000.2 │ 744.83 │ 1,162  │
├───────────┼────────┼────────┼────────┼────────┼─────────┼────────┼────────┤
│ Bytes/Sec │ 277 kB │ 277 kB │ 458 kB │ 895 kB │ 476 kB  │ 177 kB │ 277 kB │
└───────────┴────────┴────────┴────────┴────────┴─────────┴────────┴────────┘

Req/Bytes counts sampled once per second.
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;of samples: 10
&lt;span class="go"&gt;
20k requests in 10.07s, 4.76 MB read


Results for http://localhost:3000/blocking/35

┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐
│ Stat    │ 2.5%   │ 50%     │ 97.5%   │ 99%     │ Avg        │ Stdev      │ Max     │
├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤
│ Latency │ 276 ms │ 1968 ms │ 8645 ms │ 8645 ms │ 2327.81 ms │ 1998.18 ms │ 8645 ms │
└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘
┌───────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐
│ Stat      │ 1%    │ 2.5%  │ 50%   │ 97.5% │ Avg   │ Stdev │ Min   │
├───────────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┤
│ Req/Sec   │ 1     │ 1     │ 3     │ 3     │ 2.73  │ 0.62  │ 1     │
├───────────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┤
│ Bytes/Sec │ 253 B │ 253 B │ 759 B │ 759 B │ 690 B │ 156 B │ 253 B │
└───────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘

Req/Bytes counts sampled once per second.
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;of samples: 11
&lt;span class="go"&gt;
30 requests in 11.07s, 7.59 kB read
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1 process with 2 threads&lt;/strong&gt;&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;Results for http://localhost:3000/

┌─────────┬──────┬──────┬───────┬───────┬─────────┬─────────┬───────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%   │ Avg     │ Stdev   │ Max   │
├─────────┼──────┼──────┼───────┼───────┼─────────┼─────────┼───────┤
│ Latency │ 1 ms │ 2 ms │ 35 ms │ 43 ms │ 4.56 ms │ 7.63 ms │ 61 ms │
└─────────┴──────┴──────┴───────┴───────┴─────────┴─────────┴───────┘
┌───────────┬────────┬────────┬────────┬────────┬──────────┬────────┬────────┐
│ Stat      │ 1%     │ 2.5%   │ 50%    │ 97.5%  │ Avg      │ Stdev  │ Min    │
├───────────┼────────┼────────┼────────┼────────┼──────────┼────────┼────────┤
│ Req/Sec   │ 640    │ 640    │ 1,454  │ 3,379  │ 1,818.37 │ 790.84 │ 640    │
├───────────┼────────┼────────┼────────┼────────┼──────────┼────────┼────────┤
│ Bytes/Sec │ 152 kB │ 152 kB │ 346 kB │ 804 kB │ 433 kB   │ 188 kB │ 152 kB │
└───────────┴────────┴────────┴────────┴────────┴──────────┴────────┴────────┘

Req/Bytes counts sampled once per second.
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;of samples: 11
&lt;span class="go"&gt;
20k requests in 11.05s, 4.76 MB read


Results for http://localhost:3000/blocking/35

┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐
│ Stat    │ 2.5%   │ 50%     │ 97.5%   │ 99%     │ Avg        │ Stdev     │ Max     │
├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤
│ Latency │ 276 ms │ 2794 ms │ 3861 ms │ 3861 ms │ 2358.17 ms │ 957.99 ms │ 3861 ms │
└─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘
┌───────────┬───────┬───────┬─────────┬─────────┬───────┬───────┬───────┐
│ Stat      │ 1%    │ 2.5%  │ 50%     │ 97.5%   │ Avg   │ Stdev │ Min   │
├───────────┼───────┼───────┼─────────┼─────────┼───────┼───────┼───────┤
│ Req/Sec   │ 2     │ 2     │ 4       │ 4       │ 3.34  │ 0.82  │ 2     │
├───────────┼───────┼───────┼─────────┼─────────┼───────┼───────┼───────┤
│ Bytes/Sec │ 506 B │ 506 B │ 1.01 kB │ 1.01 kB │ 843 B │ 207 B │ 506 B │
└───────────┴───────┴───────┴─────────┴─────────┴───────┴───────┴───────┘

Req/Bytes counts sampled once per second.
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;of samples: 9
&lt;span class="go"&gt;
30 requests in 9.02s, 7.59 kB read
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We can see that having 1 process with 2 threads gave us better results for both endpoints.&lt;/p&gt;

&lt;p&gt;Looking to the 99th metric, it was 7ms faster for the &lt;code&gt;/&lt;/code&gt; route and 4784ms for the &lt;code&gt;/blocking&lt;/code&gt; endpoint.  &lt;/p&gt;

&lt;p&gt;This shows us that spinning up multiple independent process might seem like a quick scaling fix, but in practice, we waste resources managing process overhead instead of computing actual work. More importantly, a single process with a worker pool keeps the main Event Loop unblocked. It successfully handles all incoming traffic and efficiently distributes the heavy CPu load, resulting in a significantly lower wait times for 99% of our requests.&lt;/p&gt;

&lt;p&gt;Of course we could expand our test scenario and environment looking for more realistic numbers, but that will be a work for another article. &lt;/p&gt;

&lt;p&gt;Thanks for your attention!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>node</category>
    </item>
    <item>
      <title>Incident Report: Service failure due to storage full</title>
      <dc:creator>Jancer Lima</dc:creator>
      <pubDate>Sat, 18 Apr 2026 01:09:42 +0000</pubDate>
      <link>https://dev.to/jancera/incident-report-service-failure-due-to-storage-full-4a4p</link>
      <guid>https://dev.to/jancera/incident-report-service-failure-due-to-storage-full-4a4p</guid>
      <description>&lt;p&gt;Yesterday, my homelab server suddenly became unresponsive. It started with a flurry of Discord notifications, the universal signal that something has gone seriously wrong.&lt;/p&gt;

&lt;p&gt;I found all services offline. The logs pointed to a primary culprit: a Redis failure, specifically a Server Out of Memory error.&lt;/p&gt;

&lt;p&gt;The core error was: &lt;code&gt;RedisClient::CommandError: MISCONF Errors writing to the AOF file: No space left on device&lt;/code&gt;&lt;br&gt;
My first thought was: Why is AOF even enabled? I turned it on for testing and forgot. My root partition was at 99% capacity, just 270MB remaining out of 24GB.&lt;/p&gt;

&lt;p&gt;Further investigation revealed where the "wasted" space was hiding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PM2 Logs (~3.8GB):&lt;/strong&gt; The process manager was storing massive, unrotated text logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden Caches (~1.5GB):&lt;/strong&gt; Accumulated &lt;code&gt;~/.cache&lt;/code&gt;, &lt;code&gt;~/.npm&lt;/code&gt;, and &lt;code&gt;~/.rvm&lt;/code&gt;source files from multiple builds and deployments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To get the system breathing again, I performed a quick "surgical" cleaning:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;PM2 Flush:&lt;/strong&gt; Immediately cleared the massive log files using &lt;code&gt;pm2 flush&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log Truncation:&lt;/strong&gt; Emptied application logs using &lt;code&gt;truncate -s 0 log/*.log&lt;/code&gt; (this clears the content without deleting the file handle).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Pruning:&lt;/strong&gt; Deleted hidden build caches in &lt;code&gt;~/.npm&lt;/code&gt; and &lt;code&gt;~/.cache&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Journal Vacuum:&lt;/strong&gt; Cleared system logs with &lt;code&gt;journalctl --vacuum-size=500M&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now I had enough space to spin up all process again, but I need to recover Redis since it entered a Read-Only mode to protect data integrity.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fixing the AOF Manifest:&lt;/strong&gt; Because the disk filled during a Redis write, the &lt;code&gt;appendonly.aof.manifest&lt;/code&gt; was corrupted. I fixed it using &lt;code&gt;sudo redis-check-aof --fix&lt;/code&gt; on the manifest file inside &lt;code&gt;/var/lib/redis/appendonlydir/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clearing the MISCONF Lock:&lt;/strong&gt; Even with free space, Redis remained in a "protected" state. I manually overrode this with &lt;code&gt;redis-cli config set stop-writes-on-bgsave-error no&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service Restart:&lt;/strong&gt; Reset the systemd failure counter with &lt;code&gt;systemctl reset-failed redis-server&lt;/code&gt; and restarted the service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After that I could successfully restart all services and have everything running. All data inside redis were not critical, so I didn't care about losing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;The failure was a classic case of neglecting "boring" infrastructure: log rotation and disk monitoring. To prevent a repeat performance, I've implemented the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Log Management: Installed pm2-logrotate to cap PM2 logs at 10MB per file and limited journald to 500MB globally.&lt;/li&gt;
&lt;li&gt;Next Steps:

&lt;ul&gt;
&lt;li&gt;Expand the VM disk size (24GB is too tight for this stack).&lt;/li&gt;
&lt;li&gt;Set up a cron job for weekly &lt;code&gt;apt autoremove&lt;/code&gt; and cache clearing.&lt;/li&gt;
&lt;li&gt;Implement an automated disk usage alert (likely via Grafana or a simple shell script to Discord).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Is TLS Enough? A Retrospective on Application-Layer Encryption</title>
      <dc:creator>Jancer Lima</dc:creator>
      <pubDate>Tue, 24 Feb 2026 11:27:35 +0000</pubDate>
      <link>https://dev.to/jancera/is-tls-enough-a-retrospective-on-application-layer-encryption-3759</link>
      <guid>https://dev.to/jancera/is-tls-enough-a-retrospective-on-application-layer-encryption-3759</guid>
      <description>&lt;p&gt;Years ago, I was part of a heated debate that every engineering team eventually faces: &lt;strong&gt;Is standard TLS enough, or do we need custom application-layer encryption?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We were implementing a payment solution. The provider required a backend-to-backend integration, meaning we had to take user credit card data, send it to our server, and then forward it to the provider.&lt;/p&gt;

&lt;p&gt;My argument was that the TLS layer would be enough for it. The rest of the team disagreed. They didn't have a technical counter-argument, it was just a "lack of trust".&lt;/p&gt;

&lt;p&gt;We ended up building a complex Dual-Keypair System:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The app keypair: Used to sign requests so the server could verify the data actually came from our app (Authenticity).&lt;/li&gt;
&lt;li&gt;The server keypair: The app used the server's public key to encrypt the payload, ensuring only our backend could read it (Confidentiality).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It worked, but years later I realized the hidden parts we didn't consider.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The scale: We were small then. But if you scale to multiple server instances, you suddenly need a secure way to share those private keys. You've just turned a "payment problem" into a "key management problem".&lt;/li&gt;
&lt;li&gt;The key rotation: What happens if a key is compromised or expires? If you have users on old app versions, you're stuck. You either support legacy keys forever or force your users to update, either options are bad.&lt;/li&gt;
&lt;li&gt;The "hostile" client: We stored a key in the app. But unless you are using the device's hardware Secure Enclave (iOS) or Keystore (Android), your app is a hostile environment. A determined attacker can decompile the code and extract those keys.&lt;/li&gt;
&lt;li&gt;The TLS termination: The only valid concern was where the TLS "ends". If your HTTPS connection terminates at a Load Balancer and the internal traffic to your web server is plain HTTP, you have a gap.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Unless you are building a banking core or a literal payment gateway, &lt;strong&gt;TLS is enough&lt;/strong&gt;. If you’re worried about the "last mile" inside your VPC, solve that at the infrastructure level. Don’t bake custom crypto into your application logic unless you’re ready to manage the massive operational overhead that comes with it.&lt;/p&gt;

&lt;p&gt;Experience taught me that "Trust issues" should be solved with better infrastructure, not more code.&lt;/p&gt;

</description>
      <category>security</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
