<?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: Vishal Pandey</title>
    <description>The latest articles on DEV Community by Vishal Pandey (@0xv1shal).</description>
    <link>https://dev.to/0xv1shal</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%2F3627513%2Fa419cae9-0f21-48fe-ab29-366ed47a4488.png</url>
      <title>DEV Community: Vishal Pandey</title>
      <link>https://dev.to/0xv1shal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/0xv1shal"/>
    <language>en</language>
    <item>
      <title>From | to SIGINT : How Linux Shells Build and Control Pipelines</title>
      <dc:creator>Vishal Pandey</dc:creator>
      <pubDate>Sun, 20 Sep 2026 06:57:43 +0000</pubDate>
      <link>https://dev.to/0xv1shal/from-to-sigint-how-linux-shells-build-and-control-pipelines-44ei</link>
      <guid>https://dev.to/0xv1shal/from-to-sigint-how-linux-shells-build-and-control-pipelines-44ei</guid>
      <description>&lt;p&gt;Have you ever looked at a command like this and wondered what the &lt;code&gt;|&lt;/code&gt; is actually doing?&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="nb"&gt;cat &lt;/span&gt;file.txt | &lt;span class="nb"&gt;grep &lt;/span&gt;error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We type pipelines so often that the symbol almost disappears. &lt;code&gt;cat&lt;/code&gt; produces some text, &lt;code&gt;grep&lt;/code&gt; filters it, and the terminal prints the result. Simple enough.&lt;/p&gt;

&lt;p&gt;Linux has to build that connection before either program can do its work.&lt;/p&gt;

&lt;p&gt;There is the shell, one process running &lt;code&gt;cat&lt;/code&gt;, and another running &lt;code&gt;grep&lt;/code&gt;. The two commands have different process IDs. A kernel pipe carries bytes between them, and a process group lets the shell control them as one job.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;|&lt;/code&gt; character never reaches &lt;code&gt;cat&lt;/code&gt; or &lt;code&gt;grep&lt;/code&gt;. Bash interprets it first. It creates the pipe, starts the commands, changes their standard file descriptors, groups the processes, and waits.&lt;/p&gt;

&lt;p&gt;A tiny character is hiding a fair amount of operating-system machinery.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0vh3ovpiyuc6o7u24y6m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0vh3ovpiyuc6o7u24y6m.png" alt="How Bash connects two processes through a kernel pipe and places them in one foreground process group" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The finished pipeline
&lt;/h2&gt;

&lt;p&gt;Start with the state Bash is trying to create.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cat&lt;/code&gt; and &lt;code&gt;grep&lt;/code&gt; remain ordinary programs. &lt;code&gt;cat&lt;/code&gt; writes to standard output. &lt;code&gt;grep&lt;/code&gt; reads from standard input. Bash changes where those descriptors lead before either program begins running.&lt;/p&gt;

&lt;p&gt;Two separate ideas are involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pipe          moves bytes between processes
Process group lets the shell and terminal control related processes together
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping those ideas separate makes the rest much easier to follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bash creates a kernel pipe
&lt;/h2&gt;

&lt;p&gt;For a simple two-command pipeline, Bash needs one pipe. Conceptually, it asks the kernel for one using &lt;code&gt;pipe()&lt;/code&gt; or &lt;code&gt;pipe2()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The kernel returns two file descriptors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FD 3 -&amp;gt; pipe read end
FD 4 -&amp;gt; pipe write end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The numbers &lt;code&gt;3&lt;/code&gt; and &lt;code&gt;4&lt;/code&gt; are only examples. The kernel returns available descriptor numbers in the calling process.&lt;/p&gt;

&lt;p&gt;The pipe is a unidirectional byte stream maintained by the kernel. Bytes written through its write end can be read through its read end. It has no application-level idea of lines, JSON objects, or log records. Those meanings belong to the programs using it.&lt;/p&gt;

&lt;p&gt;At this point, the shell might have a descriptor table like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bash

FD 0 -&amp;gt; terminal input
FD 1 -&amp;gt; terminal output
FD 2 -&amp;gt; terminal error output
FD 3 -&amp;gt; pipe read end
FD 4 -&amp;gt; pipe write end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipe exists, but neither command exists yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Each command gets a process
&lt;/h2&gt;

&lt;p&gt;The traditional model is &lt;code&gt;fork()&lt;/code&gt; followed by &lt;code&gt;execve()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Bash forks a child for &lt;code&gt;cat&lt;/code&gt;. It also forks a child for &lt;code&gt;grep&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bash PID 1000
   |
   +-- child PID 1101
   |
   `-- child PID 1102
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After &lt;code&gt;fork()&lt;/code&gt;, a child inherits copies of its parent's open file descriptors. Those descriptors refer to the same underlying open-file descriptions as the parent's descriptors. The child therefore inherits access to both ends of the pipe.&lt;/p&gt;

&lt;p&gt;The child still begins as a copy of the shell process. It has not become &lt;code&gt;cat&lt;/code&gt; or &lt;code&gt;grep&lt;/code&gt; yet.&lt;/p&gt;

&lt;p&gt;That happens through &lt;code&gt;execve()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Child PID 1101 --execve()--&amp;gt; cat PID 1101
Child PID 1102 --execve()--&amp;gt; grep PID 1102
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;execve()&lt;/code&gt; replaces the program running inside a process. The process keeps its PID, while its code, stack, heap, and other program state are replaced for the new executable. File descriptors normally remain open across &lt;code&gt;execve()&lt;/code&gt; unless they carry the close-on-exec flag.&lt;/p&gt;

&lt;p&gt;This distinction is easy to miss:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fork()   creates a new process
execve() replaces the program inside a process
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real shells have implementation details and optimizations. They may use &lt;code&gt;posix_spawn()&lt;/code&gt; on some systems, and built-ins can follow different rules. The &lt;code&gt;fork()&lt;/code&gt; plus &lt;code&gt;execve()&lt;/code&gt; model still exposes the mechanics cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;dup2()&lt;/code&gt; does the wiring
&lt;/h2&gt;

&lt;p&gt;The children inherited the pipe, but their standard streams still point at the terminal. Bash must rearrange the descriptors before executing the programs.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;cat&lt;/code&gt; child needs its standard output connected to the write end of the pipe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;dup2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipe_write&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;STDOUT_FILENO&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the example descriptor numbers, that is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;dup2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Afterward, descriptors &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;4&lt;/code&gt; refer to the same pipe endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat child

FD 1 --+
       +--&amp;gt; pipe write end
FD 4 --+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The child can close descriptor &lt;code&gt;4&lt;/code&gt;. Standard output already points to the required endpoint.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;grep&lt;/code&gt; child needs the other side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;dup2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipe_read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;STDIN_FILENO&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the example numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;dup2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its standard input now refers to the pipe's read end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grep child

FD 0 --+
       +--&amp;gt; pipe read end
FD 3 --+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once both children have been prepared and the unnecessary descriptors closed, their useful descriptor tables look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat
  FD 0 -&amp;gt; inherited input
  FD 1 -&amp;gt; pipe write end
  FD 2 -&amp;gt; terminal

grep
  FD 0 -&amp;gt; pipe read end
  FD 1 -&amp;gt; terminal
  FD 2 -&amp;gt; terminal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bash can now execute the programs. &lt;code&gt;cat&lt;/code&gt; writes to FD &lt;code&gt;1&lt;/code&gt; as usual, except FD &lt;code&gt;1&lt;/code&gt; leads into the pipe. &lt;code&gt;grep&lt;/code&gt; reads FD &lt;code&gt;0&lt;/code&gt;, which leads to the other end.&lt;/p&gt;

&lt;p&gt;No special pipeline-aware version of either program is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing descriptors is part of correctness
&lt;/h2&gt;

&lt;p&gt;Every process inherits descriptors during &lt;code&gt;fork()&lt;/code&gt;. That inheritance creates copies which must be closed when a process does not need them.&lt;/p&gt;

&lt;p&gt;After starting both commands, Bash still holds its own copies of the pipe descriptors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bash
  FD 3 -&amp;gt; pipe read end
  FD 4 -&amp;gt; pipe write end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bash should close both. The children must also close every pipe end they do not use.&lt;/p&gt;

&lt;p&gt;This affects observable behaviour.&lt;/p&gt;

&lt;p&gt;A reader sees EOF only after every descriptor referring to the pipe's write end has been closed. Suppose &lt;code&gt;cat&lt;/code&gt; finishes, but Bash accidentally keeps its copy of the write end open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat's write descriptor   -&amp;gt; closed
Bash's write descriptor  -&amp;gt; still open
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the kernel's perspective, a writer still exists. &lt;code&gt;grep&lt;/code&gt; can remain blocked, waiting for bytes that will never arrive.&lt;/p&gt;

&lt;p&gt;The other direction has its own failure mode. Once every read end is closed, another write generates &lt;code&gt;SIGPIPE&lt;/code&gt;. If the writer ignores that signal, &lt;code&gt;write()&lt;/code&gt; fails with &lt;code&gt;EPIPE&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;You can see this relationship with:&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="nb"&gt;yes&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;head&lt;/code&gt; exits after five lines and closes its read side. &lt;code&gt;yes&lt;/code&gt; keeps trying to write, so it encounters the broken pipe condition and stops.&lt;/p&gt;

&lt;p&gt;Closing an unused descriptor is doing more than saving a small kernel resource. It changes when peers observe EOF and broken pipes.&lt;/p&gt;

&lt;h2&gt;
  
  
  One pipeline, several PIDs, one process group
&lt;/h2&gt;

&lt;p&gt;The pipeline now moves data correctly. Bash still needs a way to treat its processes as one interactive job.&lt;/p&gt;

&lt;p&gt;That is the role of a process group.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bash
  PID  = 1000
  PGID = 1000

Pipeline
  cat   PID = 1101, PGID = 1101
  grep  PID = 1102, PGID = 1101
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The group does not require an additional process. One member is the process-group leader, and its PID is used as the PGID. In this example, &lt;code&gt;cat&lt;/code&gt; is the leader because its PID and the group's PGID are both &lt;code&gt;1101&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An interactive shell normally arranges this with &lt;code&gt;setpgid()&lt;/code&gt;. The exact ordering is more careful than a straight-line diagram suggests because the parent and children run concurrently. The shell has to avoid races while the children start and execute their programs.&lt;/p&gt;

&lt;p&gt;The useful mental model remains simple: every external command usually receives its own PID, while all processes in one pipeline join the same process group.&lt;/p&gt;

&lt;p&gt;Bash builds a &lt;strong&gt;job&lt;/strong&gt; around that pipeline. A job is shell state: it has a job number, status, and command text. The kernel knows about processes and process groups. It does not know about Bash job specifications such as &lt;code&gt;%1&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shell job        user-facing bookkeeping in Bash
Process group    kernel mechanism beneath that job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The terminal has a foreground process group
&lt;/h2&gt;

&lt;p&gt;Process groups become especially useful once a terminal is involved.&lt;/p&gt;

&lt;p&gt;A terminal tracks one foreground process group. &lt;code&gt;tcsetpgrp()&lt;/code&gt; changes that group, while &lt;code&gt;tcgetpgrp()&lt;/code&gt; reads it.&lt;/p&gt;

&lt;p&gt;Before Bash runs the pipeline, the shell itself owns the terminal foreground:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Terminal foreground PGID = 1000
Bash PGID                = 1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bash creates the pipeline and gives terminal foreground control to PGID &lt;code&gt;1101&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Terminal foreground PGID = 1101

PGID 1101
  |- cat
  `- grep
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bash is still alive. It has simply moved outside the terminal's foreground group while it waits for the job.&lt;/p&gt;

&lt;p&gt;This foreground relationship controls which job can normally read from the terminal. It also decides where keyboard-generated job-control signals go.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;Ctrl+C&lt;/code&gt; actually does
&lt;/h2&gt;

&lt;p&gt;Pressing &lt;code&gt;Ctrl+C&lt;/code&gt; does not ask Bash to choose a PID and kill it.&lt;/p&gt;

&lt;p&gt;In a normal terminal session, the path looks more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;keyboard
   |
   v
terminal emulator
   |
   v
pseudoterminal master
   |
   v
pseudoterminal slave
   |
   v
N_TTY line discipline
   |
   v
SIGINT sent to the foreground process group
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The terminal emulator commonly writes byte &lt;code&gt;0x03&lt;/code&gt; for &lt;code&gt;Ctrl+C&lt;/code&gt; to the PTY master. On the slave side, Linux's default &lt;code&gt;N_TTY&lt;/code&gt; line discipline processes the input.&lt;/p&gt;

&lt;p&gt;Terminal settings define an interrupt character named &lt;code&gt;VINTR&lt;/code&gt;. Its usual value is &lt;code&gt;^C&lt;/code&gt;. When the &lt;code&gt;ISIG&lt;/code&gt; setting is enabled, the line discipline interprets that character and generates &lt;code&gt;SIGINT&lt;/code&gt;. The byte is consumed as terminal control input instead of being delivered to the foreground program as ordinary stdin data.&lt;/p&gt;

&lt;p&gt;The target is the terminal's foreground process group.&lt;/p&gt;

&lt;p&gt;In the example, both processes receive &lt;code&gt;SIGINT&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SIGINT -&amp;gt; PGID 1101
           |- cat  PID 1101
           `- grep PID 1102
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This explains why a whole pipeline usually reacts to one &lt;code&gt;Ctrl+C&lt;/code&gt;. The pipe is not carrying the signal. Process-group membership gives the kernel the target set.&lt;/p&gt;

&lt;p&gt;It also explains why Bash survives. Bash belongs to PGID &lt;code&gt;1000&lt;/code&gt;, while the terminal's foreground PGID is &lt;code&gt;1101&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;SIGINT&lt;/code&gt; is a request the process can handle
&lt;/h2&gt;

&lt;p&gt;The default action for &lt;code&gt;SIGINT&lt;/code&gt; is process termination, which creates the familiar effect of &lt;code&gt;Ctrl+C&lt;/code&gt; stopping a command.&lt;/p&gt;

&lt;p&gt;A program can install a handler, block the signal temporarily, or ignore it. Python, for example, normally turns &lt;code&gt;SIGINT&lt;/code&gt; into &lt;code&gt;KeyboardInterrupt&lt;/code&gt;. Other programs use it to close files, restore terminal state, or ask the user to confirm an exit.&lt;/p&gt;

&lt;p&gt;Every process in the foreground group receives the signal, but each one applies its own signal disposition. A pipeline can therefore react unevenly if one command catches &lt;code&gt;SIGINT&lt;/code&gt; and another uses the default action.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SIGKILL&lt;/code&gt; has different rules. A process cannot catch, block, or ignore it. That is one reason &lt;code&gt;kill -9&lt;/code&gt; should not be the first way to stop a program: it removes any chance for application-level cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Bash gets the prompt back
&lt;/h2&gt;

&lt;p&gt;While the foreground job runs, Bash waits for its children. It needs to notice several possible outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a process exits normally;&lt;/li&gt;
&lt;li&gt;a process terminates because of a signal;&lt;/li&gt;
&lt;li&gt;a process stops, commonly after &lt;code&gt;Ctrl+Z&lt;/code&gt; generates &lt;code&gt;SIGTSTP&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the foreground job exits or stops, Bash makes its own process group the terminal foreground again. Then it updates the job's status and prints another prompt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pipeline running:
  terminal foreground PGID = 1101

Pipeline finishes or stops:
  terminal foreground PGID = 1000
  Bash prints the prompt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fg&lt;/code&gt; and &lt;code&gt;bg&lt;/code&gt; use the same model. &lt;code&gt;fg&lt;/code&gt; continues a stopped job and gives its process group the terminal. &lt;code&gt;bg&lt;/code&gt; continues the job without transferring foreground terminal ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch the IDs yourself
&lt;/h2&gt;

&lt;p&gt;Start a pipeline that stays alive long enough to inspect:&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="nb"&gt;sleep &lt;/span&gt;100 | &lt;span class="nb"&gt;sleep &lt;/span&gt;100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From another terminal, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-o&lt;/span&gt; pid,ppid,pgid,sid,tpgid,stat,comm &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nb"&gt;sleep&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An example result might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  PID  PPID  PGID   SID TPGID STAT COMMAND
 2101  2000  2101  2000  2101 S+   sleep
 2102  2000  2101  2000  2101 S+   sleep
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact values will differ. Look at their shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the PIDs are different;&lt;/li&gt;
&lt;li&gt;the PGIDs match;&lt;/li&gt;
&lt;li&gt;the TPGID matches the pipeline's PGID because this is the terminal's foreground job;&lt;/li&gt;
&lt;li&gt;both processes belong to the same session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Press &lt;code&gt;Ctrl+C&lt;/code&gt; in the first terminal. Both &lt;code&gt;sleep&lt;/code&gt; processes should disappear.&lt;/p&gt;

&lt;p&gt;You can also inspect the terminal's configured special characters:&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="nb"&gt;stty&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for values similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;intr = ^C
susp = ^Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These mappings are configurable. &lt;code&gt;Ctrl+C&lt;/code&gt; is conventional rather than an unchangeable property of the keyboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few boundaries around the model
&lt;/h2&gt;

&lt;p&gt;The diagrams describe a normal interactive Bash session with job control enabled and external commands in the pipeline. They are a mental model, not Bash source code.&lt;/p&gt;

&lt;p&gt;Shell built-ins complicate process placement. Bash usually runs pipeline components in subshell environments, while options such as &lt;code&gt;lastpipe&lt;/code&gt; can change where the final component runs when job control is disabled.&lt;/p&gt;

&lt;p&gt;Non-interactive shells commonly run without job control. They can still build pipes and execute multiple processes, but the foreground process-group handoff may not apply in the same way.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;set -o pipefail&lt;/code&gt; changes how Bash calculates the pipeline's exit status. The processes stay separate.&lt;/p&gt;

&lt;p&gt;And a longer pipeline simply extends the same wiring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;producer | transform | consumer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It normally needs two kernel pipes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;producer stdout
      |
      v
   pipe A -&amp;gt; transform -&amp;gt; pipe B -&amp;gt; consumer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each process closes every pipe endpoint it does not need. The shell places the commands into a shared process group when interactive job control calls for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole path
&lt;/h2&gt;

&lt;p&gt;The original command was short:&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="nb"&gt;cat &lt;/span&gt;file.txt | &lt;span class="nb"&gt;grep &lt;/span&gt;error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its working shape is larger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bash parses the pipeline
        |
        v
creates a kernel pipe
        |
        v
creates a process for each command
        |
        v
uses dup2() to connect stdout and stdin
        |
        v
closes unused descriptors
        |
        v
executes cat and grep
        |
        v
places them in one process group
        |
        v
gives that group the terminal foreground
        |
        v
waits for the job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;Ctrl+C&lt;/code&gt; takes a different path through the same structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ctrl+C
  -&amp;gt; PTY
  -&amp;gt; N_TTY line discipline
  -&amp;gt; SIGINT
  -&amp;gt; foreground process group
  -&amp;gt; cat and grep each decide how to respond
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipe moves the data. The process group gives the job a boundary. The terminal uses that boundary when you ask the foreground work to stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Job-Control-Basics.html" rel="noopener noreferrer"&gt;GNU Bash manual: Job Control Basics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Signals.html" rel="noopener noreferrer"&gt;GNU Bash manual: Signals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man7/pipe.7.html" rel="noopener noreferrer"&gt;Linux man-pages: &lt;code&gt;pipe(7)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/dup.2.html" rel="noopener noreferrer"&gt;Linux man-pages: &lt;code&gt;dup(2)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/fork.2.html" rel="noopener noreferrer"&gt;Linux man-pages: &lt;code&gt;fork(2)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/execve.2.html" rel="noopener noreferrer"&gt;Linux man-pages: &lt;code&gt;execve(2)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/setpgid.2.html" rel="noopener noreferrer"&gt;Linux man-pages: &lt;code&gt;setpgid(2)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man3/tcsetpgrp.3.html" rel="noopener noreferrer"&gt;Linux man-pages: &lt;code&gt;tcsetpgrp(3)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man3/termios.3.html" rel="noopener noreferrer"&gt;Linux man-pages: &lt;code&gt;termios(3)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.kernel.org/driver-api/tty/n_tty.html" rel="noopener noreferrer"&gt;Linux kernel documentation: N_TTY&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>kernel</category>
      <category>operating</category>
      <category>systems</category>
    </item>
    <item>
      <title>Introducing limitngin — A Lightweight, Memory-Efficient &amp; Scalable Rate Limiter for Express</title>
      <dc:creator>Vishal Pandey</dc:creator>
      <pubDate>Tue, 03 Mar 2026 01:52:00 +0000</pubDate>
      <link>https://dev.to/0xv1shal/introducing-limitngin-a-lightweight-memory-efficient-scalable-rate-limiter-for-express-on</link>
      <guid>https://dev.to/0xv1shal/introducing-limitngin-a-lightweight-memory-efficient-scalable-rate-limiter-for-express-on</guid>
      <description>&lt;p&gt;I recently built and open-sourced &lt;strong&gt;limitngin&lt;/strong&gt;, a lightweight, zero-dependency, ESM-only rate limiter middleware for Express.&lt;/p&gt;

&lt;p&gt;The goal was simple:&lt;/p&gt;

&lt;p&gt;Build something that works efficiently for both small projects and larger systems, without forcing external infrastructure from day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Designed for Small Projects — Without Redis
&lt;/h2&gt;

&lt;p&gt;For:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Personal projects&lt;/li&gt;
&lt;li&gt;MVPs&lt;/li&gt;
&lt;li&gt;Internal tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You often don’t want to set up Redis just to enable basic rate limiting.&lt;/p&gt;

&lt;p&gt;limitngin uses an optimized in-memory store that is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Map-based for better memory behavior&lt;/li&gt;
&lt;li&gt;Automatically cleaned up&lt;/li&gt;
&lt;li&gt;Stable under high key churn&lt;/li&gt;
&lt;li&gt;Zero external dependencies&lt;/li&gt;
&lt;li&gt;Drop it in and it works.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Built With Scale in Mind
&lt;/h2&gt;

&lt;p&gt;Even though it starts in-memory, the internal architecture is designed for extensibility.&lt;/p&gt;

&lt;p&gt;It already supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sliding Window Counter (default)&lt;/li&gt;
&lt;li&gt;Token Bucket&lt;/li&gt;
&lt;li&gt;IP-based limiting&lt;/li&gt;
&lt;li&gt;Auth-token–based limiting&lt;/li&gt;
&lt;li&gt;Standard &lt;code&gt;RateLimit-*&lt;/code&gt; + &lt;code&gt;Retry-After&lt;/code&gt; headers&lt;/li&gt;
&lt;li&gt;Type-safe configuration using discriminated unions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And Redis (or external storage adapters) are planned for future releases.&lt;/p&gt;

&lt;p&gt;So you can start simple — and scale later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stress &amp;amp; Memory Testing
&lt;/h2&gt;

&lt;p&gt;Before publishing, I ran controlled stress tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~500,000 unique key simulations&lt;/li&gt;
&lt;li&gt;Continuous request churn&lt;/li&gt;
&lt;li&gt;Heap inspection under sustained load&lt;/li&gt;
&lt;li&gt;Manual GC checks using &lt;code&gt;node --expose-gc&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stable memory growth curve&lt;/li&gt;
&lt;li&gt;No memory leaks&lt;/li&gt;
&lt;li&gt;Proper cleanup of inactive keys&lt;/li&gt;
&lt;li&gt;~10MB reduction in peak memory after migrating from Record → Map&lt;/li&gt;
&lt;li&gt;Heap returning close to baseline after GC&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The focus was to predict behavior under pressure.&lt;/p&gt;




&lt;p&gt;If you’re building with Express and want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A clean API&lt;/li&gt;
&lt;li&gt;Zero dependencies&lt;/li&gt;
&lt;li&gt;Algorithm flexibility&lt;/li&gt;
&lt;li&gt;Efficient in-memory performance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/limitngin" rel="noopener noreferrer"&gt;limitngin&lt;/a&gt; is now available.&lt;/p&gt;

&lt;p&gt;Open to discussions, feedback, and contributions.&lt;/p&gt;

</description>
      <category>node</category>
      <category>express</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How I Took a Slow Node.js API from 5.7 sec 11ms Using Real Load Tests (Beginner-Friendly)</title>
      <dc:creator>Vishal Pandey</dc:creator>
      <pubDate>Mon, 24 Nov 2025 14:46:21 +0000</pubDate>
      <link>https://dev.to/0xv1shal/how-i-took-a-slow-nodejs-api-from-57-sec-11ms-using-real-load-tests-beginner-friendly-2bm2</link>
      <guid>https://dev.to/0xv1shal/how-i-took-a-slow-nodejs-api-from-57-sec-11ms-using-real-load-tests-beginner-friendly-2bm2</guid>
      <description>&lt;p&gt;Most tutorials show you “fast Node.js code,” but they never show you what happens when you actually measure performance under load.&lt;/p&gt;

&lt;p&gt;I wanted to learn real backend optimization, so I built a small URL redirect API:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GET /:slug  →  returns original URL&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Simple idea.&lt;br&gt;
But the first time I load-tested it, the API took 5.73 seconds to respond.&lt;/p&gt;

&lt;p&gt;This article is a beginner → mid-level friendly breakdown of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;what slowed my API&lt;/li&gt;
&lt;li&gt;how I optimized it &lt;/li&gt;
&lt;li&gt;what actually improved performance &lt;/li&gt;
&lt;li&gt;real k6 load test results at every step&lt;/li&gt;
&lt;li&gt;and what beginners usually misunderstand about backend latency&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;🖥️ Environment Note&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;All benchmarks, optimizations, and load tests in this article were performed on a local machine, not a server or cloud instance.&lt;br&gt;
Numbers will vary depending on hardware — but the optimization principles remain the same.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;&lt;code&gt;Stage 1 — Baseline Test (No Index, Direct MySQL Connection)&lt;/code&gt;&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Avg latency: 5.73 seconds&lt;br&gt;
Requests: ~68 RPS&lt;/p&gt;

&lt;p&gt;This was the first k6 run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvuetl7kx34ztrfftevic.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvuetl7kx34ztrfftevic.png" alt="before-slug-index" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are the baseline numbers:&lt;/p&gt;

&lt;p&gt;Why it was this slow?&lt;br&gt;
MySQL was doing a full table scan for every slug.&lt;br&gt;
No index → MySQL compares slug against every row → worst possible plan.&lt;br&gt;
At high concurrency, MySQL was simply overwhelmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;&lt;code&gt;Stage 2 — Added a Slug Index&lt;/code&gt;&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Avg latency: 27.1 ms&lt;br&gt;
Requests: ~18k RPS&lt;/p&gt;

&lt;p&gt;Index command: &lt;code&gt;CREATE INDEX idx_slug ON urls(slug);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj336m7y4qh1pqrofnvdy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj336m7y4qh1pqrofnvdy.png" alt="after-slug-index" width="800" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After adding the index:&lt;br&gt;
This alone brought latency from 5.73 seconds → 27 ms.&lt;br&gt;
This is the single biggest improvement you can ever make in a read-heavy API.&lt;br&gt;
Beginners underestimate how critical indexing is.&lt;br&gt;
No amount of Node.js code optimization can beat a proper DB index.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;&lt;code&gt;Stage 3 — Switched from mysql.createConnection to MySQL2 Connection Pool&lt;/code&gt;&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Avg latency: ~18 ms&lt;br&gt;
Before this, every request created a new MySQL connection.&lt;br&gt;
That’s expensive under load.&lt;br&gt;
So I switched to:&lt;br&gt;
&lt;code&gt;export const db = mysql2.createPool({&lt;br&gt;
  host,&lt;br&gt;
  user,&lt;br&gt;
  password,&lt;br&gt;
  database,&lt;br&gt;
  connectionLimit: 10,&lt;br&gt;
});&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After switching to pooling:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fed53spcu2z8bzngdans9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fed53spcu2z8bzngdans9.png" alt="after-pooling" width="800" height="624"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why this helps&lt;br&gt;
Reuses open connections&lt;br&gt;
Avoids handshake overhead&lt;br&gt;
Much better concurrency&lt;br&gt;
MySQL stays stable under load&lt;br&gt;
If you're using MySQL + Node.js:&lt;br&gt;
Connection pools are mandatory.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;&lt;code&gt;Stage 4 — Added Redis Cache (Final Optimization)&lt;/code&gt;&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Avg latency: 11.75 ms&lt;br&gt;
Requests: ~42k RPS&lt;br&gt;
Redis turns your slug lookup into an O(1) in-memory lookup.&lt;br&gt;
Updated handler:&lt;/p&gt;

&lt;p&gt;app.get('/:slug', async (req, res) =&amp;gt; {&lt;br&gt;
  const slug = req.params.slug;&lt;/p&gt;

&lt;p&gt;const cached = await redis.get(slug);&lt;br&gt;
  if (cached) return res.redirect(cached);&lt;/p&gt;

&lt;p&gt;const data = await db.query("SELECT url FROM urls WHERE slug = ?", [slug]);&lt;br&gt;
  if (!data[0].length) return res.status(404).send("Not found");&lt;/p&gt;

&lt;p&gt;await redis.set(slug, data[0][0].url);&lt;br&gt;
  return res.redirect(data[0][0].url);&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;k6 results after Redis:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fno94v4x5zd3dpk0osvlc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fno94v4x5zd3dpk0osvlc.png" alt="after-redis" width="800" height="516"&gt;&lt;/a&gt;&lt;br&gt;
From 5.7 seconds → 11 ms.&lt;br&gt;
This is a ~520x improvement, and nothing about the API changed except internal optimizations.&lt;/p&gt;

&lt;p&gt;🔗 GitHub Repository (Full Source Code)&lt;/p&gt;

&lt;p&gt;You can check out the entire project here:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/Termux-Dark-Dev/url-shortner-practice.git" rel="noopener noreferrer"&gt;Github Link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🙏 Thanks for Reading&lt;br&gt;
If you have questions, want to discuss backend engineering, or just want to say hi —&lt;br&gt;
I’m always open to messages.&lt;/p&gt;

</description>
      <category>node</category>
      <category>learning</category>
      <category>javascript</category>
      <category>redis</category>
    </item>
    <item>
      <title>How I Took a Slow Node.js API from 5.7 sec 11ms Using Real Load Tests (Beginner-Friendly)</title>
      <dc:creator>Vishal Pandey</dc:creator>
      <pubDate>Mon, 24 Nov 2025 14:46:21 +0000</pubDate>
      <link>https://dev.to/0xv1shal/how-i-took-a-slow-nodejs-api-from-57-sec-11ms-using-real-load-tests-beginner-friendly-3nop</link>
      <guid>https://dev.to/0xv1shal/how-i-took-a-slow-nodejs-api-from-57-sec-11ms-using-real-load-tests-beginner-friendly-3nop</guid>
      <description>&lt;p&gt;Most tutorials show you “fast Node.js code,” but they never show you what happens when you actually measure performance under load.&lt;/p&gt;

&lt;p&gt;I wanted to learn real backend optimization, so I built a small URL redirect API:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GET /:slug  →  returns original URL&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Simple idea.&lt;br&gt;
But the first time I load-tested it, the API took 5.73 seconds to respond.&lt;/p&gt;

&lt;p&gt;This article is a beginner → mid-level friendly breakdown of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;what slowed my API&lt;/li&gt;
&lt;li&gt;how I optimized it &lt;/li&gt;
&lt;li&gt;what actually improved performance &lt;/li&gt;
&lt;li&gt;real k6 load test results at every step&lt;/li&gt;
&lt;li&gt;and what beginners usually misunderstand about backend latency&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;&lt;code&gt;Stage 1 — Baseline Test (No Index, Direct MySQL Connection)&lt;/code&gt;&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Avg latency: 5.73 seconds&lt;br&gt;
Requests: ~68 RPS&lt;/p&gt;

&lt;p&gt;This was the first k6 run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvuetl7kx34ztrfftevic.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvuetl7kx34ztrfftevic.png" alt="before-slug-index" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are the baseline numbers:&lt;/p&gt;

&lt;p&gt;Why it was this slow?&lt;br&gt;
MySQL was doing a full table scan for every slug.&lt;br&gt;
No index → MySQL compares slug against every row → worst possible plan.&lt;br&gt;
At high concurrency, MySQL was simply overwhelmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;&lt;code&gt;Stage 2 — Added a Slug Index&lt;/code&gt;&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Avg latency: 27.1 ms&lt;br&gt;
Requests: ~18k RPS&lt;/p&gt;

&lt;p&gt;Index command: &lt;code&gt;CREATE INDEX idx_slug ON urls(slug);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj336m7y4qh1pqrofnvdy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj336m7y4qh1pqrofnvdy.png" alt="after-slug-index" width="800" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After adding the index:&lt;br&gt;
This alone brought latency from 5.73 seconds → 27 ms.&lt;br&gt;
This is the single biggest improvement you can ever make in a read-heavy API.&lt;br&gt;
Beginners underestimate how critical indexing is.&lt;br&gt;
No amount of Node.js code optimization can beat a proper DB index.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;&lt;code&gt;Stage 3 — Switched from mysql.createConnection to MySQL2 Connection Pool&lt;/code&gt;&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Avg latency: ~18 ms&lt;br&gt;
Before this, every request created a new MySQL connection.&lt;br&gt;
That’s expensive under load.&lt;br&gt;
So I switched to:&lt;br&gt;
&lt;code&gt;export const db = mysql2.createPool({&lt;br&gt;
  host,&lt;br&gt;
  user,&lt;br&gt;
  password,&lt;br&gt;
  database,&lt;br&gt;
  connectionLimit: 10,&lt;br&gt;
});&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After switching to pooling:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fed53spcu2z8bzngdans9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fed53spcu2z8bzngdans9.png" alt="after-pooling" width="800" height="624"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why this helps&lt;br&gt;
Reuses open connections&lt;br&gt;
Avoids handshake overhead&lt;br&gt;
Much better concurrency&lt;br&gt;
MySQL stays stable under load&lt;br&gt;
If you're using MySQL + Node.js:&lt;br&gt;
Connection pools are mandatory.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;&lt;code&gt;Stage 4 — Added Redis Cache (Final Optimization)&lt;/code&gt;&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Avg latency: 11.75 ms&lt;br&gt;
Requests: ~42k RPS&lt;br&gt;
Redis turns your slug lookup into an O(1) in-memory lookup.&lt;br&gt;
Updated handler:&lt;/p&gt;

&lt;p&gt;app.get('/:slug', async (req, res) =&amp;gt; {&lt;br&gt;
  const slug = req.params.slug;&lt;/p&gt;

&lt;p&gt;const cached = await redis.get(slug);&lt;br&gt;
  if (cached) return res.redirect(cached);&lt;/p&gt;

&lt;p&gt;const data = await db.query("SELECT url FROM urls WHERE slug = ?", [slug]);&lt;br&gt;
  if (!data[0].length) return res.status(404).send("Not found");&lt;/p&gt;

&lt;p&gt;await redis.set(slug, data[0][0].url);&lt;br&gt;
  return res.redirect(data[0][0].url);&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;k6 results after Redis:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fno94v4x5zd3dpk0osvlc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fno94v4x5zd3dpk0osvlc.png" alt="after-redis" width="800" height="516"&gt;&lt;/a&gt;&lt;br&gt;
From 5.7 seconds → 11 ms.&lt;br&gt;
This is a ~520x improvement, and nothing about the API changed except internal optimizations.&lt;/p&gt;

&lt;p&gt;🔗 GitHub Repository (Full Source Code)&lt;/p&gt;

&lt;p&gt;You can check out the entire project here:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/Termux-Dark-Dev/url-shortner-practice.git" rel="noopener noreferrer"&gt;Github Link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🙏 Thanks for Reading&lt;br&gt;
If you have questions, want to discuss backend engineering, or just want to say hi —&lt;br&gt;
I’m always open to messages.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>performance</category>
      <category>node</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
