<?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: Piotrek Karasinski</title>
    <description>The latest articles on DEV Community by Piotrek Karasinski (@piotrek1372).</description>
    <link>https://dev.to/piotrek1372</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%2F4009993%2Fd859a1d7-69b2-4ac0-b4ac-683ae193d0d5.jpg</url>
      <title>DEV Community: Piotrek Karasinski</title>
      <link>https://dev.to/piotrek1372</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/piotrek1372"/>
    <language>en</language>
    <item>
      <title>Signals in Linux — SIGKILL, SIGTERM, SIGCHLD and the Anatomy of a Handler</title>
      <dc:creator>Piotrek Karasinski</dc:creator>
      <pubDate>Sat, 11 Jul 2026 15:13:05 +0000</pubDate>
      <link>https://dev.to/piotrek1372/signals-in-linux-sigkill-sigterm-sigchld-and-the-anatomy-of-a-handler-23pg</link>
      <guid>https://dev.to/piotrek1372/signals-in-linux-sigkill-sigterm-sigchld-and-the-anatomy-of-a-handler-23pg</guid>
      <description>&lt;p&gt;You send &lt;code&gt;SIGTERM&lt;/code&gt; to a process and it keeps running. You send &lt;code&gt;SIGKILL&lt;/code&gt; and it vanishes instantly — but leaves an entry in the process table behind. Your &lt;code&gt;SIGINT&lt;/code&gt; handler catches the signal sometimes and misses it other times, depending on which syscall the process happened to be sitting in. Signals look like a simple mechanism — send a number, the process reacts — but underneath they're one of the most subtle corners of the kernel interface, full of race conditions and bugs that refuse to reproduce.&lt;/p&gt;

&lt;p&gt;This article takes signals apart: what delivery actually means, why some signals can't be caught, how &lt;code&gt;SIGCHLD&lt;/code&gt; ties into &lt;a href="https://devmindset.dev/en/what-fork-in-linux-really-does-and-why-it-matters-more-than-you-think/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;the fork() mechanism and zombie processes&lt;/a&gt;, and which operations are safe inside a handler versus which ones will blow your process up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a signal is — from the kernel's perspective
&lt;/h2&gt;

&lt;p&gt;A signal is an asynchronous notification delivered to a process by the kernel. It isn't a function call or a queued message — it's a bit set in the process structure and a (potential) interruption of its normal execution. Every process carries two key bitmasks in its descriptor: &lt;strong&gt;pending&lt;/strong&gt; (signals delivered but not yet handled) and &lt;strong&gt;blocked&lt;/strong&gt; (signals temporarily held off by the process).&lt;/p&gt;

&lt;p&gt;When process A calls &lt;code&gt;kill(pid_B, SIGTERM)&lt;/code&gt;, the kernel doesn't immediately hand control to process B. It sets the &lt;code&gt;SIGTERM&lt;/code&gt; bit in B's pending mask and returns. Actual &lt;strong&gt;delivery&lt;/strong&gt; — the moment B interrupts what it was doing and runs the signal's action — happens only at the next transition from kernel mode back to user mode: typically on return from a syscall or from a timer interrupt.&lt;/p&gt;

&lt;p&gt;This distinction between a signal being &lt;em&gt;generated&lt;/em&gt; and being &lt;em&gt;delivered&lt;/em&gt; is the source of most counterintuitive behavior. A process blocked in a long syscall can hold a signal pending for a while before it gets handled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signal dispositions: three possible reactions
&lt;/h2&gt;

&lt;p&gt;For each signal, a process has one of three &lt;strong&gt;dispositions&lt;/strong&gt; set, which decides what happens on delivery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default action&lt;/strong&gt; (&lt;code&gt;SIG_DFL&lt;/code&gt;) — kernel-defined behavior: terminate, terminate with a core dump, ignore, or stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignore&lt;/strong&gt; (&lt;code&gt;SIG_IGN&lt;/code&gt;) — the signal is discarded on delivery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handler&lt;/strong&gt; — a user function registered via &lt;code&gt;sigaction()&lt;/code&gt;, invoked in the process context at the moment of delivery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Default actions for the most common signals look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Number&lt;/th&gt;
&lt;th&gt;Default action&lt;/th&gt;
&lt;th&gt;Catchable?&lt;/th&gt;
&lt;th&gt;Typical source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SIGTERM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Terminate&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Polite shutdown request (&lt;code&gt;kill&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SIGKILL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Terminate&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Forced process kill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SIGINT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Terminate&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Ctrl+C in a terminal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SIGSEGV&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Core dump&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Memory protection violation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SIGCHLD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;Ignore&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Child process state change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SIGSTOP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Suspend process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SIGHUP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Terminate&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Terminal hangup / config reload&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  SIGKILL and SIGSTOP — why you can't catch them
&lt;/h2&gt;

&lt;p&gt;Two signals — &lt;code&gt;SIGKILL&lt;/code&gt; (9) and &lt;code&gt;SIGSTOP&lt;/code&gt; (19) — are uncatchable by design. You can't register a handler for them, can't set them to be ignored, can't block them with a mask. The kernel handles them itself, without ever returning control to the process.&lt;/p&gt;

&lt;p&gt;This is a deliberate design decision. If a process could intercept &lt;code&gt;SIGKILL&lt;/code&gt;, it could ignore a termination request and become unkillable. &lt;code&gt;SIGKILL&lt;/code&gt; is the operating system's last-resort mechanism — the guarantee that an administrator can always reclaim control over resources. Trying to install a handler fails with &lt;code&gt;EINVAL&lt;/code&gt;:&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="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;signal.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;errno.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;string.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sigaction&lt;/span&gt; &lt;span class="n"&gt;sa&lt;/span&gt; &lt;span class="o"&gt;=&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;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sa_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SIG_IGN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sigaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SIGKILL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// errno == EINVAL: SIGKILL cannot have a handler&lt;/span&gt;
        &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"sigaction(SIGKILL): %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strerror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errno&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="mi"&gt;0&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 practical consequence: if a process doesn't respond to &lt;code&gt;SIGTERM&lt;/code&gt;, it means its handler hung, it's stuck in an uninterruptible syscall (state &lt;code&gt;D&lt;/code&gt; — uninterruptible sleep), or it's deliberately ignoring the signal. Only then do you reach for &lt;code&gt;SIGKILL&lt;/code&gt;. But a process in state &lt;code&gt;D&lt;/code&gt; won't respond even to &lt;code&gt;SIGKILL&lt;/code&gt; until the syscall finishes — which is why you sometimes see processes that &lt;code&gt;kill -9&lt;/code&gt; won't touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  SIGCHLD and zombie processes
&lt;/h2&gt;

&lt;p&gt;This is where signals meet process management. When a child process terminates, the kernel sends &lt;code&gt;SIGCHLD&lt;/code&gt; to the parent and — crucially — keeps a minimal entry for the child in the process table. That entry holds the exit code, resource usage statistics, and the PID. A process in this state is a &lt;strong&gt;zombie&lt;/strong&gt; (state &lt;code&gt;Z&lt;/code&gt;): it no longer runs any code, but it still occupies a slot in the process table.&lt;/p&gt;

&lt;p&gt;The entry disappears only when the parent calls &lt;code&gt;wait()&lt;/code&gt; or &lt;code&gt;waitpid()&lt;/code&gt; and collects the exit code — an operation called &lt;em&gt;reaping&lt;/em&gt;. I covered the same topic in detail from the process-creation side in the article on &lt;a href="https://devmindset.dev/en/what-fork-in-linux-really-does-and-why-it-matters-more-than-you-think/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;what fork() in Linux really does&lt;/a&gt;; here we look at it from the signal side.&lt;/p&gt;

&lt;p&gt;The default disposition of &lt;code&gt;SIGCHLD&lt;/code&gt; is to ignore it — but ignoring the signal does &lt;em&gt;not&lt;/em&gt; reap zombies. You have to either actively call &lt;code&gt;wait()&lt;/code&gt;, or explicitly set the disposition to &lt;code&gt;SIG_IGN&lt;/code&gt; via &lt;code&gt;sigaction()&lt;/code&gt; (which on Linux triggers automatic reaping), or handle &lt;code&gt;SIGCHLD&lt;/code&gt; in a handler. A correct handler looks like this:&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="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;signal.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;sys/wait.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;unistd.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="c1"&gt;// Reaper: collects ALL terminated child processes.&lt;/span&gt;
&lt;span class="c1"&gt;// The loop is mandatory — a single SIGCHLD can represent&lt;/span&gt;
&lt;span class="c1"&gt;// multiple terminated children (signals don't queue).&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sigchld_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;signo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;saved_errno&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errno&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// handler must preserve errno&lt;/span&gt;
    &lt;span class="n"&gt;pid_t&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;waitpid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WNOHANG&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// reaped the process with this PID&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;errno&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;saved_errno&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;install_reaper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sigaction&lt;/span&gt; &lt;span class="n"&gt;sa&lt;/span&gt; &lt;span class="o"&gt;=&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;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sa_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sigchld_handler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;sigemptyset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sa_mask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sa_flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SA_RESTART&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;SA_NOCLDSTOP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// restart syscalls, ignore stop/cont&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sigaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SIGCHLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&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 &lt;code&gt;while&lt;/code&gt; loop with &lt;code&gt;WNOHANG&lt;/code&gt; is there for a reason. Standard signals (below 32) &lt;strong&gt;do not queue&lt;/strong&gt;: if three children terminate in quick succession before the handler gets a chance to run, you receive a single &lt;code&gt;SIGCHLD&lt;/code&gt;, not three. A handler that calls &lt;code&gt;waitpid()&lt;/code&gt; only once reaps one child and leaves two zombies behind. The loop collects everything available at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async-signal-safety: what's allowed in a handler
&lt;/h2&gt;

&lt;p&gt;A signal handler runs asynchronously — it can interrupt the process's main code at any point, including in the middle of a standard library call. This leads to the most common signal-handling bug: calling a function that isn't &lt;strong&gt;async-signal-safe&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Imagine the process is in the middle of &lt;code&gt;malloc()&lt;/code&gt;, which is modifying internal heap structures and holding a lock. A signal arrives, the handler calls &lt;code&gt;printf()&lt;/code&gt; — which internally also calls &lt;code&gt;malloc()&lt;/code&gt;. The second allocation tries to take the same lock the interrupted code is holding. The result is a deadlock or heap corruption. The same applies to any function that operates on shared global state.&lt;/p&gt;

&lt;p&gt;POSIX defines a narrow list of functions guaranteed to be async-signal-safe. The most important ones (&lt;em&gt;safe vs. unsafe in a handler&lt;/em&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Safe in a handler&lt;/th&gt;
&lt;th&gt;UNSAFE — never in a handler&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;write()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;printf()&lt;/code&gt;, &lt;code&gt;fprintf()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;read()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;malloc()&lt;/code&gt;, &lt;code&gt;free()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;waitpid()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fopen()&lt;/code&gt;, &lt;code&gt;fclose()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_exit()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;exit()&lt;/code&gt; (runs atexit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;signal()&lt;/code&gt;, &lt;code&gt;sigaction()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;most &lt;code&gt;stdio&lt;/code&gt; functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kill()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;locale functions (&lt;code&gt;setlocale&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The practical pattern: keep the handler as short as possible. Ideally it just sets a &lt;code&gt;volatile sig_atomic_t&lt;/code&gt; flag and returns, while all the logic runs in the program's main loop after checking the flag. If the handler must log something, it uses &lt;code&gt;write()&lt;/code&gt; directly on a descriptor, never &lt;code&gt;printf()&lt;/code&gt;.&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="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;signal.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;unistd.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="c1"&gt;// Flag modified by the handler, read by the main loop.&lt;/span&gt;
&lt;span class="c1"&gt;// volatile: the compiler can't cache it in a register.&lt;/span&gt;
&lt;span class="c1"&gt;// sig_atomic_t: guarantees atomic read/write.&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="kt"&gt;sig_atomic_t&lt;/span&gt; &lt;span class="n"&gt;shutdown_requested&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;handle_term&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;signo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;shutdown_requested&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// optional log — write() is async-signal-safe&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SIGTERM received, shutting down&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STDERR_FILENO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sigaction&lt;/span&gt; &lt;span class="n"&gt;sa&lt;/span&gt; &lt;span class="o"&gt;=&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;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sa_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handle_term&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;sigemptyset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sa_mask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sa_flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SA_RESTART&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;sigaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SIGTERM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;sa&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;shutdown_requested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// main loop: this is where all the real work happens,&lt;/span&gt;
        &lt;span class="c1"&gt;// including resource cleanup once the flag is detected&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// graceful shutdown outside the handler — anything is allowed here&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&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;h2&gt;
  
  
  SA_RESTART and interrupted syscalls
&lt;/h2&gt;

&lt;p&gt;There's one more mechanism that surprises people on first contact. When a signal is delivered during a blocking syscall — say, a &lt;code&gt;read()&lt;/code&gt; waiting for data on a socket — the syscall may be interrupted and return the error &lt;code&gt;EINTR&lt;/code&gt; instead of completing the operation.&lt;/p&gt;

&lt;p&gt;Without proper handling, this leads to nasty bugs: the code assumes &lt;code&gt;read()&lt;/code&gt; will either return data or block until it can, and instead it gets &lt;code&gt;EINTR&lt;/code&gt; and — if the programmer doesn't check for it — treats it as a fatal error. You have two options. The first is the &lt;code&gt;SA_RESTART&lt;/code&gt; flag in &lt;code&gt;sigaction()&lt;/code&gt;, which tells the kernel to automatically resume interrupted syscalls. The second is explicitly checking for &lt;code&gt;EINTR&lt;/code&gt; and retrying the operation:&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="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;unistd.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;errno.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="c1"&gt;// Wrapper resilient to signal interruption.&lt;/span&gt;
&lt;span class="c1"&gt;// Retries read() on EINTR instead of treating it as an error.&lt;/span&gt;
&lt;span class="kt"&gt;ssize_t&lt;/span&gt; &lt;span class="nf"&gt;read_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;ssize_t&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;errno&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;EINTR&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;n&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;Note: &lt;code&gt;SA_RESTART&lt;/code&gt; doesn't work for all syscalls. Some of them — including those tied to waiting for events, like certain &lt;code&gt;poll()&lt;/code&gt; variants or timer operations — return &lt;code&gt;EINTR&lt;/code&gt; regardless of the flag. That's why defensive network code should handle &lt;code&gt;EINTR&lt;/code&gt; explicitly anyway, even with &lt;code&gt;SA_RESTART&lt;/code&gt; set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnostics: inspecting signals in a running process
&lt;/h2&gt;

&lt;p&gt;When a process behaves strangely toward signals, you don't have to guess. The file &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/status&lt;/code&gt; shows the current signal masks in hexadecimal:&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="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'Sig|Shd'&lt;/span&gt; /proc/&lt;span class="si"&gt;$(&lt;/span&gt;pgrep &lt;span class="nt"&gt;-n&lt;/span&gt; nginx&lt;span class="si"&gt;)&lt;/span&gt;/status
&lt;span class="gp"&gt;SigQ:   0/31228          #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;queued signals / limit
&lt;span class="gp"&gt;SigPnd: 0000000000000000 #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pending &lt;span class="k"&gt;for &lt;/span&gt;this thread
&lt;span class="gp"&gt;SigBlk: 0000000000000000 #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;blocked &lt;span class="o"&gt;(&lt;/span&gt;mask&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;SigIgn: 0000000000001000 #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ignored
&lt;span class="gp"&gt;SigCgt: 0000000180014a07 #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;caught &lt;span class="o"&gt;(&lt;/span&gt;have a handler&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each bit corresponds to one signal (bit 0 = signal 1). The &lt;code&gt;SigCgt&lt;/code&gt; mask tells you which signals the process handles with its own handler — useful when you want to verify an application actually responds to &lt;code&gt;SIGHUP&lt;/code&gt; (config reload) before sending it in production.&lt;/p&gt;

&lt;p&gt;To trace signal delivery in real time, use &lt;code&gt;strace&lt;/code&gt; with a signal filter. It shows the exact moment of delivery and whether the handler was invoked — a tool I cover in the article on &lt;a href="https://devmindset.dev/en/strace-as-a-debugging-primitive-when-the-stack-trace-lies/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;reading syscalls with strace when the stack trace lies&lt;/a&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="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;strace &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;signal &lt;span class="nt"&gt;-p&lt;/span&gt; &amp;lt;pid&amp;gt;
&lt;span class="go"&gt;--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=4821} ---
rt_sigreturn({mask=[]})  = 202
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;you can see SIGTERM delivered and the &lt;span class="k"&gt;return &lt;/span&gt;from the handler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Signals in Linux aren't a simple "send a number, the process reacts" mechanism. They're an asynchronous interface with three layers of subtlety: the distinction between generation and delivery, the constraints on what you're allowed to do in a handler, and the non-queuing of standard signals. The most common bugs come from ignoring those three things — a handler calling &lt;code&gt;printf()&lt;/code&gt;, a reaper without a &lt;code&gt;while&lt;/code&gt; loop, network code that doesn't handle &lt;code&gt;EINTR&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rule of thumb: treat a signal handler like a hardware interrupt routine. The less you do in it, the fewer things can go wrong. Set a flag, return, handle the rest in the main loop — and never assume one signal means one event.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devmindset.dev/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;devmindset.dev&lt;/a&gt; — Linux internals, systems programming, and the self-taught developer mindset.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Related deep-dives:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devmindset.dev/en/what-fork-in-linux-really-does-and-why-it-matters-more-than-you-think/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;What fork() in Linux really does&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devmindset.dev/en/malloc-internals-why-free-doesnt-return-memory-to-the-system/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;malloc internals — why free() doesn't return memory to the system&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>c</category>
      <category>systems</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building an MCP Server in Python — Architecture, FastMCP, and Production Code</title>
      <dc:creator>Piotrek Karasinski</dc:creator>
      <pubDate>Fri, 03 Jul 2026 09:42:55 +0000</pubDate>
      <link>https://dev.to/piotrek1372/building-an-mcp-server-in-python-architecture-fastmcp-and-production-code-3co5</link>
      <guid>https://dev.to/piotrek1372/building-an-mcp-server-in-python-architecture-fastmcp-and-production-code-3co5</guid>
      <description>&lt;p&gt;I run my blog, devmindset.dev, through a custom MCP server.**. Publishing posts, updating SEO metadata, assigning categories — all of it goes through a protocol that, a year ago, didn't exist in production form. So I'm not writing about MCP from the documentation's point of view, but from the point of view of someone who stood up a working server and operates it daily. This isn't another "hello world" — it's protocol architecture, deliberate design decisions, and production code in Python.&lt;/p&gt;

&lt;p&gt;The Model Context Protocol (MCP) is an open standard introduced by Anthropic in November 2024, now developed under the Agentic AI Foundation (Linux Foundation). The stable spec is dated 2025-11-25, and the largest revision since launch lands on July 28, 2026 — more on that shortly, because it changes how you design transport. But let's start with the question most tutorials skip: &lt;em&gt;what does this protocol actually solve?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What MCP actually solves
&lt;/h2&gt;

&lt;p&gt;The problem MCP addresses is combinatorial. You have &lt;em&gt;M&lt;/em&gt; LLM applications (Claude Desktop, Cursor, VS Code, ChatGPT) and &lt;em&gt;N&lt;/em&gt; external systems (a database, GitHub, an internal API, WordPress). Without a shared standard, every pair needs a bespoke integration — that's &lt;strong&gt;M×N&lt;/strong&gt; implementations, each with its own format, its own auth, its own maintenance burden. MCP collapses that into &lt;strong&gt;M+N&lt;/strong&gt;: you write the server once, and every compliant client can discover and use it without a line of code on its side.&lt;/p&gt;

&lt;p&gt;Mechanically, MCP sits on &lt;strong&gt;JSON-RPC 2.0&lt;/strong&gt; and defines three roles. The &lt;strong&gt;host&lt;/strong&gt; is the LLM application that coordinates everything. The &lt;strong&gt;client&lt;/strong&gt; is instantiated by the host — one client per server. The &lt;strong&gt;server&lt;/strong&gt; provides context and capabilities. It's deliberately modeled on the Language Server Protocol: just as LSP standardized language support across editors, MCP standardizes wiring tools and data into the AI ecosystem.&lt;/p&gt;

&lt;p&gt;And here's the first misconception to defuse: &lt;strong&gt;MCP is not "function calling."&lt;/strong&gt; Function calling is a single-vendor mechanism — you define functions in your code and one specific model invokes them. MCP is a transport protocol and a negotiation layer: the server advertises its capabilities, the client discovers them at runtime, and versions are negotiated at initialization. Function calling lives inside one application; an MCP server is reusable across any host.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three primitives: tools, resources, and prompts
&lt;/h2&gt;

&lt;p&gt;An MCP server exposes capabilities through three primitives. Conflating them is the most common design mistake — each has a different contract and a different use.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Primitive&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Who controls it&lt;/th&gt;
&lt;th&gt;Use for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tool&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Executable action with validation and logic&lt;/td&gt;
&lt;td&gt;Model (calls when needed)&lt;/td&gt;
&lt;td&gt;Side-effecting operations, complex logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resource&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read-only data under a URI template&lt;/td&gt;
&lt;td&gt;Application / host&lt;/td&gt;
&lt;td&gt;Static or semi-static context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prompt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reusable template&lt;/td&gt;
&lt;td&gt;User (selects deliberately)&lt;/td&gt;
&lt;td&gt;Repeatable, structured instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rule of thumb: &lt;strong&gt;Tool&lt;/strong&gt; when you need input validation and business logic ("create a post with title X and status Y"). &lt;strong&gt;Resource&lt;/strong&gt; when you expose data under a simple parameter ("the contents of document Z"). &lt;strong&gt;Prompt&lt;/strong&gt; when you hand the user a ready-made, parameterized scenario. In practice, most servers start and end with tools — the rest is context optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transport: stdio vs Streamable HTTP
&lt;/h2&gt;

&lt;p&gt;MCP defines two transports, and choosing between them is the first architectural decision when building an MCP server.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;stdio&lt;/th&gt;
&lt;th&gt;Streamable HTTP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Location&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Local, same machine&lt;/td&gt;
&lt;td&gt;Remote, over HTTPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Run model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Host subprocess&lt;/td&gt;
&lt;td&gt;Network service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Clients&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One (process)&lt;/td&gt;
&lt;td&gt;Many concurrently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Authorization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inherited from OS&lt;/td&gt;
&lt;td&gt;OAuth 2.1 / OIDC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Use for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CLI tools, local integrations&lt;/td&gt;
&lt;td&gt;Production servers, SaaS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And here's the change most material hasn't caught up to yet. The &lt;strong&gt;2026-07-28&lt;/strong&gt; revision (currently a release candidate) removes the protocol-level session — the &lt;code&gt;Mcp-Session-Id&lt;/code&gt; header is gone (SEP-2567). Protocol version, client info, and capabilities now travel in &lt;code&gt;_meta&lt;/code&gt; on every request, and a new &lt;code&gt;server/discover&lt;/code&gt; method lets the client fetch server capabilities on demand. The practical consequence: &lt;strong&gt;any request can land on any server instance&lt;/strong&gt;. The sticky routing and shared session stores that horizontal deployments used to need are no longer required at the protocol layer.&lt;/p&gt;

&lt;p&gt;This doesn't mean your application has to be stateless. A server that needs state across calls does what HTTP APIs have always done: mint an explicit handle (say, a &lt;code&gt;basket_id&lt;/code&gt;) from one tool and have the model pass it back as an ordinary argument on later calls. So design for stateless transport from the start — it's the direction the protocol is heading, and the cheaper path to scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal production MCP server — FastMCP
&lt;/h2&gt;

&lt;p&gt;The official Python SDK ships FastMCP — a high-level framework that generates the input schema from signatures and docstrings, integrates Pydantic validation, and registers tools with a decorator. Below is not a "hello world" but a skeleton with everything that separates a toy from production code: a Pydantic model for validation, behavior annotations, async I/O, error handling, and full typing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ConfigDict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;

&lt;span class="c1"&gt;# Name the server per the {service}_mcp convention
&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weather_mcp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;API_BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example-weather.com/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ForecastInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Input validation for a forecast query.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;model_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ConfigDict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;str_strip_whitespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;forbid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# reject unknown fields
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;City name, e.g. &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Wrocław&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;min_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Forecast horizon in days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;ge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_handle_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Consistent, actionable error messages for the model.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPStatusError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: city not found. Check the spelling of the name.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: rate limit exceeded. Wait before retrying.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: API returned status &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeoutException&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: request timed out. Please try again.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: unexpected exception: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_forecast&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;annotations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Get weather forecast&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;readOnlyHint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# does not modify state
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openWorldHint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# reaches an external API
&lt;/span&gt;    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_forecast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ForecastInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return a weather forecast for a city.

    Args:
        params: validated input (city, days).
    Returns:
        str: a formatted forecast or an actionable error message.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WEATHER_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Config error: WEATHER_API_KEY is missing from the environment.&lt;/span&gt;&lt;span class="sh"&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;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/forecast&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_handle_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Forecast for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; days):&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;forecast&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;temp_c&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;°C, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;condition&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mcp&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="c1"&gt;# stdio transport (default)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Several things here are deliberate. The Pydantic model with &lt;code&gt;extra="forbid"&lt;/code&gt; rejects unknown fields instead of silently ignoring them. The decorator annotations (&lt;code&gt;readOnlyHint&lt;/code&gt;, &lt;code&gt;openWorldHint&lt;/code&gt;) are signals to the host. All I/O is async. And the secret comes from an environment variable, not the code — which I'll come back to under security.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error handling that helps the model
&lt;/h2&gt;

&lt;p&gt;Look at the &lt;code&gt;_handle_error&lt;/code&gt; function above. This isn't cosmetics. An error message in an MCP server is read by the model, not by a human staring at logs — and it decides whether the model recovers the call sensibly or gets stuck. "Error 404" says nothing; "city not found, check the spelling" tells the model what to do next. Treat every message as a recovery instruction, not a log line.&lt;/p&gt;

&lt;p&gt;It's the same discipline as &lt;a href="https://devmindset.dev/en/debugging-like-a-developer/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;debugging as a process of deduction rather than guessing&lt;/a&gt; — a precise signal instead of noise shortens the path to the cause. The difference is that here the recipient of the signal is a model planning its next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security: why tool descriptions are untrusted
&lt;/h2&gt;

&lt;p&gt;The MCP spec says it plainly: tools represent arbitrary code execution and must be treated with appropriate caution. Moreover — &lt;strong&gt;descriptions of tool behavior, including annotations, are untrusted unless they come from a trusted server&lt;/strong&gt;. This is not a formality. A malicious server can smuggle instructions into a tool description or into a tool's result that the model treats as a command — that's prompt injection via tool output.&lt;/p&gt;

&lt;p&gt;The consequences for you as a server author are concrete. Keep secrets in environment variables, never in code or descriptions (you can see it above — &lt;code&gt;WEATHER_API_KEY&lt;/code&gt; from &lt;code&gt;os.environ&lt;/code&gt;). For remote transport use OAuth 2.1 / OIDC — the 2026-07-28 revision aligns authorization more closely with OAuth and OpenID Connect, and the Enterprise-Managed Authorization extension is now stable. Validate every input with Pydantic, because the model can pass anything. And set annotations honestly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Annotation&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;readOnlyHint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tool does not modify state&lt;/td&gt;
&lt;td&gt;Fetch a forecast, read a post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;destructiveHint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Irreversible operation&lt;/td&gt;
&lt;td&gt;Delete a resource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;idempotentHint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Repeating changes nothing&lt;/td&gt;
&lt;td&gt;Set a value to X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openWorldHint&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reaches external systems&lt;/td&gt;
&lt;td&gt;Query a weather API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The host builds user-consent flows on these signals. A lied-about annotation (say, &lt;code&gt;readOnlyHint&lt;/code&gt; on a tool that deletes data) isn't just bad code — it breaks the security contract the entire MCP trust model rests on.&lt;/p&gt;

&lt;h2&gt;
  
  
  State, concurrency, and scaling
&lt;/h2&gt;

&lt;p&gt;A production server handles many clients at once, and every tool does I/O — a call to an API, a database, a disk. That's why all the code is async (&lt;code&gt;async def&lt;/code&gt;, &lt;code&gt;httpx.AsyncClient&lt;/code&gt;): one process serves many concurrent calls without blocking, because while waiting on a network response the event loop switches to another task.&lt;/p&gt;

&lt;p&gt;This is exactly the same I/O-scaling problem solved underneath by &lt;a href="https://devmindset.dev/en/epoll-vs-io_uring-when-the-event-loop-isnt-enough/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;epoll and io_uring, when the event loop isn't enough&lt;/a&gt; — the "one thread per connection" model doesn't scale indefinitely. An MCP server over Streamable HTTP sits on the same layer: async isn't an ornament, it's the condition for serving many clients on one instance. And thanks to the 2026-07-28 stateless core, horizontal scaling comes down to standing up more instances behind a load balancer — no sticky sessions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Local — stdio (default)
&lt;/span&gt;&lt;span class="n"&gt;mcp&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="c1"&gt;# Remote — Streamable HTTP, scales horizontally
&lt;/span&gt;&lt;span class="n"&gt;mcp&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="n"&gt;transport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streamable_http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Building an MCP server that isn't a toy comes down to a few deliberate decisions: transport choice (stdio locally, Streamable HTTP in production), the right primitive (tool vs resource vs prompt), Pydantic validation, actionable errors, secrets in the environment, and honest annotations. FastMCP takes the boilerplate off your hands, but architecture and security stay on yours.&lt;/p&gt;

&lt;p&gt;One more thing, and it's fresh: design for statelessness. The 2026-07-28 revision makes transport sessionless by default, and that's the cheapest path to scale the protocol has ever offered. An MCP server written today around explicit state handles instead of sessions will survive that change without a rewrite. This is the first post in a series on MCP — the next ones go deeper into security and advanced patterns.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devmindset.dev/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;devmindset.dev&lt;/a&gt; — Linux internals, systems programming, and the self-taught developer mindset.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Related deep-dives:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devmindset.dev/en/what-fork-in-linux-really-does-and-why-it-matters-more-than-you-think/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;What fork() in Linux really does&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devmindset.dev/en/signals-in-linux-sigkill-sigterm-sigchld-handler/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;Signals in Linux — SIGKILL, SIGTERM, SIGCHLD and the anatomy of a handler&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>architecture</category>
      <category>llm</category>
    </item>
    <item>
      <title>malloc internals — Why free() Doesn't Return Memory to the System</title>
      <dc:creator>Piotrek Karasinski</dc:creator>
      <pubDate>Thu, 02 Jul 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/piotrek1372/malloc-internals-why-free-doesnt-return-memory-to-the-system-33pc</link>
      <guid>https://dev.to/piotrek1372/malloc-internals-why-free-doesnt-return-memory-to-the-system-33pc</guid>
      <description>&lt;h2&gt;
  
  
  Why Your Process's RSS Grows and Doesn't Shrink
&lt;/h2&gt;

&lt;p&gt;The classic moment of confusion: a program allocates 2 GB, processes the data, frees everything via &lt;code&gt;free()&lt;/code&gt; — and &lt;code&gt;htop&lt;/code&gt; still shows 2 GB of resident memory (RSS). A leak? No. It's the designed behavior of the &lt;strong&gt;allocator&lt;/strong&gt;, which most developers mistake for a bug because they don't understand what &lt;code&gt;malloc()&lt;/code&gt; and &lt;code&gt;free()&lt;/code&gt; actually do under the hood.&lt;/p&gt;

&lt;p&gt;In the vast majority of cases, &lt;code&gt;free()&lt;/code&gt; &lt;strong&gt;does not return memory to the operating system&lt;/strong&gt;. It returns it to the &lt;strong&gt;allocator&lt;/strong&gt; — the intermediate layer between your code and the kernel that manages memory pools to avoid costly syscalls on every allocation. Understanding this layer is the difference between "we have a memory leak" and "that's arena fragmentation, RSS won't shrink, but it isn't a leak."&lt;/p&gt;

&lt;p&gt;This article dissects the internals of &lt;strong&gt;malloc&lt;/strong&gt; in glibc: arenas, bins, chunks, the &lt;code&gt;brk&lt;/code&gt; vs &lt;code&gt;mmap&lt;/code&gt; boundary, the mechanics of fragmentation, and — crucial for production decisions — when &lt;code&gt;jemalloc&lt;/code&gt; or &lt;code&gt;tcmalloc&lt;/code&gt; beats the default glibc allocator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Intermediate Layer: Why We Don't Call the Kernel Directly
&lt;/h2&gt;

&lt;p&gt;The kernel manages memory at &lt;strong&gt;page&lt;/strong&gt; granularity (usually 4 KB). If every &lt;code&gt;malloc(16)&lt;/code&gt; required a syscall to the kernel, the cost would be absurd — a syscall is ~100–500 ns, and a typical program does millions of allocations. The &lt;strong&gt;allocator&lt;/strong&gt; solves this by requesting memory from the kernel &lt;em&gt;in bulk&lt;/em&gt; and dividing it into small pieces in userspace.&lt;/p&gt;

&lt;p&gt;Two syscalls supply memory from the kernel:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Use in glibc malloc&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;brk&lt;/code&gt; / &lt;code&gt;sbrk&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Moves the data segment boundary (program break)&lt;/td&gt;
&lt;td&gt;Small allocations (&amp;lt; 128 KB), main arena&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mmap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maps anonymous pages anywhere in the address space&lt;/td&gt;
&lt;td&gt;Large allocations (≥ 128 KB) and thread arenas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# See both mechanisms in action — strace on a simple program&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;strace &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;brk,mmap ./program 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;head
&lt;/span&gt;brk&lt;span class="o"&gt;(&lt;/span&gt;NULL&lt;span class="o"&gt;)&lt;/span&gt;                &lt;span class="o"&gt;=&lt;/span&gt; 0x55a3c2a00000        &lt;span class="c"&gt;# current program break&lt;/span&gt;
brk&lt;span class="o"&gt;(&lt;/span&gt;0x55a3c2a21000&lt;span class="o"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; 0x55a3c2a21000        &lt;span class="c"&gt;# arena extension (132 KB)&lt;/span&gt;
mmap&lt;span class="o"&gt;(&lt;/span&gt;NULL, 2101248, PROT_READ|PROT_WRITE, ...&lt;span class="o"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;# large allocation (2 MB)&lt;/span&gt;
                         &lt;span class="o"&gt;=&lt;/span&gt; 0x7f3a8c000000

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key threshold is &lt;code&gt;M_MMAP_THRESHOLD&lt;/code&gt; (default 128 KB). Allocations below it come from the arena managed by &lt;code&gt;brk&lt;/code&gt;; above it — directly via &lt;a href="https://devmindset.dev/en/mmap-when-memory-mapped-i-o-beats-read-and-write/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;memory mapping (mmap)&lt;/a&gt;. This distinction has fundamental consequences for whether memory ever returns to the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of a Chunk — How malloc Remembers Sizes
&lt;/h2&gt;

&lt;p&gt;When you call &lt;code&gt;malloc(100)&lt;/code&gt;, you get a pointer to 100 bytes — but the allocator reserved more. Every memory block is a &lt;strong&gt;chunk&lt;/strong&gt; with a metadata header just before the returned pointer:&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="cm"&gt;/* Simplified chunk structure in glibc (ptmalloc2) */&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;malloc_chunk&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;size_t&lt;/span&gt;      &lt;span class="n"&gt;prev_size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="cm"&gt;/* size of the previous chunk, IF free */&lt;/span&gt;
    &lt;span class="kt"&gt;size_t&lt;/span&gt;      &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="cm"&gt;/* size of this chunk + 3 flag bits */&lt;/span&gt;

    &lt;span class="cm"&gt;/* The fields below are used ONLY when the chunk is free: */&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;malloc_chunk&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="cm"&gt;/* forward pointer in the free list */&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;malloc_chunk&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;bk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="cm"&gt;/* backward pointer in the free list */&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/* The three lowest bits of 'size' are flags (chunks 8B-aligned): */&lt;/span&gt;
&lt;span class="cm"&gt;/* PREV_INUSE     (0x1) — whether the previous chunk is in use     */&lt;/span&gt;
&lt;span class="cm"&gt;/* IS_MMAPPED     (0x2) — whether the chunk comes from mmap         */&lt;/span&gt;
&lt;span class="cm"&gt;/* NON_MAIN_ARENA (0x4) — whether the chunk belongs to a thread arena */&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A clever trick: when a chunk is &lt;strong&gt;in use&lt;/strong&gt;, the &lt;code&gt;fd&lt;/code&gt;/&lt;code&gt;bk&lt;/code&gt; fields aren't needed, so that area holds user data. When the chunk is &lt;strong&gt;free&lt;/strong&gt;, those same bytes store list pointers. This is why a &lt;a href="https://devmindset.dev/en/anatomy-of-a-segfault-from-mmu-through-kernel-to-gdb-core-dump/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;use-after-free that overwrites a freed chunk&lt;/a&gt; corrupts the allocator's lists — it writes over &lt;code&gt;fd&lt;/code&gt;/&lt;code&gt;bk&lt;/code&gt;, which on the next allocation leads to a write to an arbitrary address (the classic &lt;em&gt;fastbin dup&lt;/em&gt; exploitation vector).&lt;/p&gt;

&lt;h2&gt;
  
  
  Bins — The Structure for Managing Free Chunks
&lt;/h2&gt;

&lt;p&gt;Freed chunks aren't immediately returned to the system. They go into &lt;strong&gt;bins&lt;/strong&gt; — lists of free chunks grouped by size, so the next &lt;code&gt;malloc()&lt;/code&gt; can quickly find a matching block. glibc maintains several bin categories with different characteristics:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bin type&lt;/th&gt;
&lt;th&gt;Chunk size&lt;/th&gt;
&lt;th&gt;Characteristics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fast bins&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16–160 B (10 bins)&lt;/td&gt;
&lt;td&gt;LIFO, no coalescing — fastest, single-linked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tcache&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;per-thread, 24–1032 B&lt;/td&gt;
&lt;td&gt;Per-thread cache (glibc 2.26+), lock-free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Small bins&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt; 512 B (62 bins)&lt;/td&gt;
&lt;td&gt;FIFO, exact size, double-linked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Large bins&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;≥ 512 B&lt;/td&gt;
&lt;td&gt;Sorted, size ranges, best-fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unsorted bin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;Intermediate buffer before classification&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Tcache&lt;/strong&gt; (thread-local caching) is the most important optimization of recent years: each thread has its own pool of recently freed chunks, accessible &lt;strong&gt;without taking the arena lock&lt;/strong&gt;. This dramatically speeds up multithreaded allocations, but also introduced new attack vectors (&lt;em&gt;tcache poisoning&lt;/em&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Why free() Doesn't Return Memory
&lt;/h3&gt;

&lt;p&gt;When you free a chunk, the allocator runs a sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checks if the chunk fits in the tcache → if so, drops it there (the fastest path)&lt;/li&gt;
&lt;li&gt;If small → fast bin or small bin&lt;/li&gt;
&lt;li&gt;Tries to &lt;strong&gt;coalesce&lt;/strong&gt; with adjacent free chunks to counteract fragmentation&lt;/li&gt;
&lt;li&gt;Only if a large, contiguous free area grows at the &lt;strong&gt;top of the arena&lt;/strong&gt; (top chunk) — the allocator &lt;em&gt;may&lt;/em&gt; call &lt;code&gt;sbrk&lt;/code&gt; with a negative argument or &lt;code&gt;malloc_trim()&lt;/code&gt; to return memory&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The critical condition: memory returns to the system only when the free area is &lt;strong&gt;at the very end of the arena&lt;/strong&gt; and exceeds the threshold (&lt;code&gt;M_TRIM_THRESHOLD&lt;/code&gt;, default 128 KB). If a live chunk sits behind the freed area, &lt;code&gt;brk&lt;/code&gt; can't retreat — the memory stays reserved, though free. This is &lt;strong&gt;fragmentation&lt;/strong&gt;, not a leak.&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="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdlib.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;malloc.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/* Allocate 1000 blocks of 1 KB */&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="cm"&gt;/* Free all the EVEN ones — a fragmentation checkerboard */&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="cm"&gt;/* RSS stays high: free chunks interleave with live ones,
       brk can't retreat because live chunks block the arena's end */&lt;/span&gt;

    &lt;span class="cm"&gt;/* Explicitly force memory return to the system */&lt;/span&gt;
    &lt;span class="n"&gt;malloc_trim&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;span class="cm"&gt;/* return everything possible from the arena's end */&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&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;h2&gt;
  
  
  mmap vs brk — Why Large Allocations Are Different
&lt;/h2&gt;

&lt;p&gt;Allocations above &lt;code&gt;M_MMAP_THRESHOLD&lt;/code&gt; go directly via &lt;code&gt;mmap&lt;/code&gt; and have a &lt;strong&gt;fundamentally different return profile&lt;/strong&gt;. Each such allocation is a separate mapping that &lt;code&gt;free()&lt;/code&gt; &lt;strong&gt;immediately returns to the system&lt;/strong&gt; via &lt;code&gt;munmap&lt;/code&gt; — because it's an independent region, not part of a shared arena.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;brk&lt;/code&gt; arena (small)&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;mmap&lt;/code&gt; (large ≥128 KB)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Return to OS on free()&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rarely (only from arena's end)&lt;/td&gt;
&lt;td&gt;Immediately (munmap)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Allocation cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low (usually no syscall)&lt;/td&gt;
&lt;td&gt;High (syscall + page fault)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fragmentation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Possible (shared arena)&lt;/td&gt;
&lt;td&gt;None (isolated mappings)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Initialization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Memory may contain garbage&lt;/td&gt;
&lt;td&gt;Always zeroed (kernel guarantees)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hence the counterintuitive effect: a program doing many &lt;strong&gt;large&lt;/strong&gt; allocations may have more stable RSS than one with a million &lt;strong&gt;small&lt;/strong&gt; ones, because the large ones return to the system immediately while the small ones stay trapped in the arena. The threshold is also &lt;strong&gt;dynamic&lt;/strong&gt; — glibc increases it when it detects the program freeing large blocks, to avoid costly &lt;code&gt;mmap&lt;/code&gt;/&lt;code&gt;munmap&lt;/code&gt; cycles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arenas — Multithreaded Scaling
&lt;/h2&gt;

&lt;p&gt;In a multithreaded program, if all threads competed for a single arena, the lock on it would be a bottleneck. glibc solves this with &lt;strong&gt;multiple arenas&lt;/strong&gt;: the main arena (on &lt;code&gt;brk&lt;/code&gt;) plus additional thread arenas (on &lt;code&gt;mmap&lt;/code&gt;), each with its own lock.&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="c"&gt;# The number of arenas is capped — default 8 × core count (64-bit)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;nproc&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
64

&lt;span class="c"&gt;# Control via an environment variable&lt;/span&gt;
&lt;span class="nv"&gt;$ MALLOC_ARENA_MAX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 ./program
&lt;span class="c"&gt;# Limiting arenas reduces memory usage (fewer separate pools)&lt;/span&gt;
&lt;span class="c"&gt;# at the cost of potential lock contention with many threads&lt;/span&gt;

&lt;span class="c"&gt;# Diagnostics of all arenas' state&lt;/span&gt;
&lt;span class="nv"&gt;$ MALLOC_STATS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 ./program 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trade-off is direct: more arenas = less lock contention, but more fragmented, unreturned memory (each arena holds its own free chunks). For multithreaded, RSS-sensitive applications, &lt;code&gt;MALLOC_ARENA_MAX=2&lt;/code&gt; is a common first optimization shot. This also explains why the same program uses more memory on a 64-core machine than a 4-core one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternative Allocators — Measurement, Not Ideology
&lt;/h2&gt;

&lt;p&gt;The default &lt;code&gt;ptmalloc2&lt;/code&gt; in glibc is a general-purpose compromise. For specific workloads, specialized allocators offer a measurable advantage.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Allocator&lt;/th&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;th&gt;Typical use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;ptmalloc2&lt;/strong&gt; (glibc)&lt;/td&gt;
&lt;td&gt;Universal, always available&lt;/td&gt;
&lt;td&gt;Default, most applications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;jemalloc&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low fragmentation, predictable RSS, profiling&lt;/td&gt;
&lt;td&gt;Databases, long-running servers (Redis, FB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;tcmalloc&lt;/strong&gt; (Google)&lt;/td&gt;
&lt;td&gt;Very fast small allocations, great multithreaded&lt;/td&gt;
&lt;td&gt;Google apps, allocation-intensive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;mimalloc&lt;/strong&gt; (Microsoft)&lt;/td&gt;
&lt;td&gt;Newest, great speed/fragmentation balance&lt;/td&gt;
&lt;td&gt;New projects, .NET runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Swap the allocator WITHOUT recompilation — via LD_PRELOAD&lt;/span&gt;
&lt;span class="nv"&gt;$ LD_PRELOAD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/lib/libjemalloc.so.2 ./server
&lt;span class="c"&gt;# Same binary, different allocator — instant benchmark&lt;/span&gt;

&lt;span class="c"&gt;# Compare RSS under load (same workload, different allocators)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;/usr/bin/time &lt;span class="nt"&gt;-v&lt;/span&gt; ./server 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Maximum resident"&lt;/span&gt;
    Maximum resident &lt;span class="nb"&gt;set &lt;/span&gt;size &lt;span class="o"&gt;(&lt;/span&gt;kbytes&lt;span class="o"&gt;)&lt;/span&gt;: 524288   &lt;span class="c"&gt;# glibc&lt;/span&gt;
    Maximum resident &lt;span class="nb"&gt;set &lt;/span&gt;size &lt;span class="o"&gt;(&lt;/span&gt;kbytes&lt;span class="o"&gt;)&lt;/span&gt;: 312456   &lt;span class="c"&gt;# jemalloc — ~40% less&lt;/span&gt;

&lt;span class="c"&gt;# Permanent swap: link at compile time&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;gcc program.c &lt;span class="nt"&gt;-ljemalloc&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; program

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The swap mechanism is &lt;a href="https://devmindset.dev/en/elf-and-dynamic-linking-what-happens-before-main-runs/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;LD_PRELOAD overriding the &lt;code&gt;malloc&lt;/code&gt;/&lt;code&gt;free&lt;/code&gt; symbols from libc&lt;/a&gt; — the same interposition mechanism that powers AddressSanitizer. Practical rule: &lt;strong&gt;jemalloc&lt;/strong&gt; when you're fighting fragmentation and RSS on a long-running server, &lt;strong&gt;tcmalloc&lt;/strong&gt; when the bottleneck is small-allocation throughput across many threads. Always, however, &lt;strong&gt;measure first&lt;/strong&gt; — for many applications the default glibc is sufficient, and swapping is premature optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnostics — Tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;strace -e brk,mmap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which memory syscalls the program actually makes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MALLOC_STATS=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;glibc arena and bin statistics on exit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;malloc_info()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Programmatic dump of allocator state (XML)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;valgrind --tool=massif&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Heap usage profile over time (heap profiler)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jemalloc + jeprof&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allocation profiling with function attribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/proc/&amp;lt;pid&amp;gt;/smaps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Detailed RSS breakdown by mapping (heap, mmap, anon)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cat /proc/&amp;lt;pid&amp;gt;/status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;VmRSS vs VmData — resident vs reserved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Conclusion: The Allocator as a Layer You Must Understand
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;malloc&lt;/strong&gt; and &lt;strong&gt;free&lt;/strong&gt; aren't thin wrappers over the kernel — they're a sophisticated memory management layer with arenas, bins, chunks, and anti-fragmentation strategies. An engineer who understands this layer doesn't panic at high RSS after &lt;code&gt;free()&lt;/code&gt;, knows when it's fragmentation versus a real leak, and can deliberately match the allocator to the workload's profile.&lt;/p&gt;

&lt;p&gt;The key understandings: &lt;code&gt;free()&lt;/code&gt; returns memory to the allocator, not the system; the &lt;code&gt;brk&lt;/code&gt; vs &lt;code&gt;mmap&lt;/code&gt; boundary at 128 KB determines whether memory ever returns; tcache and multiple arenas speed up multithreading at the cost of RSS; and chunk metadata interleaves with data, which makes use-after-free a corruption vector. This knowledge turns "mysterious memory growth" into a predictable, measurable, and controllable aspect of the system.&lt;/p&gt;

&lt;p&gt;RSS that doesn't drop after &lt;code&gt;free()&lt;/code&gt; is most often not a bug in your code — it's the allocator doing exactly what it was designed to do. The question isn't "why didn't it free," but "is this fragmentation, or am I actually holding live references."&lt;/p&gt;

&lt;h3&gt;
  
  
  Related articles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://devmindset.dev/en/anatomy-of-a-segfault-from-mmu-through-kernel-to-gdb-core-dump/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;Anatomy of a Segfault — From MMU Through Kernel to gdb Core Dump&lt;/a&gt; — use-after-free and chunk corruption from the page-fault angle&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devmindset.dev/en/elf-and-dynamic-linking-what-happens-before-main-runs/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;ELF and Dynamic Linking — What Happens Before main() Runs&lt;/a&gt; — LD_PRELOAD as the allocator swap mechanism&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devmindset.dev/en/mmap-when-memory-mapped-i-o-beats-read-and-write/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;mmap — When Memory-Mapped I/O Beats read() and write()&lt;/a&gt; — mmap for large anonymous allocations&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devmindset.dev/en/what-fork-in-linux-really-does-and-why-it-matters-more-than-you-think/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;What fork() in Linux Really Does Under the Hood&lt;/a&gt; — copy-on-write and sharing heap pages&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devmindset.dev?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;devmindset.dev&lt;/a&gt; — Linux internals, systems programming, and the self-taught developer mindset.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>c</category>
      <category>performance</category>
      <category>memory</category>
    </item>
    <item>
      <title>cgroups v2 as a Native Runtime Isolation Primitive — No Docker Required</title>
      <dc:creator>Piotrek Karasinski</dc:creator>
      <pubDate>Wed, 01 Jul 2026 06:00:00 +0000</pubDate>
      <link>https://dev.to/piotrek1372/cgroups-v2-as-a-native-runtime-isolation-primitive-no-docker-required-38an</link>
      <guid>https://dev.to/piotrek1372/cgroups-v2-as-a-native-runtime-isolation-primitive-no-docker-required-38an</guid>
      <description>&lt;h2&gt;
  
  
  The Overhead Tax of Container Abstraction
&lt;/h2&gt;

&lt;p&gt;Every time a team reaches for Docker to isolate a background service—a scheduled job, a data pipeline, a monitoring daemon—it pays an overhead tax. The &lt;strong&gt;container runtime&lt;/strong&gt; (&lt;code&gt;containerd&lt;/code&gt;, &lt;code&gt;runc&lt;/code&gt;) must initialize a namespaced environment, mount overlay filesystems, and manage network virtualization, even when the only requirement is CPU throttling and memory capping for a single process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;cgroups v2&lt;/strong&gt; (Control Groups version 2), unified in Linux 4.5 and enabled by default on most modern distributions including Arch Linux since kernel 5.8+, provides that isolation primitive &lt;strong&gt;natively&lt;/strong&gt;—with zero daemon overhead, no image layers, no OCI spec compliance ceremony.&lt;/p&gt;

&lt;p&gt;This article dissects the operational architecture of the &lt;strong&gt;cgroups v2 unified hierarchy&lt;/strong&gt;, its integration with systemd's transient unit model, and programmatic control via Python—delivering the isolation contract without the abstraction tax.&lt;/p&gt;

&lt;h2&gt;
  
  
  cgroups v2: Architectural Shift from v1
&lt;/h2&gt;

&lt;p&gt;The fundamental flaw of &lt;strong&gt;cgroups v1&lt;/strong&gt; was its fragmented, per-controller hierarchy. CPU limits lived in &lt;code&gt;/sys/fs/cgroup/cpu/&lt;/code&gt;, memory limits in &lt;code&gt;/sys/fs/cgroup/memory/&lt;/code&gt;—independent trees that could produce conflicting resource assignments and made atomic process migration across controllers operationally fragile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;cgroups v2&lt;/strong&gt; enforces a single, unified hierarchy rooted at &lt;code&gt;/sys/fs/cgroup/&lt;/code&gt;. All controllers—&lt;code&gt;cpu&lt;/code&gt;, &lt;code&gt;memory&lt;/code&gt;, &lt;code&gt;io&lt;/code&gt;, &lt;code&gt;pids&lt;/code&gt;—operate within one coherent tree. The key architectural invariant: &lt;strong&gt;a process can belong to exactly one cgroup&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unified Hierarchy Verification
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify cgroups v2 is the active mode&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;mount | &lt;span class="nb"&gt;grep &lt;/span&gt;cgroup
cgroup2 on /sys/fs/cgroup &lt;span class="nb"&gt;type &lt;/span&gt;cgroup2 &lt;span class="o"&gt;(&lt;/span&gt;rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Inspect available controllers on root cgroup&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the output shows &lt;code&gt;cgroup2&lt;/code&gt; as the filesystem type and a unified set of controllers, the system is fully migrated. On hybrid systems, the &lt;code&gt;cgroup_no_v1=all&lt;/code&gt; kernel parameter forces exclusive v2 mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Direct Cgroup Lifecycle Management
&lt;/h2&gt;

&lt;p&gt;Cgroup management at its core is &lt;strong&gt;filesystem manipulation&lt;/strong&gt;. Creating a cgroup means creating a directory; assigning a process means writing its PID to &lt;code&gt;cgroup.procs&lt;/code&gt;; constraining resources means writing values to controller-specific interface files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating and Configuring a Cgroup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a cgroup for an isolated workload&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; /sys/fs/cgroup/devmindset-worker

&lt;span class="c"&gt;# Enable CPU and memory controllers on this cgroup&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"+cpu +memory"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /sys/fs/cgroup/devmindset-worker/cgroup.subtree_control

&lt;span class="c"&gt;# Cap CPU utilization: 20% of one core (quota/period in microseconds)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"20000 100000"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /sys/fs/cgroup/devmindset-worker/cpu.max

&lt;span class="c"&gt;# Hard memory limit: 256 MiB&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="m"&gt;256&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /sys/fs/cgroup/devmindset-worker/memory.max

&lt;span class="c"&gt;# Assign current shell process to the cgroup&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$$&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /sys/fs/cgroup/devmindset-worker/cgroup.procs

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;cpu.max&lt;/code&gt; interface accepts &lt;code&gt;$QUOTA $PERIOD&lt;/code&gt; pairs, directly mapping to &lt;strong&gt;CFS (Completely Fair Scheduler)&lt;/strong&gt; bandwidth throttling. Any process inside this cgroup consuming more than 20ms per 100ms window will be throttled at the scheduler level—no userspace daemon required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Systemd Integration: Transient Units as Isolation Primitive
&lt;/h2&gt;

&lt;p&gt;For production workloads on systemd-based systems, &lt;strong&gt;&lt;code&gt;systemd-run&lt;/code&gt;&lt;/strong&gt; is the idiomatic abstraction over cgroups v2. It spawns a process inside a transient systemd scope or service unit, inheriting the systemd cgroup delegation model.&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="c"&gt;# Launch an isolated process with resource constraints via transient unit&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;systemd-run &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;devmindset-worker &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;CPUQuota&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20% &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;MemoryMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;256M &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;IOWeight&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10 &lt;span class="se"&gt;\&lt;/span&gt;
    /usr/bin/python3 /opt/workers/pipeline_runner.py

&lt;span class="c"&gt;# Inspect live resource accounting&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;systemctl status devmindset-worker.scope
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/system.slice/devmindset-worker.scope/cpu.stat

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--scope&lt;/code&gt; flag creates a transient unit scoped to the calling session, while &lt;code&gt;--service&lt;/code&gt; creates a persistent transient service with full systemd lifecycle management. The &lt;code&gt;IOWeight&lt;/code&gt; property maps to the &lt;strong&gt;CFQ/BFQ I/O scheduler&lt;/strong&gt; weight, providing proportional I/O bandwidth allocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Programmatic Control via Python
&lt;/h2&gt;

&lt;p&gt;For dynamic workload management—spinning up isolated workers at runtime, adjusting quotas based on telemetry, implementing backpressure mechanisms—Python's &lt;code&gt;pathlib&lt;/code&gt; provides clean, idiomatic access to the cgroupfs interface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Final&lt;/span&gt;

&lt;span class="c1"&gt;# Micro-Rationale: Using pathlib.Path for type-safe filesystem operations
# over raw string concatenation. O(1) path construction, zero external deps.
&lt;/span&gt;&lt;span class="n"&gt;CGROUP_ROOT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Final&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/sys/fs/cgroup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CgroupV2Controller&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Manages a single cgroups v2 hierarchy for process isolation.

    Enforces CPU quota and memory hard limits on a named cgroup.
    Implements context manager protocol for deterministic cleanup.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cpu_quota_percent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;memory_max_mib&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;cpu_quota_percent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu_quota_percent must be in [1, 100], got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cpu_quota_percent&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;memory_max_mib&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory_max_mib must be &amp;gt;= 16 MiB, got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;memory_max_mib&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cpu_quota_percent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cpu_quota_percent&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_memory_max_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory_max_mib&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cgroup_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CGROUP_ROOT&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Writes a value to a cgroup interface file.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cgroup_path&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;interface&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;PermissionError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PermissionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Insufficient privileges to write to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Run as root or with CAP_SYS_ADMIN.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;OSError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to write &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CgroupV2Controller&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Creates the cgroup directory and configures resource limits.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cgroup_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;FileExistsError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;FileExistsError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cgroup &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; already exists at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cgroup_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cgroup.subtree_control&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+cpu +memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# CPU quota: $QUOTA $PERIOD in microseconds (CFS bandwidth throttling)
&lt;/span&gt;        &lt;span class="n"&gt;period_us&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100_000&lt;/span&gt;
        &lt;span class="n"&gt;quota_us&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;period_us&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cpu_quota_percent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu.max&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;quota_us&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;period_us&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Memory hard limit — OOM killer fires at this threshold
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory.max&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_memory_max_bytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assign_pid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Moves a process into this cgroup by writing to cgroup.procs.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&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;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ProcessLookupError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ProcessLookupError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No process with PID &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; found.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cgroup.procs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assign_current_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Moves the calling process into this cgroup.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign_pid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getpid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Removes the cgroup directory. All processes must be migrated first.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;procs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cgroup_path&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cgroup.procs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&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;procs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cannot destroy cgroup &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PIDs still assigned: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;procs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cgroup_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rmdir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__enter__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CgroupV2Controller&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__exit__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# Best-effort cleanup; log in production
&lt;/span&gt;
&lt;span class="c1"&gt;# --- Usage ---
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;worker_proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/python3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/opt/workers/cpu_intensive_task.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;CgroupV2Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devmindset-worker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cpu_quota_percent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;memory_max_mib&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cgroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cgroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign_pid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker_proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;worker_proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Overhead Comparison: Docker vs. Direct cgroups v2
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Docker (&lt;code&gt;runc&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Direct cgroups v2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runtime daemon&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;containerd&lt;/code&gt; + &lt;code&gt;dockerd&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Process start latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~80–150ms (OCI init)&lt;/td&gt;
&lt;td&gt;&amp;lt; 1ms (mkdir + write)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Filesystem isolation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OverlayFS mount required&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network namespace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Virtual NIC + iptables rules&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory overhead&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~15–30 MiB per container&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kernel interface&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Indirect (runc → seccomp → cgroup)&lt;/td&gt;
&lt;td&gt;Direct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Appropriate use case&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full application isolation, portability&lt;/td&gt;
&lt;td&gt;Single-process resource capping&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Conclusion: Choosing the Right Isolation Primitive
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;cgroups v2&lt;/strong&gt; is not a Docker alternative—it is the mechanism Docker is built on. Reaching for it directly means operating at the right abstraction level for the problem at hand. On a hardened Arch Linux system running a set of long-lived workers, background scrapers, or ML inference daemons, a 150-line Python controller and three &lt;code&gt;systemd-run&lt;/code&gt; flags deliver full &lt;strong&gt;runtime isolation&lt;/strong&gt; without the operational surface of a container orchestration stack.&lt;/p&gt;

&lt;p&gt;The engineers who understand the kernel primitives their tools are built on will always debug faster, profile more accurately, and architect more efficiently than those who interact exclusively through abstraction layers.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devmindset.dev?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;devmindset.dev&lt;/a&gt; — Linux internals, systems programming, and the self-taught developer mindset.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Related deep-dives:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devmindset.dev/en/epoll-vs-io_uring-when-the-event-loop-isnt-enough/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;epoll vs io_uring — when the event loop isn't enough&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devmindset.dev/en/what-fork-in-linux-really-does-and-why-it-matters-more-than-you-think/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;What fork() in Linux really does&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>devops</category>
      <category>python</category>
      <category>performance</category>
    </item>
    <item>
      <title>strace as a Debugging Primitive — When the Stack Trace Lies</title>
      <dc:creator>Piotrek Karasinski</dc:creator>
      <pubDate>Tue, 30 Jun 2026 18:29:04 +0000</pubDate>
      <link>https://dev.to/piotrek1372/strace-as-a-debugging-primitive-when-the-stack-trace-lies-20j4</link>
      <guid>https://dev.to/piotrek1372/strace-as-a-debugging-primitive-when-the-stack-trace-lies-20j4</guid>
      <description>&lt;p&gt;Your logs tell you what your code &lt;em&gt;thinks&lt;/em&gt; it did. Your stack trace tells you where the code &lt;em&gt;believed&lt;/em&gt; it was. &lt;code&gt;strace&lt;/code&gt; tells you what the kernel &lt;em&gt;actually&lt;/em&gt; did — every &lt;code&gt;openat()&lt;/code&gt;, every &lt;code&gt;read()&lt;/code&gt;, every &lt;code&gt;connect()&lt;/code&gt; that returned &lt;code&gt;-1&lt;/code&gt; while your application swallowed the error and kept lying to you. When the gap between intent and reality &lt;em&gt;is&lt;/em&gt; the bug, re-reading the source will not close it. You have to observe the boundary where userspace ends and the kernel begins.&lt;/p&gt;

&lt;p&gt;Most engineers know &lt;code&gt;strace -p &amp;lt;pid&amp;gt;&lt;/code&gt; as a panic button. Few actually &lt;em&gt;read&lt;/em&gt; the output, and fewer reach for &lt;code&gt;-f&lt;/code&gt;, &lt;code&gt;-c&lt;/code&gt;, or &lt;code&gt;-k&lt;/code&gt; when it counts. That gap is the difference between staring at a wall of syscalls and extracting a root cause in ninety seconds. Treating &lt;code&gt;strace&lt;/code&gt; as a first-class instrument is a direct extension of &lt;a href="https://devmindset.dev/en/debugging-like-a-developer/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;hypothesis-driven debugging methodology&lt;/a&gt;: you form a precise claim about what the process is doing and let the evidence confirm or kill it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What strace actually observes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;strace&lt;/code&gt; is built on &lt;a href="https://man7.org/linux/man-pages/man2/ptrace.2.html" rel="noopener noreferrer"&gt;&lt;code&gt;ptrace(2)&lt;/code&gt;&lt;/a&gt;, the same kernel facility debuggers use. It attaches to a target and stops it on every transition across the syscall boundary — once on entry, once on exit — reading the registers to decode the syscall number, its arguments, and its return value. That is the whole model, and it explains the cost: every syscall now pays for two extra context switches into the tracer. On a syscall-heavy workload the target can slow by an order of magnitude. This is not a footnote — it is the reason &lt;code&gt;strace&lt;/code&gt; belongs in diagnosis, never in a hot path or a latency-sensitive production service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading a trace line — the part nobody teaches
&lt;/h2&gt;

&lt;p&gt;Every line has the same shape: &lt;code&gt;syscall(arguments) = return_value&lt;/code&gt;. The return value is where the truth lives. A successful call returns a file descriptor, a byte count, or &lt;code&gt;0&lt;/code&gt;; a failure returns &lt;code&gt;-1&lt;/code&gt; followed by the symbolic &lt;code&gt;errno&lt;/code&gt; and its description. That &lt;code&gt;errno&lt;/code&gt; is the signal you came for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openat(AT_FDCWD, "/etc/app/config.yaml", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/share/app/config.yaml", O_RDONLY) = 3
read(3, "host: 127.0.0.1\nport: 6379\n", 4096) = 27
connect(4, {sa_family=AF_INET, sin_port=htons(6379), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 ECONNREFUSED (Connection refused)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four lines, two findings, zero guesswork: the app probed a config path that does not exist before falling back to the real one, and the Redis connection was refused outright. No log statement told you either fact — the syscalls did. The failure worth memorising is &lt;code&gt;EFAULT&lt;/code&gt;: it means a syscall received a pointer to memory the process does not own. That is the same class of bad-address access which, followed to its conclusion by the MMU and the kernel, produces a &lt;a href="https://devmindset.dev/en/anatomy-of-a-segfault-from-mmu-through-kernel-to-gdb-core-dump/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;segmentation fault&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flags that separate signal from noise
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-f&lt;/code&gt;&lt;/strong&gt; — follow forks and threads. The moment a process calls &lt;a href="https://devmindset.dev/en/what-fork-in-linux-really-does-and-why-it-matters-more-than-you-think/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;fork()&lt;/a&gt; or spawns a worker pool, an unadorned trace goes blind to the children. Almost every real service needs this flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-e trace=…&lt;/code&gt;&lt;/strong&gt; — filter by class instead of drowning. &lt;code&gt;-e trace=network&lt;/code&gt;, &lt;code&gt;-e trace=%file&lt;/code&gt;, &lt;code&gt;-e trace=memory&lt;/code&gt; cut the stream to the syscalls you care about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-c&lt;/code&gt;&lt;/strong&gt; — aggregate instead of stream. Produces a summary table of time, call count, and errors per syscall. This is your profiler the moment you suspect a process is syscall-bound.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-T&lt;/code&gt; / &lt;code&gt;-tt&lt;/code&gt;&lt;/strong&gt; — time spent inside each syscall / absolute timestamps. Together they turn "it's slow" into "it spends 600&amp;nbsp;ms in &lt;code&gt;fsync&lt;/code&gt;".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-y&lt;/code&gt; / &lt;code&gt;-yy&lt;/code&gt;&lt;/strong&gt; — decode descriptors, so &lt;code&gt;read(3, …)&lt;/code&gt; becomes &lt;code&gt;read(3&amp;lt;/usr/share/app/config.yaml&amp;gt;, …)&lt;/code&gt; and sockets show their endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-k&lt;/code&gt;&lt;/strong&gt; — attach a userspace stack trace to each syscall, bridging "which syscall" to "which line of code".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-s N&lt;/code&gt;&lt;/strong&gt; — raise the printed string length (the default truncates payloads).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The summary mode also answers "why is this slow?" without a dedicated profiler. Run any command under &lt;code&gt;strace -f -c&lt;/code&gt; and the table ranks syscalls by cumulative time — the same technique that turns a sluggish terminal into a fixable list of offending &lt;code&gt;stat()&lt;/code&gt; and &lt;code&gt;openat()&lt;/code&gt; calls when you profile &lt;a href="https://devmindset.dev/en/bashrc-anatomy-of-shell-startup-and-performance-optimization/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;shell startup and .bashrc performance&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  From raw trace to an actionable summary
&lt;/h2&gt;

&lt;p&gt;Reading &lt;code&gt;-c&lt;/code&gt; output by eye is fine once. When you want it in CI, in a regression gate, or aggregated across runs, parse it. The wrapper below runs a command under &lt;code&gt;strace -f -c&lt;/code&gt;, captures the summary from stderr, parses it into typed records, and returns the syscalls that dominate time. It handles the obvious failure modes — strace missing, the target hanging, a malformed table — instead of assuming the happy path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;shutil&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Final&lt;/span&gt;

&lt;span class="n"&gt;_HEADER_TOKENS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Final&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;syscall&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SyscallStat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;usec_per_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StraceError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Raised when strace cannot be run or its summary cannot be parsed.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;profile_syscalls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SyscallStat&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Run *cmd* under ``strace -f -c`` and return per-syscall stats, busiest first.

    Raises:
        StraceError: strace is missing, the command times out, or the summary
            table cannot be located in strace&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s stderr.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;StraceError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;empty command&lt;/span&gt;&lt;span class="sh"&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;shutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;which&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;StraceError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strace is not installed or not on PATH&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# the traced command may legitimately exit non-zero
&lt;/span&gt;        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeoutExpired&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;StraceError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traced command exceeded &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_parse_summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_parse_summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SyscallStat&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ln&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ln&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_HEADER_TOKENS&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="bp"&gt;None&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;header&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;StraceError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no summary table found; was -c passed to strace?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SyscallStat&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;  &lt;span class="c1"&gt;# the total row terminates the table
&lt;/span&gt;        &lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;cols&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="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;IndexError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;  &lt;span class="c1"&gt;# skip anything that is not a data row
&lt;/span&gt;        &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SyscallStat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;StraceError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary table present but no rows could be parsed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage: profile.py COMMAND [ARGS...]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;stat&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;profile_syscalls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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;span class="n"&gt;marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  (has errors)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ljust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rjust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;9.4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; calls  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usec_per_call&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;8.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; us/call&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;marker&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is deliberately defensive: a missing binary, a timeout, and an unparseable table each raise a typed &lt;code&gt;StraceError&lt;/code&gt; rather than returning silently wrong data. The &lt;code&gt;check=False&lt;/code&gt; is intentional — the program you are tracing is allowed to exit non-zero; that is frequently the entire reason you are tracing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  strace vs ltrace vs gdb vs perf — pick the right lens
&lt;/h2&gt;

&lt;p&gt;strace is one lens, not the only one. Each tool observes a different layer at a different cost, and reaching for the wrong one wastes time or perturbs the very behaviour you are chasing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Observes&lt;/th&gt;
&lt;th&gt;Overhead&lt;/th&gt;
&lt;th&gt;Best question&lt;/th&gt;
&lt;th&gt;Production-safe?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;strace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Syscalls (kernel boundary)&lt;/td&gt;
&lt;td&gt;High (2 ctx switches/call)&lt;/td&gt;
&lt;td&gt;What is the process asking the kernel to do?&lt;/td&gt;
&lt;td&gt;No — diagnosis only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ltrace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Library / function calls (userspace)&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Which library calls fire, with what arguments?&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;gdb&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full program state, memory, breakpoints&lt;/td&gt;
&lt;td&gt;Very high (stops the world)&lt;/td&gt;
&lt;td&gt;What is the exact state at this line?&lt;/td&gt;
&lt;td&gt;No (interactive)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;perf trace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Syscalls via perf_events&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;How do syscalls behave under real load?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;bpftrace / eBPF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Arbitrary kernel + user probes&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;td&gt;Custom, low-overhead production tracing&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When not to reach for strace
&lt;/h2&gt;

&lt;p&gt;The overhead is not academic, and it is not the only constraint. On hardened systems, &lt;code&gt;/proc/sys/kernel/yama/ptrace_scope&lt;/code&gt; can forbid attaching to a process you do not own — you will get &lt;code&gt;EPERM&lt;/code&gt; no matter how root you feel. Inside containers, a restrictive &lt;code&gt;seccomp&lt;/code&gt; profile may block &lt;code&gt;ptrace&lt;/code&gt; entirely. And on a service handling live traffic, doubling the cost of every syscall is not a debugging session, it is an incident.&lt;/p&gt;

&lt;p&gt;For anything that must run against production load, reach for &lt;code&gt;perf trace&lt;/code&gt; or an eBPF tool such as &lt;code&gt;bpftrace&lt;/code&gt;, which observe the same syscall boundary at a fraction of the cost. But on a box you control, with a process you can afford to slow down, &lt;code&gt;strace&lt;/code&gt; remains the fastest way to answer the only question that matters when intent and reality diverge: what is this process &lt;em&gt;actually&lt;/em&gt; doing? Once you can read its output fluently, that question stops being a mystery and becomes a lookup.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devmindset.dev?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;devmindset.dev&lt;/a&gt; — Linux internals, systems programming, and the self-taught developer mindset.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Related deep-dives:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devmindset.dev/en/what-fork-in-linux-really-does-and-why-it-matters-more-than-you-think/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;What fork() in Linux really does&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devmindset.dev/en/signals-in-linux-sigkill-sigterm-sigchld-handler/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;Signals in Linux — SIGKILL, SIGTERM, SIGCHLD and the anatomy of a handler&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>debugging</category>
      <category>performance</category>
      <category>c</category>
    </item>
  </channel>
</rss>
