<?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: Renato Valim</title>
    <description>The latest articles on DEV Community by Renato Valim (@fullplasticalchemist).</description>
    <link>https://dev.to/fullplasticalchemist</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1030740%2F343348af-dfb6-44ea-b947-7cae8f286d96.png</url>
      <title>DEV Community: Renato Valim</title>
      <link>https://dev.to/fullplasticalchemist</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fullplasticalchemist"/>
    <language>en</language>
    <item>
      <title>TIL: BEAM Dirty Work!!</title>
      <dc:creator>Renato Valim</dc:creator>
      <pubDate>Wed, 01 Oct 2025 11:55:49 +0000</pubDate>
      <link>https://dev.to/fullplasticalchemist/til-beam-dirty-work-15k5</link>
      <guid>https://dev.to/fullplasticalchemist/til-beam-dirty-work-15k5</guid>
      <description>&lt;p&gt;I’ve been studying again operating systems — processes, threads, CPU scheduling — and wanted to connect that knowledge to my day-to-day work as an Elixir developer. So I fired up &lt;code&gt;iex -S mix phx.server&lt;/code&gt; and started poking around.&lt;/p&gt;

&lt;p&gt;First surprise: running &lt;code&gt;pgrep -f beam.smp&lt;/code&gt; showed &lt;strong&gt;3 OS processes&lt;/strong&gt;. Turns out two were orphaned ElixirLS language servers from old Neovim sessions (oops), and one was my Phoenix app.&lt;/p&gt;

&lt;p&gt;Second (not so) surprise: &lt;code&gt;ps -M &amp;lt;phoenix app pid&amp;gt; | wc -l&lt;/code&gt; revealed my Phoenix app was running &lt;strong&gt;32 OS threads&lt;/strong&gt; (on my M1 Mac)&lt;/p&gt;

&lt;p&gt;I wanted to understand exactly the purpouse of all those threads, but after some research I ended up reading the &lt;code&gt;:erlang.system_info/1&lt;/code&gt; &lt;a href="https://www.erlang.org/doc/apps/erts/erlang.html#system_info/1" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;. Scrolling through the available options, I noticed some intriguing entries: &lt;code&gt;dirty_cpu_schedulers&lt;/code&gt;, &lt;code&gt;dirty_io_schedulers&lt;/code&gt;, ...&lt;/p&gt;

&lt;p&gt;“Dirty” schedulers? I’d never heard that term before. Very interesting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:erlang&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:schedulers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;8&lt;/span&gt;
&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:erlang&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:dirty_cpu_schedulers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;8&lt;/span&gt;
&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:erlang&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:dirty_io_schedulers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That rabbit hole led me to discover one of the BEAM VM’s most clever architectural decisions — and revealed a fundamental challenge at the boundary between managed VM code and native code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: When the BEAM Loses Control
&lt;/h2&gt;

&lt;p&gt;The BEAM VM is famous for its ability to run &lt;em&gt;gazillions!!!&lt;/em&gt; of lightweight processes concurrently. It does this through &lt;strong&gt;cooperative multitasking&lt;/strong&gt; — each process runs for a bit, then yields control so others can run. The BEAM VM can preempt any process after some "reductions" (think of these as instruction counts).&lt;/p&gt;

&lt;p&gt;But there’s a catch: &lt;strong&gt;this only works for BEAM Bytecode.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you call a NIF (Native Implemented Function) — C/Rust code compiled into a &lt;code&gt;.so/.dylib/.dll&lt;/code&gt; shared library — something dangerous happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This is Elixir code — BEAM has full control&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;some_func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
 &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;other_func&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="c1"&gt;# Can be preempted by BEAM&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="c1"&gt;# This calls C code — BEAM loses control&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
 &lt;span class="ss"&gt;:crypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Cannot be interrupted by BEAM&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Journey of the Program Counter
&lt;/h2&gt;

&lt;p&gt;Here’s what happens when a scheduler thread executes a NIF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scheduler Thread 1 (single OS thread):
├─ Process A: needs to hash password (bcrypt NIF)
├─ Process B: handle HTTP request (waiting…)
├─ Process C: send email (waiting…)
└─ Process D: database query (waiting…)

Timeline:
0ms: Scheduler picks Process A
1ms: Process A calls bcrypt NIF
 PC (Program Counter) jumps from BEAM bytecode → C code in .so file
2ms: [PC executing C code — "BEAM cannot see inside"]
10ms: [Still in C code…]
50ms: [Still in C code…]
100ms: bcrypt returns! PC jumps back to BEAM
101ms: Scheduler can FINALLY pick Process B

Result: Process B, C, D waited 100ms even though they had work to do!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Can’t BEAM Interrupt C Code?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;BEAM bytecode&lt;/strong&gt; runs in the VM’s interpreter loop, something like this (merely illustrative!!!):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;while (true) {
 instruction = fetch()
 execute(instruction)
 reductions++

 if (reductions &amp;gt; MAX_REDUCTIONS) {
 // That's enough!! Let your brother play the videogame now
 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;strong&gt;C code&lt;/strong&gt; in a NIF executes as raw CPU instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// crypto.so
int hash_password(char* pwd) {
 for (int i = 0; i &amp;lt; 1000000; i++) {
 // Complex hashing logic
 // BEAM’s while loop isn’t running!
 // Can’t check reductions count
 // Can’t yield control
 }
 return result;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The program counter has left BEAM’s interpreter and is running native machine code directly. BEAM just has to wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wait, Doesn’t the OS Share CPU Time?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Great question!&lt;/strong&gt; Yes, the OS scheduler does time-slice CPU between threads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OS Level (works fine):
Thread 1 (Scheduler 1): [10ms] [pause] [10ms] [pause]
Thread 2 (Scheduler 2): [pause] [10ms] [pause] [10ms]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that doesn’t help &lt;strong&gt;inside a single thread:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inside Scheduler Thread 1:
├─ Process A [running bcrypt NIF — 100ms]
│ └─ OS gives thread CPU time 
│ └─ But thread is busy executing C code
│ └─ Processes B, C, D are stuck in queue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The OS is sharing CPU time between &lt;strong&gt;threads&lt;/strong&gt;, but the BEAM can’t share scheduler time between Erlang processes while stuck in that native code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Dirty Schedulers
&lt;/h2&gt;

&lt;p&gt;BEAM has dirty schedulers: separate thread pools for running potentially blocking operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────┐
│ Normal Schedulers (8 threads on my M1 Mac) 
├────────────────────────────────────────────────┤
│ Thread 1: [Process A][Process B][Process C]
│ Thread 2: [Process D][Process E][Process F]
│ ...
│ &amp;gt; Handle regular Erlang processes 
│ &amp;gt; Stay responsive and preemptible 
└────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────┐
│ Dirty CPU Schedulers (8 threads on my M1 Mac) 
├────────────────────────────────────────────────┤
│ Thread 1: [bcrypt NIF — can block for 100ms] 
│ Thread 2: [image compression NIF] 
│ Thread 3: [crypto operations] 
│ &amp;gt; Run CPU-intensive NIFs 
│ &amp;gt; Can block without affecting normal schedulers 
└────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────┐
│ Dirty IO Schedulers (10 threads on my M1 Mac) 
├────────────────────────────────────────────────┤
│ &amp;gt; Run IO-heavy NIFs (file operations, etc.) 
└────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  See It In Action
&lt;/h2&gt;

&lt;p&gt;On my machine running a Phoenix app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In IEx&lt;/span&gt;
&lt;span class="ss"&gt;:erlang&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:schedulers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; 8&lt;/span&gt;
&lt;span class="ss"&gt;:erlang&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:dirty_cpu_schedulers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; 8&lt;/span&gt;
&lt;span class="ss"&gt;:erlang&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:dirty_io_schedulers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; 10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total: &lt;strong&gt;26 worker threads&lt;/strong&gt; just for scheduling!&lt;/p&gt;

&lt;p&gt;From the OS perspective:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ps &lt;span class="nt"&gt;-M&lt;/span&gt; &amp;lt;beam_pid&amp;gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
33 &lt;span class="c"&gt;# 32 threads total (8 + 8 + 10 + system threads)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How NIFs Use Dirty Schedulers
&lt;/h2&gt;

&lt;p&gt;NIF authors mark functions as "dirty":&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="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;ERL_NIF_TERM&lt;/span&gt; &lt;span class="nf"&gt;slow_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ErlNifEnv&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="c1"&gt;// CPU-intensive work&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;ErlNifFunc&lt;/span&gt; &lt;span class="n"&gt;nif_funcs&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="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&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;slow_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ERL_NIF_DIRTY_JOB_CPU_BOUND&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="c1"&gt;//                     ^^^^ This tells BEAM to use dirty scheduler&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you call it from Elixir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="ss"&gt;:crypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ↓&lt;/span&gt;
&lt;span class="c1"&gt;# BEAM identifies it is dirty work&lt;/span&gt;
&lt;span class="c1"&gt;# ↓ &lt;/span&gt;
&lt;span class="c1"&gt;# Schedules it on a dirty CPU scheduler instead&lt;/span&gt;
&lt;span class="c1"&gt;# ↓&lt;/span&gt;
&lt;span class="c1"&gt;# Normal schedulers stay responsive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Real-World Impact
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Without dirty schedulers:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User A: Registers account (triggers bcrypt)
Result: Entire Phoenix app freezes for 100ms
 — Health checks timeout
 — WebSockets disconnect
 — Simple GET requests stall
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;With dirty schedulers:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User A: Registers account (triggers bcrypt on dirty scheduler)
Result: Rest of app stays responsive
 — Other requests process normally
 — WebSockets maintain connection
 — Only password hashing takes 100ms (expected)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;The BEAM VM’s responsiveness comes from &lt;strong&gt;cooperative multitasking&lt;/strong&gt;, but that breaks down when calling native code. Dirty schedulers solve this by isolating potentially blocking operations on separate threads, keeping your application responsive even when running expensive C/Rust operations.&lt;/p&gt;

&lt;p&gt;Next time you use &lt;code&gt;:crypto&lt;/code&gt;, &lt;code&gt;:bcrypt&lt;/code&gt;, or any NIF-based library, remember: there’s a whole separate pool of threads handling that work so your web requests don’t freeze!&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>erlang</category>
      <category>todayilearned</category>
      <category>computerscience</category>
    </item>
  </channel>
</rss>
