<?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: Peng Zhang</title>
    <description>The latest articles on DEV Community by Peng Zhang (@pengubco).</description>
    <link>https://dev.to/pengubco</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%2F3815781%2F33246863-88ff-4ad3-bc54-5549d526b36d.jpeg</url>
      <title>DEV Community: Peng Zhang</title>
      <link>https://dev.to/pengubco</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pengubco"/>
    <language>en</language>
    <item>
      <title>Who killed my service: collecting kernel kill logs with OTEL</title>
      <dc:creator>Peng Zhang</dc:creator>
      <pubDate>Tue, 31 Mar 2026 05:56:35 +0000</pubDate>
      <link>https://dev.to/pengubco/who-killed-my-service-collecting-kernel-kill-logs-with-otel-1epl</link>
      <guid>https://dev.to/pengubco/who-killed-my-service-collecting-kernel-kill-logs-with-otel-1epl</guid>
      <description>&lt;p&gt;We run a container platform. For privacy and security reasons, we do not collect kernel logs because customer workloads use the same kernel as the host and kernel messages can contain sensitive customer data, such as command-line arguments surfaced in audit logs. However, we recently hit a blind spot: &lt;code&gt;foo.service&lt;/code&gt; was killed with no trace in its own logs or systemd logs. No error, no panic, no graceful shutdown message. The process was just gone. Our suspicion pointed to the kernel, but we had no evidence. This post shows how to close the gap by collecting selected kernel kill logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Does the Kernel Kill a Process?
&lt;/h2&gt;

&lt;p&gt;The kernel protects the system and it will kill user space processes that either violated a safety rule or the system has run out of a physical resource needed to stay stable. Here are some common causes of a kernel kill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Out of Memory (OOM) Killer&lt;/strong&gt;: When RAM and swap are fully exhausted, the kernel picks a victim using an OOM score and kills it to prevent a total system hang. This is the most common cause of mysterious process deaths.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Log signal: &lt;code&gt;Out of memory: Killed process ...&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Memory cgroup limit&lt;/strong&gt;: In containerized environments, each container typically has a memory cgroup limit. When a container exceeds its limit, the kernel's cgroup OOM killer fires even if the host has plenty of free memory.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Log signal: &lt;code&gt;Memory cgroup out of memory: Killed process ...&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Segmentation Fault&lt;/strong&gt;: A process accesses memory it doesn't own, or writes to a read-only region. The CPU raises a hardware exception, the kernel catches it, and the process is terminated with &lt;code&gt;SIGSEGV&lt;/code&gt;. Note that the Go runtime captures SIGSEGV and turns it into a panic. See &lt;a href="https://pkg.go.dev/os/signal#hdr-Default_behavior_of_signals_in_Go_programs" rel="noopener noreferrer"&gt;Default behavior of signals in Go programs&lt;/a&gt;. JVM also does similar: catch SIGSEGV and throw NPE, see &lt;a href="https://docs.oracle.com/en/java/javase/21/troubleshoot/handle-signals-and-exceptions.html" rel="noopener noreferrer"&gt;Handle Signals and Exceptions&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Note that the Go runtime does not catch the SIGSEGV if CGO is enabled and the segfault occurs in C code. Similar to JVM, segfault in JNI are not caught either. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Log signal: &lt;code&gt;segfault at ... ip ... sp ... error 4&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Illegal Instruction&lt;/strong&gt;: A process executes a CPU instruction it doesn't understand, often from a binary compiled for the wrong architecture, or a corrupted executable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Log signal: &lt;code&gt;traps: invalid opcode ...&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Kernel Oops via a Driver&lt;/strong&gt;: A system call triggers a bug in a kernel driver. The kernel tries to stay alive but kills the offending process to stabilize the system.&lt;/p&gt;

&lt;p&gt;The common thread: none of these show up in the application's own logs. The process is gone before it can write anything. The evidence lives exclusively in the kernel ring buffer, accessible via &lt;code&gt;dmesg&lt;/code&gt; or &lt;code&gt;journalctl -k&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Config Change
&lt;/h2&gt;

&lt;p&gt;You can use &lt;a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/journaldreceiver" rel="noopener noreferrer"&gt;OpenTelemetry Collector journald receiver&lt;/a&gt; to collect kernel logs. However, the receiver needs to be separate from the service receiver. For example, in the following, there is one receiver for systemd service (journalctl -u) and another receiver for kernel log (journalctl -k).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;journald/service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/log/journal&lt;/span&gt;
    &lt;span class="na"&gt;units&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;foo&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;info&lt;/span&gt;
    &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;file_storage&lt;/span&gt;
    &lt;span class="na"&gt;start_at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;beginning&lt;/span&gt;

  &lt;span class="na"&gt;journald/kernel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/log/journal&lt;/span&gt;
    &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;_TRANSPORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kernel&lt;/span&gt;
    &lt;span class="na"&gt;grep&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(?i)(out&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;memory|killed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;process|memory&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cgroup&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;out&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;memory|segfault&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;at|traps:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;invalid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;opcode|general&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;protection)'&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;debug&lt;/span&gt;
    &lt;span class="na"&gt;start_at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;beginning&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth explaining.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why &lt;code&gt;priority: debug&lt;/code&gt; for the kernel receiver?
&lt;/h3&gt;

&lt;p&gt;Kernel messages don't follow the same severity conventions as application logs. Critical events like OOM kills are often emitted at &lt;code&gt;debug&lt;/code&gt; or &lt;code&gt;info&lt;/code&gt; priority in journald's view, not &lt;code&gt;err&lt;/code&gt; or &lt;code&gt;crit&lt;/code&gt;. If you set &lt;code&gt;priority: info&lt;/code&gt; or higher, you'll silently drop the exact messages you're looking for. Setting &lt;code&gt;priority: debug&lt;/code&gt; ensures the receiver reads all kernel messages and lets the &lt;code&gt;grep&lt;/code&gt; filter do the real work of keeping only what's relevant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why a separate receiver?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;units&lt;/code&gt; filter and &lt;code&gt;matches&lt;/code&gt; filter in the journald receiver are ANDed together when both are specified. That means combining &lt;code&gt;units: [foo]&lt;/code&gt; with &lt;code&gt;matches: [{_TRANSPORT: kernel}]&lt;/code&gt; would look for kernel messages that also belong to the &lt;code&gt;foo&lt;/code&gt; unit, which is contradictory. Kernel messages don't have a unit. There's also no OR logic at the receiver level. Multiple entries within &lt;code&gt;matches&lt;/code&gt; are ANDed as well. So if you want "service logs OR kernel logs", you need two receivers. The only alternative would be to drop all filtering and read everything from journald, then use a filter processor downstream, but that means ingesting far more data than you need, which defeats the purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Each journald receiver is a separate journalctl process
&lt;/h3&gt;

&lt;p&gt;OTEL doesn't read journald logs directly; it spawns a &lt;code&gt;journalctl&lt;/code&gt; process per receiver. This also means, to use the &lt;code&gt;grep&lt;/code&gt; filter in the receiver, systemd &lt;a href="https://raw.githubusercontent.com/systemd/systemd/v239/NEWS" rel="noopener noreferrer"&gt;v239+&lt;/a&gt; is required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bash-5.3# systemctl status otelcol
● otelcol.service - Executes open telemetry collector to send logs.
     Loaded: loaded (/x86_64-bottlerocket-linux-gnu/sys-root/usr/lib/systemd/system/otelcol.service; static)
     Active: active (running) since Sat 2026-03-07 00:07:51 UTC; 3 days ago
   Main PID: 2341 (otelcol)
      Tasks: 12 (limit: 9256)
     Memory: 25.5M
        CPU: 24min 5.574s
     CGroup: /system.slice/otelcol.service
             ├─2341 /usr/sbin/otelcol 
             ├─2348 journalctl --utc --output=json --follow --no-tail --unit foo --priority info --directory /var/log/journal --after-cursor xxxxx
             └─2349 journalctl --utc --output=json --follow --no-tail --priority debug --directory /var/log/journal _TRANSPORT=kernel --grep=xxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Appendix: Examples that trigger kernel kill
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Trigger OOM (scenario-oom.service)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Unit]
Description=Demo Scenario - OOM Kill
After=network.target

[Service]
Type=simple
MemoryMax=32M
# tail /dev/zero reads zeros endlessly into memory — hits the limit in seconds
ExecStart=/bin/bash -c 'echo "Eating memory..."; tail /dev/zero'
Restart=no

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Trigger segfault
&lt;/h3&gt;

&lt;p&gt;Go version&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="c"&gt;/*
// Dereference NULL in C — bypasses Go's runtime signal handler,
// so the CPU fault reaches the kernel and appears in journalctl -k:
//   "segfault at 0 ip ... error 4 in segfault[...]"
#include &amp;lt;stdlib.h&amp;gt;
void trigger_segfault() {
    volatile int *p = NULL;
    *p = 1;
}
*/&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"C"&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trigger_segfault&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;Python version&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;import&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Triggering segfault via null pointer dereference...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Trigger sigill
&lt;/h3&gt;

&lt;p&gt;Go version&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="c"&gt;/*
// Execute UD2 in C — bypasses Go's runtime signal handler,
// so the CPU trap reaches the kernel and appears in journalctl -k:
//   "traps: sigill[PID] trap invalid opcode ..."
void trigger_sigill() {
    __asm__("ud2");
}
*/&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"C"&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trigger_sigill&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;Python version&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;import&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mmap&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Executing UD2 (undefined instruction) to trigger SIGILL...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mmap&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;mmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PAGESIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prot&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PROT_READ&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;mmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PROT_WRITE&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;mmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PROT_EXEC&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="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\x0f\x0b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# UD2 opcode
&lt;/span&gt;&lt;span class="n"&gt;ftype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CFUNCTYPE&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="n"&gt;fptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c_char&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="nf"&gt;from_buffer&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;ctypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c_void_p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;ftype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fptr&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>linux</category>
    </item>
    <item>
      <title>You probably want to disable cgo: Go's stdlib has pure-Go implementations</title>
      <dc:creator>Peng Zhang</dc:creator>
      <pubDate>Tue, 31 Mar 2026 05:48:14 +0000</pubDate>
      <link>https://dev.to/pengubco/you-probably-want-to-disable-cgo-gos-stdlib-has-pure-go-implementations-3f65</link>
      <guid>https://dev.to/pengubco/you-probably-want-to-disable-cgo-gos-stdlib-has-pure-go-implementations-3f65</guid>
      <description>&lt;p&gt;&lt;code&gt;CGO_ENABLED&lt;/code&gt; defaults to &lt;code&gt;1&lt;/code&gt;. That means a standard &lt;code&gt;go build&lt;/code&gt; produces a binary that links against C libraries (e.g., glibc) at runtime. For many parts of the Go standard library, there is a C-backed implementation and a pure-Go implementation. &lt;code&gt;CGO_ENABLED&lt;/code&gt; selects which one gets compiled in. Pure-Go alternatives also exist for third-party libraries, so it is likely you can turn off cgo by setting &lt;code&gt;CGO_ENABLED=0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Whether to use C libraries or not is a build-time decision, not a runtime fallback like "foo.so doesn't exist, fall back to pure-Go". If the required &lt;code&gt;.so&lt;/code&gt; is missing when the cgo binary runs, the dynamic linker fails immediately with an error like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;./dns-with-cgo: error while loading shared libraries: libresolv.so.2: cannot open shared object file: No such file or directory
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example: DNS resolution with net.LookupHost
&lt;/h2&gt;

&lt;p&gt;With cgo enabled, &lt;code&gt;net.LookupHost&lt;/code&gt; delegates to glibc's &lt;code&gt;getaddrinfo&lt;/code&gt; via &lt;code&gt;libresolv.so.2&lt;/code&gt;. With cgo disabled, the same call uses Go's built-in resolver that reads &lt;code&gt;/etc/resolv.conf&lt;/code&gt; directly, with no C library involved. Same API either way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"net"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;addrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LookupHost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"amazon.com"&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"error:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"resolved:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;addrs&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;Build it both ways:&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;# cgo on (default)&lt;/span&gt;
go build &lt;span class="nt"&gt;-o&lt;/span&gt; dns-with-cgo &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# cgo off&lt;/span&gt;
&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 go build &lt;span class="nt"&gt;-o&lt;/span&gt; dns-no-cgo &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ldd shows the difference
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;% file dns-with-cgo
dns-with-cgo: ELF 64-bit LSB executable, x86-64, version 1 &lt;span class="o"&gt;(&lt;/span&gt;SYSV&lt;span class="o"&gt;)&lt;/span&gt;,     
dynamically linked &lt;span class="o"&gt;(&lt;/span&gt;uses shared libs&lt;span class="o"&gt;)&lt;/span&gt;, BuildID[sha1]&lt;span class="o"&gt;=&lt;/span&gt;4d51b80894921ca4be742c64a7dd76c4c7205697, not stripped

% ldd dns-with-cgo 
        linux-vdso.so.1 &lt;span class="o"&gt;(&lt;/span&gt;0x00007ffd8efb9000&lt;span class="o"&gt;)&lt;/span&gt;
        libresolv.so.2 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; /lib64/libresolv.so.2 &lt;span class="o"&gt;(&lt;/span&gt;0x00007f5bc44b8000&lt;span class="o"&gt;)&lt;/span&gt;
        libpthread.so.0 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; /lib64/libpthread.so.0 &lt;span class="o"&gt;(&lt;/span&gt;0x00007f5bc429a000&lt;span class="o"&gt;)&lt;/span&gt;
        libc.so.6 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; /lib64/libc.so.6 &lt;span class="o"&gt;(&lt;/span&gt;0x00007f5bc3eed000&lt;span class="o"&gt;)&lt;/span&gt;
        /lib64/ld-linux-x86-64.so.2 &lt;span class="o"&gt;(&lt;/span&gt;0x00007f5bc46ce000&lt;span class="o"&gt;)&lt;/span&gt;

% file dns-no-cgo 
dns-no-cgo: ELF 64-bit LSB executable, x86-64, version 1 &lt;span class="o"&gt;(&lt;/span&gt;SYSV&lt;span class="o"&gt;)&lt;/span&gt;, 
statically linked, BuildID[sha1]&lt;span class="o"&gt;=&lt;/span&gt;e8c17c6cb53d034c052336acd4b83a09527bd22c, not stripped

% ldd dns-no-cgo 
        not a dynamic executable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cgo binary dynamically links against &lt;code&gt;libresolv.so.2&lt;/code&gt;, &lt;code&gt;libc.so.6&lt;/code&gt;, and other glibc libraries. The &lt;code&gt;CGO_ENABLED=0&lt;/code&gt; binary is fully static, no shared libraries at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  strace confirms it at runtime
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# cgo on: loads libresolv.so.2, then delegates to glibc&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;openat &lt;span class="nt"&gt;-f&lt;/span&gt; ./dns-with-cgo 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"(resolv|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;conf)"&lt;/span&gt;
openat&lt;span class="o"&gt;(&lt;/span&gt;AT_FDCWD, &lt;span class="s2"&gt;"/lib64/libresolv.so.2"&lt;/span&gt;, O_RDONLY|O_CLOEXEC&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 3 &lt;span class="c"&gt;# &amp;lt;-- glibc dependency&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;pid  1553] openat&lt;span class="o"&gt;(&lt;/span&gt;AT_FDCWD, &lt;span class="s2"&gt;"/etc/resolv.conf"&lt;/span&gt;, O_RDONLY|O_CLOEXEC&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 5
&lt;span class="o"&gt;[&lt;/span&gt;pid  1553] openat&lt;span class="o"&gt;(&lt;/span&gt;AT_FDCWD, &lt;span class="s2"&gt;"/etc/nsswitch.conf"&lt;/span&gt;, O_RDONLY|O_CLOEXEC&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 5
resolved: &lt;span class="o"&gt;[&lt;/span&gt;98.82.161.185 98.87.170.74 98.87.170.71]

&lt;span class="c"&gt;# cgo off: Go reads config directly, no .so loaded&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;openat &lt;span class="nt"&gt;-f&lt;/span&gt; ./dns-no-cgo 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"(resolv|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;conf)"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;pid  2326] openat&lt;span class="o"&gt;(&lt;/span&gt;AT_FDCWD, &lt;span class="s2"&gt;"/etc/resolv.conf"&lt;/span&gt;, O_RDONLY|O_CLOEXEC&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 5
&lt;span class="o"&gt;[&lt;/span&gt;pid  2326] openat&lt;span class="o"&gt;(&lt;/span&gt;AT_FDCWD, &lt;span class="s2"&gt;"/etc/nsswitch.conf"&lt;/span&gt;, O_RDONLY|O_CLOEXEC&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 5
resolved: &lt;span class="o"&gt;[&lt;/span&gt;98.82.161.185 98.87.170.74 98.87.170.71]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With cgo, the first thing that happens is loading the C library. Without cgo, Go skips straight to reading the resolver config itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CGO_ENABLED=0 matters
&lt;/h2&gt;

&lt;p&gt;A cgo binary built on one Linux may depend on &lt;code&gt;libc.so.6&lt;/code&gt; being present and ABI-compatible on the target. That's usually fine, but it's an implicit runtime dependency you may not notice until deployment fails on a minimal OS, or an OS with incompatible libc versions, or inside a scratch container image with no libc. &lt;code&gt;CGO_ENABLED=0&lt;/code&gt; removes that dependency entirely. The binary becomes fully static and runs on any Linux system regardless of what libc is installed.&lt;/p&gt;

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

&lt;p&gt;Go's pure-Go implementations are not always full replacements. The pure-Go DNS resolver doesn't support all NSS plugins, so environments relying on custom NSS modules for service discovery or LDAP may see different behavior. For standard DNS over &lt;code&gt;/etc/resolv.conf&lt;/code&gt;, it works fine.&lt;/p&gt;

&lt;p&gt;Wherever the stdlib has both a cgo and a pure-Go path, &lt;code&gt;CGO_ENABLED=0&lt;/code&gt; at build time opts into the pure-Go one. You get a simpler, more portable binary at the cost of potentially narrower feature coverage in those specific areas. Just remember the choice is made when you run &lt;code&gt;go build&lt;/code&gt;, not when the binary runs on the target machine.&lt;/p&gt;

</description>
      <category>go</category>
    </item>
  </channel>
</rss>
