<?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: Bartosz Osiej</title>
    <description>The latest articles on DEV Community by Bartosz Osiej (@bartoszosiej).</description>
    <link>https://dev.to/bartoszosiej</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%2F4088500%2F7c989efc-dcb0-43df-9909-e04efd402bdf.png</url>
      <title>DEV Community: Bartosz Osiej</title>
      <link>https://dev.to/bartoszosiej</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bartoszosiej"/>
    <language>en</language>
    <item>
      <title>Linux Process Monitoring: From /proc to eBPF</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Tue, 20 Oct 2026 16:00:37 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/linux-process-monitoring-from-proc-to-ebpf-9ii</link>
      <guid>https://dev.to/bartoszosiej/linux-process-monitoring-from-proc-to-ebpf-9ii</guid>
      <description>&lt;h2&gt;
  
  
  Three generations
&lt;/h2&gt;

&lt;p&gt;Linux process monitoring moved through three generations, and most orgs are&lt;br&gt;
still on generation two while reading alerts from generation three.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Generation&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Strengths&lt;/th&gt;
&lt;th&gt;Weaknesses&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. &lt;code&gt;/proc&lt;/code&gt; polling&lt;/td&gt;
&lt;td&gt;read &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/stat&lt;/code&gt; every N seconds&lt;/td&gt;
&lt;td&gt;trivially portable, no deps&lt;/td&gt;
&lt;td&gt;lossy, high overhead at scale, no events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. &lt;code&gt;ptrace&lt;/code&gt; + &lt;code&gt;exec&lt;/code&gt; hooks&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;strace&lt;/code&gt;-style attach&lt;/td&gt;
&lt;td&gt;precise, per-syscall&lt;/td&gt;
&lt;td&gt;stops the world on every syscall, no fleet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. eBPF tracepoints&lt;/td&gt;
&lt;td&gt;kernel callback, no attach cost&lt;/td&gt;
&lt;td&gt;lossless, kernel-attributed, fleet-capable&lt;/td&gt;
&lt;td&gt;requires BTF, kernel 5.8+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Generation 1: &lt;code&gt;/proc&lt;/code&gt; polling
&lt;/h2&gt;

&lt;p&gt;Everyone starts here. A cron or daemon loops &lt;code&gt;for pid in /proc/[0-9]*&lt;/code&gt; and reads&lt;br&gt;
&lt;code&gt;stat&lt;/code&gt; to snapshot &lt;code&gt;utime&lt;/code&gt;/&lt;code&gt;stime&lt;/code&gt;/&lt;code&gt;state&lt;/code&gt;, then diffs between polls to derive&lt;br&gt;
CPU% and detect a process that opened many files.&lt;/p&gt;

&lt;p&gt;Problems that bite by year two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polling is lossy.&lt;/strong&gt; A process that opens 5000 files and exits in 50 ms
between 1s polls is invisible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overhead scales as O(pids × fields × freqs)&lt;/strong&gt;. On a 16k-PID CI box polled
at 10 Hz you are reading ~160k files/sec of kernel state — you spend more
time reading &lt;code&gt;/proc&lt;/code&gt; than the workload does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No parent/child lineage.&lt;/strong&gt; Reconstructing the tree means storing state per
sample and stitching PPID across polls — racy and expensive.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Generation 2: ptrace/exec
&lt;/h2&gt;

&lt;p&gt;Tools like &lt;code&gt;strace&lt;/code&gt;, &lt;code&gt;sysdig&lt;/code&gt;, and early eBPF wrappers use &lt;code&gt;ptrace(PTRACE_ATTACH)&lt;/code&gt;&lt;br&gt;
or seccomp-notify to get per-event visibility. Precise, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ptrace&lt;/code&gt; &lt;strong&gt;stops the tracee&lt;/strong&gt; on every syscall — throughput collapses.&lt;/li&gt;
&lt;li&gt;You attach to what you &lt;em&gt;know&lt;/em&gt; to attach to. Unknown binaries launched by an
attacker are missed unless you already hook &lt;code&gt;exec&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;State is local to one machine; nothing here speaks fleet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;sysdig (pre-3.x) is this generation's poster child: brilliant forensics, poor&lt;br&gt;
for always-on production load.&lt;/p&gt;
&lt;h2&gt;
  
  
  Generation 3: eBPF tracepoints
&lt;/h2&gt;

&lt;p&gt;eBPF tracepoints attach in kernel space and never stop the traced task. You get&lt;br&gt;
an event per syscall entry/exit with a &lt;strong&gt;kernel-attributed pid&lt;/strong&gt;, a timestamp,&lt;br&gt;
and a ring-buffer export to userspace. The program is verified at load time —&lt;br&gt;
if it can crash the kernel, it won't load.&lt;/p&gt;

&lt;p&gt;The modern pattern is &lt;strong&gt;not&lt;/strong&gt; raw &lt;code&gt;bpftrace&lt;/code&gt; one-liners (generation 2.5) but a&lt;br&gt;
&lt;strong&gt;per-process scorer&lt;/strong&gt;: maintain a map keyed by &lt;code&gt;pid&lt;/code&gt; (or better, by&lt;br&gt;
&lt;code&gt;pid + start_time&lt;/code&gt; to defeat PID recycling), increment a counter on&lt;br&gt;
&lt;code&gt;openat&lt;/code&gt;-for-write, and emit a verdict when the windowed rate exceeds a&lt;br&gt;
threshold. That is the core of Talus's detector, and it is ~120 lines of eBPF&lt;br&gt;
plus a ~200-line ring-buffer drain in Rust.&lt;/p&gt;

&lt;p&gt;Key correctness notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Window size matters.&lt;/strong&gt; 1-second windows give same-second verdicts; 10s
windows give you alerts after encryption. Talus ships 1s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path vs name.&lt;/strong&gt; Matching on the binary name is spoofable; matching on the
binary &lt;strong&gt;path + inode&lt;/strong&gt; and the open &lt;strong&gt;path&lt;/strong&gt; is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PID recycling&lt;/strong&gt; is real under churn. Key maps by &lt;code&gt;pid + start_time&lt;/code&gt;; that is
what Talus does, and what naive &lt;code&gt;/proc&lt;/code&gt; scrapers forget.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Fleet: the missing third act
&lt;/h2&gt;

&lt;p&gt;Single-host eBPF monitoring is a solved problem (run the binary). Fleet is&lt;br&gt;
where generation 3 beats 1+2 decisively: a single agent per host streams events&lt;br&gt;
to a central sink (Kafka / ClickHouse / MemGraph), and the verdict engine&lt;br&gt;
correlates across hostnames by &lt;code&gt;machine-id&lt;/code&gt;. That is how you spot a lateral&lt;br&gt;
spray that opens files on 3 hosts within 200 ms.&lt;/p&gt;

&lt;p&gt;Talus ships that sink and an Enterprise fleet dashboard; the agent stays a&lt;br&gt;
single static binary per host. Per-machine license:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;talus-process-monitor
talus license activate &amp;lt;KEY&amp;gt;
talus monitor &lt;span class="nt"&gt;--auto-kill&lt;/span&gt;        &lt;span class="c"&gt;# detects + SIGKILL + streams to fleet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://hartwell-labs.pl/talus-process-monitor/pricing.html?utm_source=blog&amp;amp;utm_medium=cta&amp;amp;utm_campaign=linux-process-monitoring" rel="noopener noreferrer"&gt;
   class="buy-button"&amp;gt;View fleet pricing →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;{"&lt;a class="mentioned-user" href="https://dev.to/context"&gt;@context&lt;/a&gt;":"&lt;a href="https://schema.org%22,%22@type%22:%22Article" rel="noopener noreferrer"&gt;https://schema.org","@type":"Article&lt;/a&gt;",&lt;br&gt;
"headline":"Linux Process Monitoring: From /proc to eBPF",&lt;br&gt;
"description":"Three generations of Linux process monitoring and why eBPF tracepoints win for per-process, fleet-scale detection.",&lt;br&gt;
"author":{"@type":"Person","name":"Bartosz Osiej"},&lt;br&gt;
"datePublished":"2026-10-20","dateModified":"2026-10-20",&lt;br&gt;
"mainEntityOfPage":{"@type":"WebPage","&lt;a class="mentioned-user" href="https://dev.to/id"&gt;@id&lt;/a&gt;":"&lt;a href="https://bo.hartwell-labs.pl/content/linux-process-monitoring-proc-to-ebpf%22%7D" rel="noopener noreferrer"&gt;https://bo.hartwell-labs.pl/content/linux-process-monitoring-proc-to-ebpf"}&lt;/a&gt;}&lt;/p&gt;

</description>
      <category>linux</category>
      <category>ebpf</category>
      <category>monitoring</category>
      <category>processmonitor</category>
    </item>
    <item>
      <title>Fleet Monitoring for Linux: Managing 100+ Servers Without Kubernetes</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Tue, 20 Oct 2026 16:00:03 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/fleet-monitoring-for-linux-managing-100-servers-without-kubernetes-550n</link>
      <guid>https://dev.to/bartoszosiej/fleet-monitoring-for-linux-managing-100-servers-without-kubernetes-550n</guid>
      <description>&lt;h2&gt;
  
  
  The misconception
&lt;/h2&gt;

&lt;p&gt;"Fleet monitoring = Kubernetes monitoring." It is not. Kubernetes is a&lt;br&gt;
scheduling/orchestration control plane that some fleets happen to use. Most&lt;br&gt;
Linux fleets — edge, bare metal, on-prem, air gaps, mixed distros — are just&lt;br&gt;
hosts that need watching. You should not have to stand up a cluster to watch&lt;br&gt;
them.&lt;/p&gt;

&lt;p&gt;The right mental model: &lt;strong&gt;an agent per host&lt;/strong&gt; (one static binary), &lt;strong&gt;a shared&lt;br&gt;
event sink&lt;/strong&gt;, and &lt;strong&gt;one dashboard keyed to a per-host identity&lt;/strong&gt; that survives&lt;br&gt;
re-imaging. Everything else is implementation detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why agents that are "just a CLI" fail at fleet scale
&lt;/h2&gt;

&lt;p&gt;Tools that work on one host and fall apart at 100 nodes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They write state to &lt;code&gt;/tmp&lt;/code&gt; keyed by hostname → PID/host confusion when hosts
are rebuilt.&lt;/li&gt;
&lt;li&gt;They assume a single machine-id, so a fleet dashboard can't tell host A from
host B after a re-image.&lt;/li&gt;
&lt;li&gt;They have no fleet identity, so correlation is "grep the logs by hostname"
(slow, lossy) instead of "join on machine-id."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Talus's fleet model is deliberately the anti-silo: every event carries the&lt;br&gt;
host's &lt;code&gt;machine-id&lt;/code&gt; (and a per-machine license seat), and the agents all speak&lt;br&gt;
the same wire format to a shared sink.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fleet stack (one binary, no cluster)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;host A        host B        host N
  |             |             |
talus          talus         talus
(1 static      (same          (same
 binary)       binary)        binary)
  |             |             |
  +------+------+------+-----+/
         |               |
    Kafka / ClickHouse   MemGraph (process tree)
         |
   dashboard + SIEM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent&lt;/strong&gt;: one ELF (x86_64 + aarch64), eBPF tracepoints, signed audit log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sink&lt;/strong&gt;: Kafka topics, ClickHouse, or MemGraph — pick one, all ships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Licensing&lt;/strong&gt;: per-machine annual keys, &lt;code&gt;machine-id&lt;/code&gt;-pinned, server-side seat
enforcement. Fleet tier is &lt;code&gt;$30/node/year&lt;/code&gt; from 3 nodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deploy that survives chaos
&lt;/h2&gt;

&lt;p&gt;At 100+ hosts the deployment story is what bites first. Talus's pattern that&lt;br&gt;
works on mixed fleets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One package.&lt;/strong&gt; &lt;code&gt;pip install talus-process-monitor&lt;/code&gt; on every distro, or the
static binary from GitHub Releases — no package-manager wars.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root once, run without it.&lt;/strong&gt; eBPF load needs root/CAP_BPF at attach; after
that the drain and scorer drop privileges via seccomp + Landlock. That is why
Talus self-sandboxes — it matters more on a fleet than on one box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity via &lt;code&gt;machine-id&lt;/code&gt;.&lt;/strong&gt; Each agent registers with
&lt;code&gt;/etc/machine-id&lt;/code&gt; + a per-node license. Re-imaging a node? &lt;code&gt;talus license
deactivate&lt;/code&gt; on the old, &lt;code&gt;activate&lt;/code&gt; on the new — seats stay honest, no
double-billing.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# fleet deploy example (any CM, here shaped as Ansible)&lt;/span&gt;
- hosts: all
  tasks:
    - name: &lt;span class="nb"&gt;install &lt;/span&gt;talus
      ansible.builtin.command: talus &lt;span class="nb"&gt;install&lt;/span&gt;
    - name: activate license
      ansible.builtin.command: talus license activate &lt;span class="o"&gt;{{&lt;/span&gt; talus_license_key &lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fleet threat detection you can actually read
&lt;/h2&gt;

&lt;p&gt;The killer use-case is not "one host" but &lt;strong&gt;cross-host timing&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10:03:17 hostA opens 41k files — kill.&lt;/li&gt;
&lt;li&gt;10:03:18 hostB opens 18k files — kill (lateral from A in &amp;lt;1 s).&lt;/li&gt;
&lt;li&gt;10:03:19 hostC — blocked before it started, because the sink saw B's canary
and pre-empted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That timing is invisible to per-host-only tools and invisible to SIEMs fed by&lt;br&gt;
polled logs. Kernel-attached agents feeding a shared sink are the only way to&lt;br&gt;
see sub-second lateral moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing seats and cost
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fleet license: &lt;code&gt;$30 / node / year&lt;/code&gt;, min 3 nodes, max 500, cancel any time.&lt;/li&gt;
&lt;li&gt;A node that goes dark stops billing only when you &lt;code&gt;deactivate&lt;/code&gt; it — there is
no "phantom usage" trap.&lt;/li&gt;
&lt;li&gt;Batch prepaid keys exist if procurement needs PO + net-30;
&lt;code&gt;contact@hartwell-labs.pl&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://hartwell-labs.pl/talus-process-monitor/pricing.html?utm_source=blog&amp;amp;utm_medium=cta&amp;amp;utm_campaign=fleet-monitoring-linux" rel="noopener noreferrer"&gt;
   class="buy-button"&amp;gt;Fleet pricing ($30/node) →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;{"&lt;a class="mentioned-user" href="https://dev.to/context"&gt;@context&lt;/a&gt;":"&lt;a href="https://schema.org%22,%22@type%22:%22Article" rel="noopener noreferrer"&gt;https://schema.org","@type":"Article&lt;/a&gt;",&lt;br&gt;
"headline":"Fleet Monitoring for Linux: Managing 100+ Servers Without Kubernetes",&lt;br&gt;
"description":"eBPF agent-per-host fleet monitoring without Kubernetes: per-machine licensing, shared Kafka sink, cross-host correlation.",&lt;br&gt;
"author":{"@type":"Person","name":"Bartosz Osiej"},&lt;br&gt;
"datePublished":"2026-10-20","dateModified":"2026-10-20",&lt;br&gt;
"mainEntityOfPage":{"@type":"WebPage","&lt;a class="mentioned-user" href="https://dev.to/id"&gt;@id&lt;/a&gt;":"&lt;a href="https://bo.hartwell-labs.pl/content/fleet-monitoring-linux%22%7D" rel="noopener noreferrer"&gt;https://bo.hartwell-labs.pl/content/fleet-monitoring-linux"}&lt;/a&gt;}&lt;/p&gt;

</description>
      <category>linux</category>
      <category>ebpf</category>
      <category>fleet</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>eBPF vs Traditional Monitoring: Why Kernel-Level Wins</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Tue, 20 Oct 2026 16:00:00 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/ebpf-vs-traditional-monitoring-why-kernel-level-wins-9jc</link>
      <guid>https://dev.to/bartoszosiej/ebpf-vs-traditional-monitoring-why-kernel-level-wins-9jc</guid>
      <description>&lt;h2&gt;
  
  
  The latency tax of polling
&lt;/h2&gt;

&lt;p&gt;Traditional Linux monitoring is built on two lies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"Sampling every second is good enough."&lt;/li&gt;
&lt;li&gt;"Logs arrive in order and are never dropped."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both break under load. A ransomware process that opens 4000 files in 120 ms&lt;br&gt;
between two 1s polls is invisible to generation-1 &lt;code&gt;/proc&lt;/code&gt; scrapers. A&lt;br&gt;
&lt;code&gt;journald&lt;/code&gt;/syslog pipeline that buffers under burst &lt;strong&gt;reorders&lt;/strong&gt; events, so the&lt;br&gt;
alert that says "encryptor X ran" arrives after X has already finished.&lt;/p&gt;

&lt;p&gt;eBPF does not have this problem. An eBPF tracepoint fires &lt;strong&gt;in kernel space at&lt;br&gt;
the syscall boundary&lt;/strong&gt; — the event is read by userspace from a lockless ring&lt;br&gt;
buffer, with a timestamp taken before the syscall returns to userspace. There is&lt;br&gt;
no polling interval to miss, no log line to drop, no context switch to stall.&lt;/p&gt;

&lt;h2&gt;
  
  
  The overhead inversion
&lt;/h2&gt;

&lt;p&gt;The counterintuitive result, which I verified while writing&lt;br&gt;
&lt;a href="https://bo.hartwell-labs.pl/content/ebpf-ransomware-monitor-lessons" rel="noopener noreferrer"&gt;ebpf-ransomware-monitor-lessons&lt;/a&gt;,&lt;br&gt;
is that eBPF monitoring has &lt;strong&gt;lower overhead&lt;/strong&gt; than doing nothing under pressure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ptrace(PTRACE_ATTACH)&lt;/code&gt; stops the tracee on every syscall — throughput
collapses the instant you attach broadly. eBPF does &lt;strong&gt;not&lt;/strong&gt; stop the task; it
runs a verified callback and returns to userspace.&lt;/li&gt;
&lt;li&gt;Parsing &lt;code&gt;/proc&lt;/code&gt; is O(pids) and grows with process churn; eBPF is O(events)
and the kernel already had those events.&lt;/li&gt;
&lt;li&gt;At &amp;gt;100k open/s the &lt;code&gt;/proc&lt;/code&gt; scraper starts competing with the workload for
inode and dentry locks; the eBPF program does not touch those locks at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Accuracy: attribution that can't be faked
&lt;/h2&gt;

&lt;p&gt;Traditional tools attribute work to a &lt;strong&gt;PID&lt;/strong&gt; (a number that recycles) or to&lt;br&gt;
&lt;strong&gt;stdout/stderr text&lt;/strong&gt; (which an attacker can write to selectively). eBPF&lt;br&gt;
tracepoints come with the kernel's own view: pid, tgid, uid, gid, absolute&lt;br&gt;
inode, and &lt;code&gt;task-&amp;gt;comm&lt;/code&gt; — all kernel-attributed, none spoofable from userspace.&lt;/p&gt;

&lt;p&gt;For ransomware this is the difference between "alert: something wrote files" and&lt;br&gt;
"alert: pid 3412 (&lt;code&gt;systemd-resolve&lt;/code&gt;... no, a renamed &lt;code&gt;python&lt;/code&gt; with inode&lt;br&gt;
&lt;code&gt;/var/lib/.x/enc&lt;/code&gt;) opened 3412 files for write in 0.8 s and is renaming each&lt;br&gt;
&lt;code&gt;.doc&lt;/code&gt; → &lt;code&gt;.doc.ez&lt;/code&gt;." The second one kills processes safely; the first one pages&lt;br&gt;
an on-call engineer at 3 a.m. for a backup job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Response without round-trips
&lt;/h2&gt;

&lt;p&gt;Traditional monitors detect, page, and wait for a human. eBPF programs can&lt;br&gt;
&lt;strong&gt;act in the same kernel boundary&lt;/strong&gt;: queue a &lt;code&gt;SIGKILL&lt;/code&gt;, open a Landlock&lt;br&gt;
restrict, or drop network — before the syscall returns to userspace. That is&lt;br&gt;
not theoretical: Talus's &lt;code&gt;--auto-kill&lt;/code&gt; fires &lt;code&gt;SIGKILL&lt;/code&gt; in under 1 s of the first&lt;br&gt;
mass-open, on the same box, no network hop.&lt;/p&gt;

&lt;h2&gt;
  
  
  When traditional still wins
&lt;/h2&gt;

&lt;p&gt;eBPF is not magic duct tape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Portability to old kernels.&lt;/strong&gt; eBPF CO-RE needs 5.8+ / BTF. Ships in 2020,
but some long-term enterprise distros still lag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-off debugging.&lt;/strong&gt; &lt;code&gt;strace&lt;/code&gt; remains the fastest way to answer "what did my
one test do just now." eBPF tools like bpftrace own this too, but the mental
model is heavier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance retention.&lt;/strong&gt; Some auditors still ask for logs you can grep in
vim at 2 a.m. eBPF pipelines still ship to a log sink; it is complementary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The kernel-level conclusion
&lt;/h2&gt;

&lt;p&gt;Monitoring that lives in userspace and polls is, by construction, watching the&lt;br&gt;
past. Monitoring that lives in the kernel with eBPF is watching the present —&lt;br&gt;
and can act before it becomes history. That is why Talus ships its detector as&lt;br&gt;
an eBPF program and its response guardrails in the same one-second window.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hartwell-labs.pl/talus-process-monitor/?utm_source=blog&amp;amp;utm_medium=cta&amp;amp;utm_campaign=ebpf-vs-traditional-monitoring" rel="noopener noreferrer"&gt;
   class="buy-button"&amp;gt;Run Talus on your servers →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;{"&lt;a class="mentioned-user" href="https://dev.to/context"&gt;@context&lt;/a&gt;":"&lt;a href="https://schema.org%22,%22@type%22:%22Article" rel="noopener noreferrer"&gt;https://schema.org","@type":"Article&lt;/a&gt;",&lt;br&gt;
"headline":"eBPF vs Traditional Monitoring: Why Kernel-Level Wins",&lt;br&gt;
"description":"Kernel-attached eBPF events beat polling and log parsing for accuracy, overhead and same-second response.",&lt;br&gt;
"author":{"@type":"Person","name":"Bartosz Osiej"},&lt;br&gt;
"datePublished":"2026-10-20","dateModified":"2026-10-20",&lt;br&gt;
"mainEntityOfPage":{"@type":"WebPage","&lt;a class="mentioned-user" href="https://dev.to/id"&gt;@id&lt;/a&gt;":"&lt;a href="https://bo.hartwell-labs.pl/content/ebpf-vs-traditional-monitoring%22%7D" rel="noopener noreferrer"&gt;https://bo.hartwell-labs.pl/content/ebpf-vs-traditional-monitoring"}&lt;/a&gt;}&lt;/p&gt;

</description>
      <category>ebpf</category>
      <category>monitoring</category>
      <category>linux</category>
      <category>performance</category>
    </item>
    <item>
      <title>How to Detect Ransomware Activity on Linux with eBPF</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:59:29 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/how-to-detect-ransomware-activity-on-linux-with-ebpf-3b1i</link>
      <guid>https://dev.to/bartoszosiej/how-to-detect-ransomware-activity-on-linux-with-ebpf-3b1i</guid>
      <description>&lt;h2&gt;
  
  
  The shape of a Linux ransomware hit
&lt;/h2&gt;

&lt;p&gt;Ransomware on Linux does not usually announce itself with a GUI. It looks like&lt;br&gt;
a normal process that, in a short window, opens a large number of files for&lt;br&gt;
writing, writes to them, and renames them to a new extension. The signatures&lt;br&gt;
are always new; the &lt;strong&gt;shape&lt;/strong&gt; is stable. That shape is what eBPF is built to&lt;br&gt;
score.&lt;/p&gt;

&lt;p&gt;This is the same material I wrote up in&lt;br&gt;
&lt;a href="https://bo.hartwell-labs.pl/content/ebpf-ransomware-monitor-lessons" rel="noopener noreferrer"&gt;ebpf-ransomware-monitor-lessons&lt;/a&gt;,&lt;br&gt;
but here I focus on the &lt;strong&gt;detection steps&lt;/strong&gt; you can implement yourself, and&lt;br&gt;
where Talus takes over so you do not maintain them.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why eBPF, and not syslog or /proc
&lt;/h2&gt;

&lt;p&gt;Polling &lt;code&gt;/proc/&amp;lt;pid&amp;gt;&lt;/code&gt; or parsing &lt;code&gt;auditd&lt;/code&gt; logs is, by construction, &lt;strong&gt;after the&lt;br&gt;
fact&lt;/strong&gt; and lossy under load. eBPF runs at the kernel boundary: you get every&lt;br&gt;
&lt;code&gt;openat&lt;/code&gt;, &lt;code&gt;rename&lt;/code&gt;, &lt;code&gt;unlink&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt; event as it happens, in kernel space,&lt;br&gt;
with a per-process attribution that never drops events even at ring-buffer&lt;br&gt;
saturation (you get a &lt;code&gt;lost&lt;/code&gt; counter, not silence).&lt;/p&gt;

&lt;p&gt;The two eBPF tracepoints that carry the signal are&lt;br&gt;
&lt;code&gt;tracepoint/syscalls/sys_enter_openat&lt;/code&gt; (and the &lt;code&gt;exit_*&lt;/code&gt; pair) and&lt;br&gt;
&lt;code&gt;tracepoint/syscalls/sys_enter_unlink&lt;/code&gt;/&lt;code&gt;rename&lt;/code&gt;. You do &lt;strong&gt;not&lt;/strong&gt; need full&lt;br&gt;
&lt;code&gt;ptrace&lt;/code&gt; overhead — tracepoints are stable ABI across kernel minor versions when&lt;br&gt;
paired with BTF (Linux 5.8+, every distro since 2020).&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: score open-rate per process
&lt;/h2&gt;

&lt;p&gt;Maintain a per-&lt;code&gt;(pid)&lt;/code&gt; counter, increment on each write-capable &lt;code&gt;openat&lt;/code&gt; with&lt;br&gt;
&lt;code&gt;O_WRONLY|O_CREAT&lt;/code&gt; or &lt;code&gt;O_RDWR&lt;/code&gt;, and roll it over every &lt;strong&gt;1-second window&lt;/strong&gt;.&lt;br&gt;
The decision rule is embarrassingly simple and catches every public encryptor&lt;br&gt;
I have benchmarked it against:&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;/* pseudocode, maps to the kernel-space program in process-monitor-ebpf */&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;pid_count&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;verdict&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ALERT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start with &lt;code&gt;THRESHOLD=50&lt;/code&gt; files/second. On a real host the background load&lt;br&gt;
(normal &lt;code&gt;rsync&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, a compiler) is usually under 10/s; a mass-encryptor is&lt;br&gt;
in the hundreds. Tune down to 20/s only if you have many compilers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A common mistake is using &lt;code&gt;cpu_cycle&lt;/code&gt; counters instead of &lt;strong&gt;file operations&lt;/strong&gt;.&lt;br&gt;
A process can burn CPU and be innocent; rapid &lt;strong&gt;file opens for write&lt;/strong&gt; is the&lt;br&gt;
behavioral invariant of ransomware, not CPU.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Step 2: the canary read
&lt;/h2&gt;

&lt;p&gt;Rate-scoring gives false positives on &lt;code&gt;rsync&lt;/code&gt;, &lt;code&gt;git checkout&lt;/code&gt;, backup jobs.&lt;br&gt;
Add a &lt;strong&gt;canary read&lt;/strong&gt;: a decoy file placed on each host (&lt;code&gt;.canary&lt;/code&gt; in &lt;code&gt;/var/lib&lt;/code&gt;).&lt;br&gt;
Any process that opens the canary and then starts writing to many other files&lt;br&gt;
is behaving like ransomware, not like a backup tool that happens to walk the&lt;br&gt;
tree. The canary read is cheap (one extra tracepoint match) and cuts noise&lt;br&gt;
dramatically.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3: the kill guardrail
&lt;/h2&gt;

&lt;p&gt;When verdict fires, you must terminate the process — but recklessness kills&lt;br&gt;
production. Talus's kill path enforces four guardrails, all of which I would&lt;br&gt;
implement for an in-house agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Whitelist by path and binary name.&lt;/strong&gt; &lt;code&gt;/usr/bin/rsync&lt;/code&gt;, &lt;code&gt;/usr/sbin/anacron&lt;/code&gt;,
the backup tool's real path — never kill these.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canary verification.&lt;/strong&gt; Only kill if the canary was read within the window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parent attribution.&lt;/strong&gt; Kill the encryptor, not the shell that launched it
unless the parent also opened files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limit.&lt;/strong&gt; Max one kill per process per minute to prevent a looping
process from DOS-ing the host.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Step 4: correlate across the fleet
&lt;/h2&gt;

&lt;p&gt;A single host can be patient-zero. The real value of fleet monitoring is&lt;br&gt;
seeing &lt;strong&gt;that three hosts in the same subnet started encrypting within 200 ms of&lt;br&gt;
each other&lt;/strong&gt; and cutting lateral movement before it lands. Talus streams events&lt;br&gt;
to Kafka/ClickHouse and lets you key on &lt;code&gt;hostname&lt;/code&gt;, &lt;code&gt;pid&lt;/code&gt;, and the&lt;br&gt;
&lt;code&gt;machine-id&lt;/code&gt;-derived fleet tag.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tuning notes from real boxes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Threshold &lt;strong&gt;50&lt;/strong&gt; is the sweet spot for &amp;gt;95% of servers. Database
&lt;code&gt;pg_dump&lt;/code&gt;, ELK bulk, and Kubernetes node rotation all sit below 20/s.&lt;/li&gt;
&lt;li&gt;On write-heavy analytics boxes (ClickHouse on a hot compacting node), raise the
canary file path exclusion rather than the threshold — you still want to catch
a real encryptor that is masquerading as a compactor.&lt;/li&gt;
&lt;li&gt;The decision window must be &lt;strong&gt;one second&lt;/strong&gt;. Below that you risk killing a
&lt;code&gt;tar&lt;/code&gt; of many small files; above that you lose the "same-second" response that
matters.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Doing it yourself vs Talus
&lt;/h2&gt;

&lt;p&gt;You can absolutely build Steps 1–3 from scratch; they are ~150 lines of eBPF +&lt;br&gt;
a 200-line userspace scorer. But Steps 3 (guardrails) and 4 (fleet) are where&lt;br&gt;
production systems break, and they are where Talus has shipped fixes. The&lt;br&gt;
engine is MIT; if you prefer to own the policy layer, fork it. If you want to&lt;br&gt;
&lt;strong&gt;run it today&lt;/strong&gt;, the per-machine activation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;talus-process-monitor
talus license activate &amp;lt;KEY&amp;gt;      &lt;span class="c"&gt;# 30-day Enterprise trial, no card&lt;/span&gt;
talus monitor &lt;span class="nt"&gt;--auto-kill&lt;/span&gt;        &lt;span class="c"&gt;# &amp;lt;1s verdict to SIGKILL, canary-aware&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://hartwell-labs.pl/talus-process-monitor/?utm_source=blog&amp;amp;utm_medium=cta&amp;amp;utm_campaign=detect-ransomware-linux-ebpf" rel="noopener noreferrer"&gt;
   class="buy-button"&amp;gt;Monitor with Talus →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;{"&lt;a class="mentioned-user" href="https://dev.to/context"&gt;@context&lt;/a&gt;":"&lt;a href="https://schema.org%22,%22@type%22:%22Article" rel="noopener noreferrer"&gt;https://schema.org","@type":"Article&lt;/a&gt;",&lt;br&gt;
"headline":"How to Detect Ransomware Activity on Linux with eBPF",&lt;br&gt;
"description":"Guide to detecting ransomware using eBPF open-rate scoring, canary reads, kill guardrails, and fleet correlation.",&lt;br&gt;
"author":{"@type":"Person","name":"Bartosz Osiej"},&lt;br&gt;
"datePublished":"2026-10-20","dateModified":"2026-10-20",&lt;br&gt;
"mainEntityOfPage":{"@type":"WebPage","&lt;a class="mentioned-user" href="https://dev.to/id"&gt;@id&lt;/a&gt;":"&lt;a href="https://bo.hartwell-labs.pl/content/detect-ransomware-linux-ebpf%22%7D" rel="noopener noreferrer"&gt;https://bo.hartwell-labs.pl/content/detect-ransomware-linux-ebpf"}&lt;/a&gt;}&lt;/p&gt;

</description>
      <category>ebpf</category>
      <category>security</category>
      <category>linux</category>
      <category>ransomware</category>
    </item>
    <item>
      <title>Best eBPF Monitoring Tools in 2026: A Practical Comparison</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:59:27 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/best-ebpf-monitoring-tools-in-2026-a-practical-comparison-4fbd</link>
      <guid>https://dev.to/bartoszosiej/best-ebpf-monitoring-tools-in-2026-a-practical-comparison-4fbd</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;eBPF is no longer academic — by 2026 it is the default substrate for Linux&lt;br&gt;
observability and security. But the market has split in two: &lt;strong&gt;Kuberscale&lt;br&gt;
platforms&lt;/strong&gt; (Falco, Tetragon, Tracee, Cilium Hubble) that assume you run&lt;br&gt;
Kubernetes, and &lt;strong&gt;standalone agents&lt;/strong&gt; (Talus) that assume you just have Linux&lt;br&gt;
servers and want results before lunch.&lt;/p&gt;

&lt;p&gt;This article compares the five most used tools on &lt;strong&gt;standalone process&lt;br&gt;
monitoring&lt;/strong&gt; and &lt;strong&gt;fleet behavior&lt;/strong&gt; — the exact criteria that matter when you&lt;br&gt;
are not running a CNI and don't want a sidecar on every node.&lt;/p&gt;
&lt;h2&gt;
  
  
  What "ebpf monitoring tools" means today
&lt;/h2&gt;

&lt;p&gt;In 2026 the category is overloaded, so let's scope it precisely. I am comparing&lt;br&gt;
tools that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Attach eBPF programs to &lt;strong&gt;kernel events&lt;/strong&gt; (not polling &lt;code&gt;/proc&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Aggregate events &lt;strong&gt;per process&lt;/strong&gt; over a window.&lt;/li&gt;
&lt;li&gt;Can emit &lt;strong&gt;alerts&lt;/strong&gt; and ideally &lt;strong&gt;act&lt;/strong&gt; (kill the process).&lt;/li&gt;
&lt;li&gt;Scale beyond one host with a &lt;strong&gt;fleet view&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I am &lt;strong&gt;not&lt;/strong&gt; comparing full EDR/XDR suites, distros, or SaaS SIEMs that happen&lt;br&gt;
to ingest eBPF agents. Those are a different purchase committee.&lt;/p&gt;
&lt;h2&gt;
  
  
  The landscape
&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;Runtime model&lt;/th&gt;
&lt;th&gt;K8s-coupled?&lt;/th&gt;
&lt;th&gt;Kill?&lt;/th&gt;
&lt;th&gt;Fleet?&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Talus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 static binary, no cluster&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ SIGKILL &amp;lt;1s&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;$50/node/yr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Falco&lt;/td&gt;
&lt;td&gt;Daemonset / standalone agent&lt;/td&gt;
&lt;td&gt;⚠️ prefers k8s&lt;/td&gt;
&lt;td&gt;❌ alerts only&lt;/td&gt;
&lt;td&gt;⚠️ via SIEM&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tetragon&lt;/td&gt;
&lt;td&gt;Cilium + Hubble agent&lt;/td&gt;
&lt;td&gt;✅ (Cilium)&lt;/td&gt;
&lt;td&gt;✅ via policy&lt;/td&gt;
&lt;td&gt;✅ via Hubble&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bpftrace&lt;/td&gt;
&lt;td&gt;CLI one-off traces&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tracee&lt;/td&gt;
&lt;td&gt;Single-agent + k8s helm&lt;/td&gt;
&lt;td&gt;⚠️ helm-heavy&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;⚠️ via integrations&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Numbers from each project’s public docs + GitHub as of 2026-09.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Falco — the CNCF default, not your laptop
&lt;/h2&gt;

&lt;p&gt;Falco is brilliant if your entire world is Kubernetes. Inside a cluster its&lt;br&gt;
rules engine (YARA-ish &lt;code&gt;condition:&lt;/code&gt; language) is mature and well-supported.&lt;br&gt;
But spin it up on five standalone servers and you immediately hit friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It expects either a Kubernetes daemonset or a host-level install that still
configures itself like a k8s workload.&lt;/li&gt;
&lt;li&gt;Rules ship as a big YAML ruleset you must trim; the signal-to-noise ratio on a
non-K8s host is poor for weeks.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;detects&lt;/strong&gt; only. Response is "alert someone else" — no kill.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Talus treats those five servers like what they are: Linux. One binary, one&lt;br&gt;
&lt;code&gt;--auto-kill&lt;/code&gt; flag, verdict to &lt;code&gt;SIGKILL&lt;/code&gt; in the same one-second window that&lt;br&gt;
detects.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tetragon — Cilium's gift to Kubernetes
&lt;/h2&gt;

&lt;p&gt;Tetragon belongs to Cilium. It is the most capable eBPF security product on the&lt;br&gt;
market &lt;strong&gt;inside Kubernetes&lt;/strong&gt;, and its process lineage graph is gorgeous. But the&lt;br&gt;
same sentence is "inside Kubernetes": it depends on the Cilium CNI, Hubble&lt;br&gt;
relay, and a Kubernetes operator. If you are reading this and you do not deploy&lt;br&gt;
Cilium, Tetragon is a large amount of correct software you don't need.&lt;/p&gt;

&lt;p&gt;Talus deliberately couples to nothing: no CNI, no CRI, no kubelet. Kernel 5.8+&lt;br&gt;
and root is enough.&lt;/p&gt;
&lt;h2&gt;
  
  
  bpftrace — debugging, not guarding
&lt;/h2&gt;

&lt;p&gt;bpftrace is the &lt;code&gt;strace&lt;/code&gt; of eBPF and that is its superpower. It is unbeatable for&lt;br&gt;
one-off forensic looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight bpf"&gt;&lt;code&gt;bpftool prog | bpftrace -e '&lt;span class="nl"&gt;tracepoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;sysca&lt;/span&gt;&lt;span class="kt"&gt;ll&lt;/span&gt;&lt;span class="nl"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;sys_enter_openat { printf("%s %s\n", comm, str(args-&amp;gt;filename)); }'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But a monitoring &lt;strong&gt;product&lt;/strong&gt; is a different shape. bpftrace is a CLI; it has no&lt;br&gt;
daemon, no state across boots, no fleet, no alerting channel you can point at a&lt;br&gt;
Slack webhook from a config file, and no concept of "kill the process that did&lt;br&gt;
this."&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracee — single agent, but still a project
&lt;/h2&gt;

&lt;p&gt;Tracee (Aqua) can run as a single agent and has good forensic export. Its&lt;br&gt;
default install path, though, is Helm, and tuning it without k8s is a doc-dive.&lt;br&gt;
For an operator on three bare-metal hosts, Tracee is a reasonable second choice&lt;br&gt;
only if you are already drinking the Aqua ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Talus wins
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standalone hosts.&lt;/strong&gt; eBPF tracepoints in a single static binary; no kernel
modules, no reboot, no cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detect + respond in the same second.&lt;/strong&gt; Per-process open-rate heuristic in a
1-second window, &lt;code&gt;SIGKILL&lt;/code&gt; immediately, guardrails + whitelists so it cannot
kill your own backup job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy.&lt;/strong&gt; Runs on your hosts. No cloud, no telemetry, no third-party.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fleet, not a sidecar.&lt;/strong&gt; One dashboard for the whole fleet, keyed to a
per-machine license.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MIT core.&lt;/strong&gt; The detection engine stays MIT and public; the paid tier is the
license keys and response guards.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest trade-off
&lt;/h2&gt;

&lt;p&gt;You give up Kubernetes-native integration. Talus is not going to sit in your&lt;br&gt;
Cilium CNI graph or emit to Hubble. If you are 100% Kubernetes with Cilium&lt;br&gt;
today, Tetragon is better fit. If you run &lt;strong&gt;any&lt;/strong&gt; server that is not in that&lt;br&gt;
cluster — edge, bare metal, a CI box, a customer's host — Talus is what closes&lt;br&gt;
the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick decision
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;All workloads in k8s + Cilium → &lt;strong&gt;Tetragon&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Mostly k8s, but some stray hosts → &lt;strong&gt;both&lt;/strong&gt;: Tetragon in-cluster, Talus on the
stray boxes, one dashboard.&lt;/li&gt;
&lt;li&gt;Real Linux servers, edge, air gap, or just "I want this on before lunch" →
&lt;strong&gt;Talus&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://hartwell-labs.pl/talus-process-monitor/pricing.html?utm_source=blog&amp;amp;utm_medium=cta&amp;amp;utm_campaign=best-ebpf-monitoring-tools-2026" rel="noopener noreferrer"&gt;
   class="buy-button"&amp;gt;Compare pricing →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "&lt;a class="mentioned-user" href="https://dev.to/context"&gt;@context&lt;/a&gt;":"&lt;a href="https://schema.org" rel="noopener noreferrer"&gt;https://schema.org&lt;/a&gt;",&lt;br&gt;
  "@type":"Article",&lt;br&gt;
  "headline":"Best eBPF Monitoring Tools in 2026: A Practical Comparison",&lt;br&gt;
  "description":"Comparing Talus, Falco, Tetragon, bpftrace, Tracee for standalone Linux process monitoring.",&lt;br&gt;
  "author":{"@type":"Person","name":"Bartosz Osiej"},&lt;br&gt;
  "datePublished":"2026-10-20",&lt;br&gt;
  "dateModified":"2026-10-20",&lt;br&gt;
  "mainEntityOfPage":{"@type":"WebPage","&lt;a class="mentioned-user" href="https://dev.to/id"&gt;@id&lt;/a&gt;":"&lt;a href="https://bo.hartwell-labs.pl/content/best-ebpf-monitoring-tools-2026%22" rel="noopener noreferrer"&gt;https://bo.hartwell-labs.pl/content/best-ebpf-monitoring-tools-2026"&lt;/a&gt;}&lt;br&gt;
}&lt;/p&gt;

</description>
      <category>ebpf</category>
      <category>monitoring</category>
      <category>security</category>
      <category>rust</category>
    </item>
    <item>
      <title>Linux Process Monitoring: From /proc to eBPF</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:54:36 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/linux-process-monitoring-from-proc-to-ebpf-20ag</link>
      <guid>https://dev.to/bartoszosiej/linux-process-monitoring-from-proc-to-ebpf-20ag</guid>
      <description>&lt;h2&gt;
  
  
  Three generations
&lt;/h2&gt;

&lt;p&gt;Linux process monitoring moved through three generations, and most orgs are&lt;br&gt;
still on generation two while reading alerts from generation three.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Generation&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Strengths&lt;/th&gt;
&lt;th&gt;Weaknesses&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. &lt;code&gt;/proc&lt;/code&gt; polling&lt;/td&gt;
&lt;td&gt;read &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/stat&lt;/code&gt; every N seconds&lt;/td&gt;
&lt;td&gt;trivially portable, no deps&lt;/td&gt;
&lt;td&gt;lossy, high overhead at scale, no events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. &lt;code&gt;ptrace&lt;/code&gt; + &lt;code&gt;exec&lt;/code&gt; hooks&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;strace&lt;/code&gt;-style attach&lt;/td&gt;
&lt;td&gt;precise, per-syscall&lt;/td&gt;
&lt;td&gt;stops the world on every syscall, no fleet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. eBPF tracepoints&lt;/td&gt;
&lt;td&gt;kernel callback, no attach cost&lt;/td&gt;
&lt;td&gt;lossless, kernel-attributed, fleet-capable&lt;/td&gt;
&lt;td&gt;requires BTF, kernel 5.8+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Generation 1: &lt;code&gt;/proc&lt;/code&gt; polling
&lt;/h2&gt;

&lt;p&gt;Everyone starts here. A cron or daemon loops &lt;code&gt;for pid in /proc/[0-9]*&lt;/code&gt; and reads&lt;br&gt;
&lt;code&gt;stat&lt;/code&gt; to snapshot &lt;code&gt;utime&lt;/code&gt;/&lt;code&gt;stime&lt;/code&gt;/&lt;code&gt;state&lt;/code&gt;, then diffs between polls to derive&lt;br&gt;
CPU% and detect a process that opened many files.&lt;/p&gt;

&lt;p&gt;Problems that bite by year two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polling is lossy.&lt;/strong&gt; A process that opens 5000 files and exits in 50 ms
between 1s polls is invisible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overhead scales as O(pids × fields × freqs)&lt;/strong&gt;. On a 16k-PID CI box polled
at 10 Hz you are reading ~160k files/sec of kernel state — you spend more
time reading &lt;code&gt;/proc&lt;/code&gt; than the workload does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No parent/child lineage.&lt;/strong&gt; Reconstructing the tree means storing state per
sample and stitching PPID across polls — racy and expensive.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Generation 2: ptrace/exec
&lt;/h2&gt;

&lt;p&gt;Tools like &lt;code&gt;strace&lt;/code&gt;, &lt;code&gt;sysdig&lt;/code&gt;, and early eBPF wrappers use &lt;code&gt;ptrace(PTRACE_ATTACH)&lt;/code&gt;&lt;br&gt;
or seccomp-notify to get per-event visibility. Precise, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ptrace&lt;/code&gt; &lt;strong&gt;stops the tracee&lt;/strong&gt; on every syscall — throughput collapses.&lt;/li&gt;
&lt;li&gt;You attach to what you &lt;em&gt;know&lt;/em&gt; to attach to. Unknown binaries launched by an
attacker are missed unless you already hook &lt;code&gt;exec&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;State is local to one machine; nothing here speaks fleet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;sysdig (pre-3.x) is this generation's poster child: brilliant forensics, poor&lt;br&gt;
for always-on production load.&lt;/p&gt;
&lt;h2&gt;
  
  
  Generation 3: eBPF tracepoints
&lt;/h2&gt;

&lt;p&gt;eBPF tracepoints attach in kernel space and never stop the traced task. You get&lt;br&gt;
an event per syscall entry/exit with a &lt;strong&gt;kernel-attributed pid&lt;/strong&gt;, a timestamp,&lt;br&gt;
and a ring-buffer export to userspace. The program is verified at load time —&lt;br&gt;
if it can crash the kernel, it won't load.&lt;/p&gt;

&lt;p&gt;The modern pattern is &lt;strong&gt;not&lt;/strong&gt; raw &lt;code&gt;bpftrace&lt;/code&gt; one-liners (generation 2.5) but a&lt;br&gt;
&lt;strong&gt;per-process scorer&lt;/strong&gt;: maintain a map keyed by &lt;code&gt;pid&lt;/code&gt; (or better, by&lt;br&gt;
&lt;code&gt;pid + start_time&lt;/code&gt; to defeat PID recycling), increment a counter on&lt;br&gt;
&lt;code&gt;openat&lt;/code&gt;-for-write, and emit a verdict when the windowed rate exceeds a&lt;br&gt;
threshold. That is the core of Talus's detector, and it is ~120 lines of eBPF&lt;br&gt;
plus a ~200-line ring-buffer drain in Rust.&lt;/p&gt;

&lt;p&gt;Key correctness notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Window size matters.&lt;/strong&gt; 1-second windows give same-second verdicts; 10s
windows give you alerts after encryption. Talus ships 1s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path vs name.&lt;/strong&gt; Matching on the binary name is spoofable; matching on the
binary &lt;strong&gt;path + inode&lt;/strong&gt; and the open &lt;strong&gt;path&lt;/strong&gt; is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PID recycling&lt;/strong&gt; is real under churn. Key maps by &lt;code&gt;pid + start_time&lt;/code&gt;; that is
what Talus does, and what naive &lt;code&gt;/proc&lt;/code&gt; scrapers forget.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Fleet: the missing third act
&lt;/h2&gt;

&lt;p&gt;Single-host eBPF monitoring is a solved problem (run the binary). Fleet is&lt;br&gt;
where generation 3 beats 1+2 decisively: a single agent per host streams events&lt;br&gt;
to a central sink (Kafka / ClickHouse / MemGraph), and the verdict engine&lt;br&gt;
correlates across hostnames by &lt;code&gt;machine-id&lt;/code&gt;. That is how you spot a lateral&lt;br&gt;
spray that opens files on 3 hosts within 200 ms.&lt;/p&gt;

&lt;p&gt;Talus ships that sink and an Enterprise fleet dashboard; the agent stays a&lt;br&gt;
single static binary per host. Per-machine license:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;talus-process-monitor
talus license activate &amp;lt;KEY&amp;gt;
talus monitor &lt;span class="nt"&gt;--auto-kill&lt;/span&gt;        &lt;span class="c"&gt;# detects + SIGKILL + streams to fleet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://hartwell-labs.pl/talus-process-monitor/pricing.html?utm_source=blog&amp;amp;utm_medium=cta&amp;amp;utm_campaign=linux-process-monitoring" rel="noopener noreferrer"&gt;
   class="buy-button"&amp;gt;View fleet pricing →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;{"&lt;a class="mentioned-user" href="https://dev.to/context"&gt;@context&lt;/a&gt;":"&lt;a href="https://schema.org%22,%22@type%22:%22Article" rel="noopener noreferrer"&gt;https://schema.org","@type":"Article&lt;/a&gt;",&lt;br&gt;
"headline":"Linux Process Monitoring: From /proc to eBPF",&lt;br&gt;
"description":"Three generations of Linux process monitoring and why eBPF tracepoints win for per-process, fleet-scale detection.",&lt;br&gt;
"author":{"@type":"Person","name":"Bartosz Osiej"},&lt;br&gt;
"datePublished":"2026-10-20","dateModified":"2026-10-20",&lt;br&gt;
"mainEntityOfPage":{"@type":"WebPage","&lt;a class="mentioned-user" href="https://dev.to/id"&gt;@id&lt;/a&gt;":"&lt;a href="https://bo.hartwell-labs.pl/content/linux-process-monitoring-proc-to-ebpf%22%7D" rel="noopener noreferrer"&gt;https://bo.hartwell-labs.pl/content/linux-process-monitoring-proc-to-ebpf"}&lt;/a&gt;}&lt;/p&gt;

</description>
      <category>linux</category>
      <category>ebpf</category>
      <category>monitoring</category>
      <category>processmonitor</category>
    </item>
    <item>
      <title>Fleet Monitoring for Linux: Managing 100+ Servers Without Kubernetes</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:54:05 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/fleet-monitoring-for-linux-managing-100-servers-without-kubernetes-p05</link>
      <guid>https://dev.to/bartoszosiej/fleet-monitoring-for-linux-managing-100-servers-without-kubernetes-p05</guid>
      <description>&lt;h2&gt;
  
  
  The misconception
&lt;/h2&gt;

&lt;p&gt;"Fleet monitoring = Kubernetes monitoring." It is not. Kubernetes is a&lt;br&gt;
scheduling/orchestration control plane that some fleets happen to use. Most&lt;br&gt;
Linux fleets — edge, bare metal, on-prem, air gaps, mixed distros — are just&lt;br&gt;
hosts that need watching. You should not have to stand up a cluster to watch&lt;br&gt;
them.&lt;/p&gt;

&lt;p&gt;The right mental model: &lt;strong&gt;an agent per host&lt;/strong&gt; (one static binary), &lt;strong&gt;a shared&lt;br&gt;
event sink&lt;/strong&gt;, and &lt;strong&gt;one dashboard keyed to a per-host identity&lt;/strong&gt; that survives&lt;br&gt;
re-imaging. Everything else is implementation detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why agents that are "just a CLI" fail at fleet scale
&lt;/h2&gt;

&lt;p&gt;Tools that work on one host and fall apart at 100 nodes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They write state to &lt;code&gt;/tmp&lt;/code&gt; keyed by hostname → PID/host confusion when hosts
are rebuilt.&lt;/li&gt;
&lt;li&gt;They assume a single machine-id, so a fleet dashboard can't tell host A from
host B after a re-image.&lt;/li&gt;
&lt;li&gt;They have no fleet identity, so correlation is "grep the logs by hostname"
(slow, lossy) instead of "join on machine-id."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Talus's fleet model is deliberately the anti-silo: every event carries the&lt;br&gt;
host's &lt;code&gt;machine-id&lt;/code&gt; (and a per-machine license seat), and the agents all speak&lt;br&gt;
the same wire format to a shared sink.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fleet stack (one binary, no cluster)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;host A        host B        host N
  |             |             |
talus          talus         talus
(1 static      (same          (same
 binary)       binary)        binary)
  |             |             |
  +------+------+------+-----+/
         |               |
    Kafka / ClickHouse   MemGraph (process tree)
         |
   dashboard + SIEM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent&lt;/strong&gt;: one ELF (x86_64 + aarch64), eBPF tracepoints, signed audit log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sink&lt;/strong&gt;: Kafka topics, ClickHouse, or MemGraph — pick one, all ships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Licensing&lt;/strong&gt;: per-machine annual keys, &lt;code&gt;machine-id&lt;/code&gt;-pinned, server-side seat
enforcement. Fleet tier is &lt;code&gt;$30/node/year&lt;/code&gt; from 3 nodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deploy that survives chaos
&lt;/h2&gt;

&lt;p&gt;At 100+ hosts the deployment story is what bites first. Talus's pattern that&lt;br&gt;
works on mixed fleets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One package.&lt;/strong&gt; &lt;code&gt;pip install talus-process-monitor&lt;/code&gt; on every distro, or the
static binary from GitHub Releases — no package-manager wars.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root once, run without it.&lt;/strong&gt; eBPF load needs root/CAP_BPF at attach; after
that the drain and scorer drop privileges via seccomp + Landlock. That is why
Talus self-sandboxes — it matters more on a fleet than on one box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity via &lt;code&gt;machine-id&lt;/code&gt;.&lt;/strong&gt; Each agent registers with
&lt;code&gt;/etc/machine-id&lt;/code&gt; + a per-node license. Re-imaging a node? &lt;code&gt;talus license
deactivate&lt;/code&gt; on the old, &lt;code&gt;activate&lt;/code&gt; on the new — seats stay honest, no
double-billing.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# fleet deploy example (any CM, here shaped as Ansible)&lt;/span&gt;
- hosts: all
  tasks:
    - name: &lt;span class="nb"&gt;install &lt;/span&gt;talus
      ansible.builtin.command: talus &lt;span class="nb"&gt;install&lt;/span&gt;
    - name: activate license
      ansible.builtin.command: talus license activate &lt;span class="o"&gt;{{&lt;/span&gt; talus_license_key &lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fleet threat detection you can actually read
&lt;/h2&gt;

&lt;p&gt;The killer use-case is not "one host" but &lt;strong&gt;cross-host timing&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;10:03:17 hostA opens 41k files — kill.&lt;/li&gt;
&lt;li&gt;10:03:18 hostB opens 18k files — kill (lateral from A in &amp;lt;1 s).&lt;/li&gt;
&lt;li&gt;10:03:19 hostC — blocked before it started, because the sink saw B's canary
and pre-empted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That timing is invisible to per-host-only tools and invisible to SIEMs fed by&lt;br&gt;
polled logs. Kernel-attached agents feeding a shared sink are the only way to&lt;br&gt;
see sub-second lateral moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing seats and cost
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fleet license: &lt;code&gt;$30 / node / year&lt;/code&gt;, min 3 nodes, max 500, cancel any time.&lt;/li&gt;
&lt;li&gt;A node that goes dark stops billing only when you &lt;code&gt;deactivate&lt;/code&gt; it — there is
no "phantom usage" trap.&lt;/li&gt;
&lt;li&gt;Batch prepaid keys exist if procurement needs PO + net-30;
&lt;code&gt;contact@hartwell-labs.pl&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://hartwell-labs.pl/talus-process-monitor/pricing.html?utm_source=blog&amp;amp;utm_medium=cta&amp;amp;utm_campaign=fleet-monitoring-linux" rel="noopener noreferrer"&gt;
   class="buy-button"&amp;gt;Fleet pricing ($30/node) →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;{"&lt;a class="mentioned-user" href="https://dev.to/context"&gt;@context&lt;/a&gt;":"&lt;a href="https://schema.org%22,%22@type%22:%22Article" rel="noopener noreferrer"&gt;https://schema.org","@type":"Article&lt;/a&gt;",&lt;br&gt;
"headline":"Fleet Monitoring for Linux: Managing 100+ Servers Without Kubernetes",&lt;br&gt;
"description":"eBPF agent-per-host fleet monitoring without Kubernetes: per-machine licensing, shared Kafka sink, cross-host correlation.",&lt;br&gt;
"author":{"@type":"Person","name":"Bartosz Osiej"},&lt;br&gt;
"datePublished":"2026-10-20","dateModified":"2026-10-20",&lt;br&gt;
"mainEntityOfPage":{"@type":"WebPage","&lt;a class="mentioned-user" href="https://dev.to/id"&gt;@id&lt;/a&gt;":"&lt;a href="https://bo.hartwell-labs.pl/content/fleet-monitoring-linux%22%7D" rel="noopener noreferrer"&gt;https://bo.hartwell-labs.pl/content/fleet-monitoring-linux"}&lt;/a&gt;}&lt;/p&gt;

</description>
      <category>linux</category>
      <category>ebpf</category>
      <category>fleet</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>eBPF vs Traditional Monitoring: Why Kernel-Level Wins</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:54:04 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/ebpf-vs-traditional-monitoring-why-kernel-level-wins-41j8</link>
      <guid>https://dev.to/bartoszosiej/ebpf-vs-traditional-monitoring-why-kernel-level-wins-41j8</guid>
      <description>&lt;h2&gt;
  
  
  The latency tax of polling
&lt;/h2&gt;

&lt;p&gt;Traditional Linux monitoring is built on two lies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"Sampling every second is good enough."&lt;/li&gt;
&lt;li&gt;"Logs arrive in order and are never dropped."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both break under load. A ransomware process that opens 4000 files in 120 ms&lt;br&gt;
between two 1s polls is invisible to generation-1 &lt;code&gt;/proc&lt;/code&gt; scrapers. A&lt;br&gt;
&lt;code&gt;journald&lt;/code&gt;/syslog pipeline that buffers under burst &lt;strong&gt;reorders&lt;/strong&gt; events, so the&lt;br&gt;
alert that says "encryptor X ran" arrives after X has already finished.&lt;/p&gt;

&lt;p&gt;eBPF does not have this problem. An eBPF tracepoint fires &lt;strong&gt;in kernel space at&lt;br&gt;
the syscall boundary&lt;/strong&gt; — the event is read by userspace from a lockless ring&lt;br&gt;
buffer, with a timestamp taken before the syscall returns to userspace. There is&lt;br&gt;
no polling interval to miss, no log line to drop, no context switch to stall.&lt;/p&gt;

&lt;h2&gt;
  
  
  The overhead inversion
&lt;/h2&gt;

&lt;p&gt;The counterintuitive result, which I verified while writing&lt;br&gt;
&lt;a href="https://bo.hartwell-labs.pl/content/ebpf-ransomware-monitor-lessons" rel="noopener noreferrer"&gt;ebpf-ransomware-monitor-lessons&lt;/a&gt;,&lt;br&gt;
is that eBPF monitoring has &lt;strong&gt;lower overhead&lt;/strong&gt; than doing nothing under pressure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ptrace(PTRACE_ATTACH)&lt;/code&gt; stops the tracee on every syscall — throughput
collapses the instant you attach broadly. eBPF does &lt;strong&gt;not&lt;/strong&gt; stop the task; it
runs a verified callback and returns to userspace.&lt;/li&gt;
&lt;li&gt;Parsing &lt;code&gt;/proc&lt;/code&gt; is O(pids) and grows with process churn; eBPF is O(events)
and the kernel already had those events.&lt;/li&gt;
&lt;li&gt;At &amp;gt;100k open/s the &lt;code&gt;/proc&lt;/code&gt; scraper starts competing with the workload for
inode and dentry locks; the eBPF program does not touch those locks at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Accuracy: attribution that can't be faked
&lt;/h2&gt;

&lt;p&gt;Traditional tools attribute work to a &lt;strong&gt;PID&lt;/strong&gt; (a number that recycles) or to&lt;br&gt;
&lt;strong&gt;stdout/stderr text&lt;/strong&gt; (which an attacker can write to selectively). eBPF&lt;br&gt;
tracepoints come with the kernel's own view: pid, tgid, uid, gid, absolute&lt;br&gt;
inode, and &lt;code&gt;task-&amp;gt;comm&lt;/code&gt; — all kernel-attributed, none spoofable from userspace.&lt;/p&gt;

&lt;p&gt;For ransomware this is the difference between "alert: something wrote files" and&lt;br&gt;
"alert: pid 3412 (&lt;code&gt;systemd-resolve&lt;/code&gt;... no, a renamed &lt;code&gt;python&lt;/code&gt; with inode&lt;br&gt;
&lt;code&gt;/var/lib/.x/enc&lt;/code&gt;) opened 3412 files for write in 0.8 s and is renaming each&lt;br&gt;
&lt;code&gt;.doc&lt;/code&gt; → &lt;code&gt;.doc.ez&lt;/code&gt;." The second one kills processes safely; the first one pages&lt;br&gt;
an on-call engineer at 3 a.m. for a backup job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Response without round-trips
&lt;/h2&gt;

&lt;p&gt;Traditional monitors detect, page, and wait for a human. eBPF programs can&lt;br&gt;
&lt;strong&gt;act in the same kernel boundary&lt;/strong&gt;: queue a &lt;code&gt;SIGKILL&lt;/code&gt;, open a Landlock&lt;br&gt;
restrict, or drop network — before the syscall returns to userspace. That is&lt;br&gt;
not theoretical: Talus's &lt;code&gt;--auto-kill&lt;/code&gt; fires &lt;code&gt;SIGKILL&lt;/code&gt; in under 1 s of the first&lt;br&gt;
mass-open, on the same box, no network hop.&lt;/p&gt;

&lt;h2&gt;
  
  
  When traditional still wins
&lt;/h2&gt;

&lt;p&gt;eBPF is not magic duct tape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Portability to old kernels.&lt;/strong&gt; eBPF CO-RE needs 5.8+ / BTF. Ships in 2020,
but some long-term enterprise distros still lag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-off debugging.&lt;/strong&gt; &lt;code&gt;strace&lt;/code&gt; remains the fastest way to answer "what did my
one test do just now." eBPF tools like bpftrace own this too, but the mental
model is heavier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance retention.&lt;/strong&gt; Some auditors still ask for logs you can grep in
vim at 2 a.m. eBPF pipelines still ship to a log sink; it is complementary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The kernel-level conclusion
&lt;/h2&gt;

&lt;p&gt;Monitoring that lives in userspace and polls is, by construction, watching the&lt;br&gt;
past. Monitoring that lives in the kernel with eBPF is watching the present —&lt;br&gt;
and can act before it becomes history. That is why Talus ships its detector as&lt;br&gt;
an eBPF program and its response guardrails in the same one-second window.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hartwell-labs.pl/talus-process-monitor/?utm_source=blog&amp;amp;utm_medium=cta&amp;amp;utm_campaign=ebpf-vs-traditional-monitoring" rel="noopener noreferrer"&gt;
   class="buy-button"&amp;gt;Run Talus on your servers →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;{"&lt;a class="mentioned-user" href="https://dev.to/context"&gt;@context&lt;/a&gt;":"&lt;a href="https://schema.org%22,%22@type%22:%22Article" rel="noopener noreferrer"&gt;https://schema.org","@type":"Article&lt;/a&gt;",&lt;br&gt;
"headline":"eBPF vs Traditional Monitoring: Why Kernel-Level Wins",&lt;br&gt;
"description":"Kernel-attached eBPF events beat polling and log parsing for accuracy, overhead and same-second response.",&lt;br&gt;
"author":{"@type":"Person","name":"Bartosz Osiej"},&lt;br&gt;
"datePublished":"2026-10-20","dateModified":"2026-10-20",&lt;br&gt;
"mainEntityOfPage":{"@type":"WebPage","&lt;a class="mentioned-user" href="https://dev.to/id"&gt;@id&lt;/a&gt;":"&lt;a href="https://bo.hartwell-labs.pl/content/ebpf-vs-traditional-monitoring%22%7D" rel="noopener noreferrer"&gt;https://bo.hartwell-labs.pl/content/ebpf-vs-traditional-monitoring"}&lt;/a&gt;}&lt;/p&gt;

</description>
      <category>ebpf</category>
      <category>monitoring</category>
      <category>linux</category>
      <category>performance</category>
    </item>
    <item>
      <title>How to Detect Ransomware Activity on Linux with eBPF</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:53:32 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/how-to-detect-ransomware-activity-on-linux-with-ebpf-3j7p</link>
      <guid>https://dev.to/bartoszosiej/how-to-detect-ransomware-activity-on-linux-with-ebpf-3j7p</guid>
      <description>&lt;h2&gt;
  
  
  The shape of a Linux ransomware hit
&lt;/h2&gt;

&lt;p&gt;Ransomware on Linux does not usually announce itself with a GUI. It looks like&lt;br&gt;
a normal process that, in a short window, opens a large number of files for&lt;br&gt;
writing, writes to them, and renames them to a new extension. The signatures&lt;br&gt;
are always new; the &lt;strong&gt;shape&lt;/strong&gt; is stable. That shape is what eBPF is built to&lt;br&gt;
score.&lt;/p&gt;

&lt;p&gt;This is the same material I wrote up in&lt;br&gt;
&lt;a href="https://bo.hartwell-labs.pl/content/ebpf-ransomware-monitor-lessons" rel="noopener noreferrer"&gt;ebpf-ransomware-monitor-lessons&lt;/a&gt;,&lt;br&gt;
but here I focus on the &lt;strong&gt;detection steps&lt;/strong&gt; you can implement yourself, and&lt;br&gt;
where Talus takes over so you do not maintain them.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why eBPF, and not syslog or /proc
&lt;/h2&gt;

&lt;p&gt;Polling &lt;code&gt;/proc/&amp;lt;pid&amp;gt;&lt;/code&gt; or parsing &lt;code&gt;auditd&lt;/code&gt; logs is, by construction, &lt;strong&gt;after the&lt;br&gt;
fact&lt;/strong&gt; and lossy under load. eBPF runs at the kernel boundary: you get every&lt;br&gt;
&lt;code&gt;openat&lt;/code&gt;, &lt;code&gt;rename&lt;/code&gt;, &lt;code&gt;unlink&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt; event as it happens, in kernel space,&lt;br&gt;
with a per-process attribution that never drops events even at ring-buffer&lt;br&gt;
saturation (you get a &lt;code&gt;lost&lt;/code&gt; counter, not silence).&lt;/p&gt;

&lt;p&gt;The two eBPF tracepoints that carry the signal are&lt;br&gt;
&lt;code&gt;tracepoint/syscalls/sys_enter_openat&lt;/code&gt; (and the &lt;code&gt;exit_*&lt;/code&gt; pair) and&lt;br&gt;
&lt;code&gt;tracepoint/syscalls/sys_enter_unlink&lt;/code&gt;/&lt;code&gt;rename&lt;/code&gt;. You do &lt;strong&gt;not&lt;/strong&gt; need full&lt;br&gt;
&lt;code&gt;ptrace&lt;/code&gt; overhead — tracepoints are stable ABI across kernel minor versions when&lt;br&gt;
paired with BTF (Linux 5.8+, every distro since 2020).&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: score open-rate per process
&lt;/h2&gt;

&lt;p&gt;Maintain a per-&lt;code&gt;(pid)&lt;/code&gt; counter, increment on each write-capable &lt;code&gt;openat&lt;/code&gt; with&lt;br&gt;
&lt;code&gt;O_WRONLY|O_CREAT&lt;/code&gt; or &lt;code&gt;O_RDWR&lt;/code&gt;, and roll it over every &lt;strong&gt;1-second window&lt;/strong&gt;.&lt;br&gt;
The decision rule is embarrassingly simple and catches every public encryptor&lt;br&gt;
I have benchmarked it against:&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;/* pseudocode, maps to the kernel-space program in process-monitor-ebpf */&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;pid_count&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;verdict&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ALERT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start with &lt;code&gt;THRESHOLD=50&lt;/code&gt; files/second. On a real host the background load&lt;br&gt;
(normal &lt;code&gt;rsync&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, a compiler) is usually under 10/s; a mass-encryptor is&lt;br&gt;
in the hundreds. Tune down to 20/s only if you have many compilers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A common mistake is using &lt;code&gt;cpu_cycle&lt;/code&gt; counters instead of &lt;strong&gt;file operations&lt;/strong&gt;.&lt;br&gt;
A process can burn CPU and be innocent; rapid &lt;strong&gt;file opens for write&lt;/strong&gt; is the&lt;br&gt;
behavioral invariant of ransomware, not CPU.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Step 2: the canary read
&lt;/h2&gt;

&lt;p&gt;Rate-scoring gives false positives on &lt;code&gt;rsync&lt;/code&gt;, &lt;code&gt;git checkout&lt;/code&gt;, backup jobs.&lt;br&gt;
Add a &lt;strong&gt;canary read&lt;/strong&gt;: a decoy file placed on each host (&lt;code&gt;.canary&lt;/code&gt; in &lt;code&gt;/var/lib&lt;/code&gt;).&lt;br&gt;
Any process that opens the canary and then starts writing to many other files&lt;br&gt;
is behaving like ransomware, not like a backup tool that happens to walk the&lt;br&gt;
tree. The canary read is cheap (one extra tracepoint match) and cuts noise&lt;br&gt;
dramatically.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3: the kill guardrail
&lt;/h2&gt;

&lt;p&gt;When verdict fires, you must terminate the process — but recklessness kills&lt;br&gt;
production. Talus's kill path enforces four guardrails, all of which I would&lt;br&gt;
implement for an in-house agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Whitelist by path and binary name.&lt;/strong&gt; &lt;code&gt;/usr/bin/rsync&lt;/code&gt;, &lt;code&gt;/usr/sbin/anacron&lt;/code&gt;,
the backup tool's real path — never kill these.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canary verification.&lt;/strong&gt; Only kill if the canary was read within the window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parent attribution.&lt;/strong&gt; Kill the encryptor, not the shell that launched it
unless the parent also opened files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limit.&lt;/strong&gt; Max one kill per process per minute to prevent a looping
process from DOS-ing the host.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Step 4: correlate across the fleet
&lt;/h2&gt;

&lt;p&gt;A single host can be patient-zero. The real value of fleet monitoring is&lt;br&gt;
seeing &lt;strong&gt;that three hosts in the same subnet started encrypting within 200 ms of&lt;br&gt;
each other&lt;/strong&gt; and cutting lateral movement before it lands. Talus streams events&lt;br&gt;
to Kafka/ClickHouse and lets you key on &lt;code&gt;hostname&lt;/code&gt;, &lt;code&gt;pid&lt;/code&gt;, and the&lt;br&gt;
&lt;code&gt;machine-id&lt;/code&gt;-derived fleet tag.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tuning notes from real boxes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Threshold &lt;strong&gt;50&lt;/strong&gt; is the sweet spot for &amp;gt;95% of servers. Database
&lt;code&gt;pg_dump&lt;/code&gt;, ELK bulk, and Kubernetes node rotation all sit below 20/s.&lt;/li&gt;
&lt;li&gt;On write-heavy analytics boxes (ClickHouse on a hot compacting node), raise the
canary file path exclusion rather than the threshold — you still want to catch
a real encryptor that is masquerading as a compactor.&lt;/li&gt;
&lt;li&gt;The decision window must be &lt;strong&gt;one second&lt;/strong&gt;. Below that you risk killing a
&lt;code&gt;tar&lt;/code&gt; of many small files; above that you lose the "same-second" response that
matters.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Doing it yourself vs Talus
&lt;/h2&gt;

&lt;p&gt;You can absolutely build Steps 1–3 from scratch; they are ~150 lines of eBPF +&lt;br&gt;
a 200-line userspace scorer. But Steps 3 (guardrails) and 4 (fleet) are where&lt;br&gt;
production systems break, and they are where Talus has shipped fixes. The&lt;br&gt;
engine is MIT; if you prefer to own the policy layer, fork it. If you want to&lt;br&gt;
&lt;strong&gt;run it today&lt;/strong&gt;, the per-machine activation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;talus-process-monitor
talus license activate &amp;lt;KEY&amp;gt;      &lt;span class="c"&gt;# 30-day Enterprise trial, no card&lt;/span&gt;
talus monitor &lt;span class="nt"&gt;--auto-kill&lt;/span&gt;        &lt;span class="c"&gt;# &amp;lt;1s verdict to SIGKILL, canary-aware&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://hartwell-labs.pl/talus-process-monitor/?utm_source=blog&amp;amp;utm_medium=cta&amp;amp;utm_campaign=detect-ransomware-linux-ebpf" rel="noopener noreferrer"&gt;
   class="buy-button"&amp;gt;Monitor with Talus →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;{"&lt;a class="mentioned-user" href="https://dev.to/context"&gt;@context&lt;/a&gt;":"&lt;a href="https://schema.org%22,%22@type%22:%22Article" rel="noopener noreferrer"&gt;https://schema.org","@type":"Article&lt;/a&gt;",&lt;br&gt;
"headline":"How to Detect Ransomware Activity on Linux with eBPF",&lt;br&gt;
"description":"Guide to detecting ransomware using eBPF open-rate scoring, canary reads, kill guardrails, and fleet correlation.",&lt;br&gt;
"author":{"@type":"Person","name":"Bartosz Osiej"},&lt;br&gt;
"datePublished":"2026-10-20","dateModified":"2026-10-20",&lt;br&gt;
"mainEntityOfPage":{"@type":"WebPage","&lt;a class="mentioned-user" href="https://dev.to/id"&gt;@id&lt;/a&gt;":"&lt;a href="https://bo.hartwell-labs.pl/content/detect-ransomware-linux-ebpf%22%7D" rel="noopener noreferrer"&gt;https://bo.hartwell-labs.pl/content/detect-ransomware-linux-ebpf"}&lt;/a&gt;}&lt;/p&gt;

</description>
      <category>ebpf</category>
      <category>security</category>
      <category>linux</category>
      <category>ransomware</category>
    </item>
    <item>
      <title>Best eBPF Monitoring Tools in 2026: A Practical Comparison</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:53:30 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/best-ebpf-monitoring-tools-in-2026-a-practical-comparison-16ll</link>
      <guid>https://dev.to/bartoszosiej/best-ebpf-monitoring-tools-in-2026-a-practical-comparison-16ll</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;eBPF is no longer academic — by 2026 it is the default substrate for Linux&lt;br&gt;
observability and security. But the market has split in two: &lt;strong&gt;Kuberscale&lt;br&gt;
platforms&lt;/strong&gt; (Falco, Tetragon, Tracee, Cilium Hubble) that assume you run&lt;br&gt;
Kubernetes, and &lt;strong&gt;standalone agents&lt;/strong&gt; (Talus) that assume you just have Linux&lt;br&gt;
servers and want results before lunch.&lt;/p&gt;

&lt;p&gt;This article compares the five most used tools on &lt;strong&gt;standalone process&lt;br&gt;
monitoring&lt;/strong&gt; and &lt;strong&gt;fleet behavior&lt;/strong&gt; — the exact criteria that matter when you&lt;br&gt;
are not running a CNI and don't want a sidecar on every node.&lt;/p&gt;
&lt;h2&gt;
  
  
  What "ebpf monitoring tools" means today
&lt;/h2&gt;

&lt;p&gt;In 2026 the category is overloaded, so let's scope it precisely. I am comparing&lt;br&gt;
tools that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Attach eBPF programs to &lt;strong&gt;kernel events&lt;/strong&gt; (not polling &lt;code&gt;/proc&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Aggregate events &lt;strong&gt;per process&lt;/strong&gt; over a window.&lt;/li&gt;
&lt;li&gt;Can emit &lt;strong&gt;alerts&lt;/strong&gt; and ideally &lt;strong&gt;act&lt;/strong&gt; (kill the process).&lt;/li&gt;
&lt;li&gt;Scale beyond one host with a &lt;strong&gt;fleet view&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I am &lt;strong&gt;not&lt;/strong&gt; comparing full EDR/XDR suites, distros, or SaaS SIEMs that happen&lt;br&gt;
to ingest eBPF agents. Those are a different purchase committee.&lt;/p&gt;
&lt;h2&gt;
  
  
  The landscape
&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;Runtime model&lt;/th&gt;
&lt;th&gt;K8s-coupled?&lt;/th&gt;
&lt;th&gt;Kill?&lt;/th&gt;
&lt;th&gt;Fleet?&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Talus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 static binary, no cluster&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ SIGKILL &amp;lt;1s&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;$50/node/yr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Falco&lt;/td&gt;
&lt;td&gt;Daemonset / standalone agent&lt;/td&gt;
&lt;td&gt;⚠️ prefers k8s&lt;/td&gt;
&lt;td&gt;❌ alerts only&lt;/td&gt;
&lt;td&gt;⚠️ via SIEM&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tetragon&lt;/td&gt;
&lt;td&gt;Cilium + Hubble agent&lt;/td&gt;
&lt;td&gt;✅ (Cilium)&lt;/td&gt;
&lt;td&gt;✅ via policy&lt;/td&gt;
&lt;td&gt;✅ via Hubble&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bpftrace&lt;/td&gt;
&lt;td&gt;CLI one-off traces&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tracee&lt;/td&gt;
&lt;td&gt;Single-agent + k8s helm&lt;/td&gt;
&lt;td&gt;⚠️ helm-heavy&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;⚠️ via integrations&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Numbers from each project’s public docs + GitHub as of 2026-09.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Falco — the CNCF default, not your laptop
&lt;/h2&gt;

&lt;p&gt;Falco is brilliant if your entire world is Kubernetes. Inside a cluster its&lt;br&gt;
rules engine (YARA-ish &lt;code&gt;condition:&lt;/code&gt; language) is mature and well-supported.&lt;br&gt;
But spin it up on five standalone servers and you immediately hit friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It expects either a Kubernetes daemonset or a host-level install that still
configures itself like a k8s workload.&lt;/li&gt;
&lt;li&gt;Rules ship as a big YAML ruleset you must trim; the signal-to-noise ratio on a
non-K8s host is poor for weeks.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;detects&lt;/strong&gt; only. Response is "alert someone else" — no kill.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Talus treats those five servers like what they are: Linux. One binary, one&lt;br&gt;
&lt;code&gt;--auto-kill&lt;/code&gt; flag, verdict to &lt;code&gt;SIGKILL&lt;/code&gt; in the same one-second window that&lt;br&gt;
detects.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tetragon — Cilium's gift to Kubernetes
&lt;/h2&gt;

&lt;p&gt;Tetragon belongs to Cilium. It is the most capable eBPF security product on the&lt;br&gt;
market &lt;strong&gt;inside Kubernetes&lt;/strong&gt;, and its process lineage graph is gorgeous. But the&lt;br&gt;
same sentence is "inside Kubernetes": it depends on the Cilium CNI, Hubble&lt;br&gt;
relay, and a Kubernetes operator. If you are reading this and you do not deploy&lt;br&gt;
Cilium, Tetragon is a large amount of correct software you don't need.&lt;/p&gt;

&lt;p&gt;Talus deliberately couples to nothing: no CNI, no CRI, no kubelet. Kernel 5.8+&lt;br&gt;
and root is enough.&lt;/p&gt;
&lt;h2&gt;
  
  
  bpftrace — debugging, not guarding
&lt;/h2&gt;

&lt;p&gt;bpftrace is the &lt;code&gt;strace&lt;/code&gt; of eBPF and that is its superpower. It is unbeatable for&lt;br&gt;
one-off forensic looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight bpf"&gt;&lt;code&gt;bpftool prog | bpftrace -e '&lt;span class="nl"&gt;tracepoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;sysca&lt;/span&gt;&lt;span class="kt"&gt;ll&lt;/span&gt;&lt;span class="nl"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;sys_enter_openat { printf("%s %s\n", comm, str(args-&amp;gt;filename)); }'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But a monitoring &lt;strong&gt;product&lt;/strong&gt; is a different shape. bpftrace is a CLI; it has no&lt;br&gt;
daemon, no state across boots, no fleet, no alerting channel you can point at a&lt;br&gt;
Slack webhook from a config file, and no concept of "kill the process that did&lt;br&gt;
this."&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracee — single agent, but still a project
&lt;/h2&gt;

&lt;p&gt;Tracee (Aqua) can run as a single agent and has good forensic export. Its&lt;br&gt;
default install path, though, is Helm, and tuning it without k8s is a doc-dive.&lt;br&gt;
For an operator on three bare-metal hosts, Tracee is a reasonable second choice&lt;br&gt;
only if you are already drinking the Aqua ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Talus wins
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standalone hosts.&lt;/strong&gt; eBPF tracepoints in a single static binary; no kernel
modules, no reboot, no cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detect + respond in the same second.&lt;/strong&gt; Per-process open-rate heuristic in a
1-second window, &lt;code&gt;SIGKILL&lt;/code&gt; immediately, guardrails + whitelists so it cannot
kill your own backup job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy.&lt;/strong&gt; Runs on your hosts. No cloud, no telemetry, no third-party.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fleet, not a sidecar.&lt;/strong&gt; One dashboard for the whole fleet, keyed to a
per-machine license.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MIT core.&lt;/strong&gt; The detection engine stays MIT and public; the paid tier is the
license keys and response guards.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest trade-off
&lt;/h2&gt;

&lt;p&gt;You give up Kubernetes-native integration. Talus is not going to sit in your&lt;br&gt;
Cilium CNI graph or emit to Hubble. If you are 100% Kubernetes with Cilium&lt;br&gt;
today, Tetragon is better fit. If you run &lt;strong&gt;any&lt;/strong&gt; server that is not in that&lt;br&gt;
cluster — edge, bare metal, a CI box, a customer's host — Talus is what closes&lt;br&gt;
the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick decision
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;All workloads in k8s + Cilium → &lt;strong&gt;Tetragon&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Mostly k8s, but some stray hosts → &lt;strong&gt;both&lt;/strong&gt;: Tetragon in-cluster, Talus on the
stray boxes, one dashboard.&lt;/li&gt;
&lt;li&gt;Real Linux servers, edge, air gap, or just "I want this on before lunch" →
&lt;strong&gt;Talus&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://hartwell-labs.pl/talus-process-monitor/pricing.html?utm_source=blog&amp;amp;utm_medium=cta&amp;amp;utm_campaign=best-ebpf-monitoring-tools-2026" rel="noopener noreferrer"&gt;
   class="buy-button"&amp;gt;Compare pricing →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "&lt;a class="mentioned-user" href="https://dev.to/context"&gt;@context&lt;/a&gt;":"&lt;a href="https://schema.org" rel="noopener noreferrer"&gt;https://schema.org&lt;/a&gt;",&lt;br&gt;
  "@type":"Article",&lt;br&gt;
  "headline":"Best eBPF Monitoring Tools in 2026: A Practical Comparison",&lt;br&gt;
  "description":"Comparing Talus, Falco, Tetragon, bpftrace, Tracee for standalone Linux process monitoring.",&lt;br&gt;
  "author":{"@type":"Person","name":"Bartosz Osiej"},&lt;br&gt;
  "datePublished":"2026-10-20",&lt;br&gt;
  "dateModified":"2026-10-20",&lt;br&gt;
  "mainEntityOfPage":{"@type":"WebPage","&lt;a class="mentioned-user" href="https://dev.to/id"&gt;@id&lt;/a&gt;":"&lt;a href="https://bo.hartwell-labs.pl/content/best-ebpf-monitoring-tools-2026%22" rel="noopener noreferrer"&gt;https://bo.hartwell-labs.pl/content/best-ebpf-monitoring-tools-2026"&lt;/a&gt;}&lt;br&gt;
}&lt;/p&gt;

</description>
      <category>ebpf</category>
      <category>monitoring</category>
      <category>security</category>
      <category>rust</category>
    </item>
    <item>
      <title>I measured dev.to's engagement layer. 92% of posts get zero reactions, and the good comments aren't people.</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Wed, 14 Oct 2026 10:47:22 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/i-measured-devtos-engagement-layer-92-of-posts-get-zero-reactions-and-the-good-comments-arent-59ph</link>
      <guid>https://dev.to/bartoszosiej/i-measured-devtos-engagement-layer-92-of-posts-get-zero-reactions-and-the-good-comments-arent-59ph</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I ran three measurements on dev.to using its public API. The results are not a matter of taste:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;91.7% of posts receive zero reactions. 95.7% receive zero comments.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The curated feed shows a mean of 28.8 reactions. The real site shows &lt;strong&gt;0.21&lt;/strong&gt;. The feed is a selection filter, not a sample.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publishing more makes each post do worse.&lt;/strong&gt; Authors with 1–5 posts in a month: 23.3% of them got a reaction. Authors with 21–25: &lt;strong&gt;7.5%&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The strongest comments are frequently automated. Across a 1,366-comment sweep, the same accounts sprayed the same template across &lt;strong&gt;14–22 unrelated threads&lt;/strong&gt;, and distinct accounts reused identical 4-grams.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The conclusion is not "dev.to is bad". It is narrower and more useful: &lt;strong&gt;comment and reaction counts on this platform are farmable, and a farmable metric carries no information.&lt;/strong&gt; If you are publishing there, optimise for being worth following, not for the counter going up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I checked
&lt;/h2&gt;

&lt;p&gt;I publish there too. My account had 27 posts and a combined reaction count of zero — which is either a signal that the writing is bad or a signal that the signal is broken. Those have very different remedies, so it was worth measuring.&lt;/p&gt;

&lt;p&gt;Everything below comes from the public API. Nothing here is inferred from vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measurement 1: what the site actually looks like
&lt;/h2&gt;

&lt;p&gt;The obvious way to sample dev.to is to read the listings, which show a curated selection. That method is wrong, and it is wrong in a flattering direction.&lt;/p&gt;

&lt;p&gt;Someone sampling 2,500 posts from the feed and filtering to August got a pleasant picture:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;curated feed&lt;/th&gt;
&lt;th&gt;random sample of the site&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;posts&lt;/td&gt;
&lt;td&gt;170&lt;/td&gt;
&lt;td&gt;460&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mean reactions&lt;/td&gt;
&lt;td&gt;28.8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.21&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zero reactions&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;91.7%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;≥10 reactions&lt;/td&gt;
&lt;td&gt;70.6%&lt;/td&gt;
&lt;td&gt;0.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zero comments&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;95.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The omissions are not random. Posts the listing &lt;em&gt;included&lt;/em&gt; averaged 0.40 reactions; posts it &lt;em&gt;skipped&lt;/em&gt; averaged 0.15. Sampling through the feed hands you a rosier platform than the one that exists.&lt;/p&gt;

&lt;p&gt;One useful control: posts 14–20 days old had a 13.6% chance of any reaction; posts 42–48 days old, 15.0%. Nothing meaningful arrives after the first fortnight. If your post is flat at two weeks, waiting longer will not fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measurement 2: volume is actively counterproductive
&lt;/h2&gt;

&lt;p&gt;The relationship between how much you post and how each post does is inverted, and it is steep:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authors publishing &lt;strong&gt;1–5 posts&lt;/strong&gt; in August: &lt;strong&gt;23.3%&lt;/strong&gt; of their posts got a reaction.&lt;/li&gt;
&lt;li&gt;Authors publishing &lt;strong&gt;21–25 posts&lt;/strong&gt; in August: &lt;strong&gt;7.5%&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those high-volume authors produced 18,386 of the 45,400 posts in the corpus. They are not a curiosity — they are the majority of what the platform serves, and they are the least rewarded.&lt;/p&gt;

&lt;p&gt;This is worth stating plainly because the instinct when a post flops is to publish more. On this platform that instinct is wrong in a measurable way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measurement 3: the good comments are synthetic
&lt;/h2&gt;

&lt;p&gt;Here is the part that should change behaviour, because I expected obvious bot slop and did not find any.&lt;/p&gt;

&lt;p&gt;On one thread with 35 reactions and 14 comments, the comments were unusually good — substantive pushback, real technical counter-examples. Too good. Reading fourteen in sequence revealed a skeleton: validate the author's framing, add one measured nuance, mention a product the account is named after, cite a number.&lt;/p&gt;

&lt;p&gt;Structural counts on the eight non-author comments:&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;count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;product/company plug&lt;/td&gt;
&lt;td&gt;4 / 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opens by validating a phrase&lt;/td&gt;
&lt;td&gt;5 / 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;genuinely pushes back&lt;/td&gt;
&lt;td&gt;2 / 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;auto-generated username (random hex)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A detector built for the old "Great post!" style scored these at a mean coziness of &lt;strong&gt;0.25&lt;/strong&gt; — confidently human. It was not broken. It was built to catch cliches, and these comments arrive armoured with exactly the thing that defeats it: real specifics.&lt;/p&gt;

&lt;p&gt;Widening to 38 articles, &lt;strong&gt;1,366 comments, 346 accounts&lt;/strong&gt;, the fingerprint held:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The most prolific commenters appeared on &lt;strong&gt;14–22 distinct articles each&lt;/strong&gt; — the same accounts, including on the original thread.&lt;/li&gt;
&lt;li&gt;Distinct accounts reused identical 4-grams. One phrase appeared across &lt;strong&gt;13 different accounts&lt;/strong&gt;. Humans do not converge on phrasing; templated output does.&lt;/li&gt;
&lt;li&gt;11 accounts left long product plugs; 32 opened with phrase-validation; 4 ran the full skeleton end to end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Separately, an audit of another account's MCP posts identified the single strongest comment on the thread as coming from an account that &lt;strong&gt;publishes two full technical articles one second apart, every day, at the same minute past midnight UTC&lt;/strong&gt;. No human batches on that schedule. The sophisticated comment was the product, and it was better at looking like real feedback than real feedback is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why detection does not rescue you
&lt;/h2&gt;

&lt;p&gt;The instinct is to reach for a classifier. That does not work, and there is peer-reviewed evidence for it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Meng et al. (2025) found people at &lt;strong&gt;50.8%&lt;/strong&gt; accuracy distinguishing fake from real product reviews — a coin flip. Detectors did no better.&lt;/li&gt;
&lt;li&gt;Krishna et al. (NeurIPS 2023) showed light paraphrasing collapses DetectGPT from &lt;strong&gt;70.3% to 4.6%&lt;/strong&gt;, and defeats GPTZero, OpenAI's classifier, and watermarking.&lt;/li&gt;
&lt;li&gt;Liang et al. (Patterns 2023) showed detectors are biased against non-native English writers and trivially bypassable by prompting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Specificity is camouflage. The "real technical detail" that makes a synthetic comment feel human is the same mechanism that blinds the detector.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually survived
&lt;/h2&gt;

&lt;p&gt;One result, and it is the only one I found that held up. In a within-author comparison — each writer against their own baseline, to strip out audience size — every stylistic effect collapsed to roughly zero:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;title pattern&lt;/th&gt;
&lt;th&gt;effect on reactions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;first person ("I", "my")&lt;/td&gt;
&lt;td&gt;−0.08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;listicle / numbered list&lt;/td&gt;
&lt;td&gt;−0.08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;question mark&lt;/td&gt;
&lt;td&gt;−0.08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;colon&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The phrasing was never doing the work. It was a marker of &lt;em&gt;which kind of account&lt;/em&gt; was posting. What survived was the tag choice and whether anyone was already reading you.&lt;/p&gt;

&lt;p&gt;So the strategy inverts: &lt;strong&gt;do not optimise the post, build the reason to read the next one.&lt;/strong&gt; That means a profile with a real website on it, an identity, and a reason to follow. The post is the entry point, not the asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable part
&lt;/h2&gt;

&lt;p&gt;The bots are a demand signal. Nobody funds a content operation against one keyword cluster, twice a day, indefinitely, for nothing. That cluster converts into affiliate revenue, lead generation, or SEO authority someone intends to sell. Automated capital farming a topic is downstream evidence the topic has money in it.&lt;/p&gt;

&lt;p&gt;The inversion is that once the commentary layer is mostly machines, the genuine builders are not a slice of the audience — they are the whole of it, and they are rare. Which is why the number to watch is not reactions. It is whether the people who come back brought something specific with them.&lt;/p&gt;

&lt;p&gt;All measurements above use dev.to's public API and are reproducible by anyone with the same queries. The scripts are public: &lt;a href="https://github.com/P0rt/the_cozy_web" rel="noopener noreferrer"&gt;&lt;code&gt;analyze_devto.py&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/P0rt/the_cozy_web" rel="noopener noreferrer"&gt;&lt;code&gt;sweep_devto.py&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>forensics</category>
      <category>dataanalysis</category>
      <category>python</category>
    </item>
    <item>
      <title>A free AI chat that costs me nothing: BYOK on Cloudflare Workers AI</title>
      <dc:creator>Bartosz Osiej</dc:creator>
      <pubDate>Tue, 13 Oct 2026 16:53:29 +0000</pubDate>
      <link>https://dev.to/bartoszosiej/a-free-ai-chat-that-costs-me-nothing-byok-on-cloudflare-workers-ai-5878</link>
      <guid>https://dev.to/bartoszosiej/a-free-ai-chat-that-costs-me-nothing-byok-on-cloudflare-workers-ai-5878</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;There is no free tier that stays free. The usual answer is to buy credits and hope. There is another: &lt;strong&gt;make the user pay, using their own key.&lt;/strong&gt; Workers AI gives out tokens; you relay the request. You pay $0, you never store a credential you didn't already hand you, and the abuse story collapses into "revoke your own token."&lt;/p&gt;

&lt;p&gt;This is the architecture I used for the chat on &lt;code&gt;ai.hartwell-labs.pl&lt;/code&gt;. It took an afternoon. The auth was twenty lines. The thing that actually broke me twice was something else entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape
&lt;/h2&gt;

&lt;p&gt;A Worker with no AI binding at all. It never touches a model on your behalf — it forwards the caller's own credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleChat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// { provider, apiKey, accountId, model }&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&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;bad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Optional: confirm the token can actually list accounts&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.cloudflare.com/client/v4/accounts?per_page=1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;a&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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;bad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bad_token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errors&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upstream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;BUILDERS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/event-stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&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;That is genuinely the whole backend. Token lives in &lt;code&gt;localStorage&lt;/code&gt;, goes up with each request, comes back down with the stream. The Worker holds nothing between calls.&lt;/p&gt;

&lt;p&gt;Two details that matter more than the auth code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Account discovery removes a step.&lt;/strong&gt; &lt;code&gt;/client/v4/accounts?per_page=50&lt;/code&gt; with the user's token returns their accounts. You auto-select the first, store it, and never show a picker. The user pastes a token and starts typing. One interaction, not three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure messages are the product.&lt;/strong&gt; Cloudflare answers a bad token with &lt;code&gt;6003: Invalid request headers / 6111: Invalid format for Authorization header&lt;/code&gt; — which is a genuinely terrible thing to show a human who has pasted the wrong kind of credential. Map it to something actionable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid format for Authorization header&lt;/span&gt;&lt;span class="dl"&gt;"&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="nf"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bad_token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Token format invalid — use a scoped API Token (not Global API Key). &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Create at: dash.cloudflare.com → My Profile → API Tokens → &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Create Token → Account · Workers AI · Read&lt;/span&gt;&lt;span class="dl"&gt;"&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;Most people who paste a Global API Key into an OAuth-shaped field are not going to read an error code. They will read that sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that cost me an hour
&lt;/h2&gt;

&lt;p&gt;The model emits structured directives — this one edits a rich-text notepad in the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Zapisuję.
[notepad action=new doc="Zakupy"]
- mleko
- chleb
[/notepad]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client strips the tags and applies them. Simple, and it worked fine in testing with a complete response.&lt;/p&gt;

&lt;p&gt;It failed in production, live, in the one place I never tested: &lt;strong&gt;while the bytes were still arriving.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Streaming means you re-render on every chunk. At some instant the accumulated buffer is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Zapisuję.

- mleko
- chleb
[/notepad
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The closing tag is half-formed. Every sensible "strip the complete directive" regex fails to match, because a complete directive is by definition not what's on screen yet. So the note contents — the part the user explicitly asked to be filed away — flash through the chat as a stray bullet list.&lt;/p&gt;

&lt;p&gt;The fix is to give up on parsing completeness and delete everything after the opening tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stripDirectives&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&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="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="sr"&gt;notepad&lt;/span&gt;&lt;span class="se"&gt;\b[^\]]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\][\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;?\[\/&lt;/span&gt;&lt;span class="sr"&gt;notepad&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="sr"&gt;/gi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// complete block&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="sr"&gt;notepad&lt;/span&gt;&lt;span class="se"&gt;\b[^\]]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\][\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*$/gi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;// partial — from the tag to EOF&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="sr"&gt;notepad&lt;/span&gt;&lt;span class="se"&gt;\b[^\]]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="sr"&gt;/gi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\[\/&lt;/span&gt;&lt;span class="sr"&gt;notepad&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="sr"&gt;/gi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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 second rule is the whole lesson. &lt;strong&gt;In a stream, an unterminated construct is not "not yet valid" — it is already visible.&lt;/strong&gt; Treat anything from the opening delimiter onward as untrusted, every frame, and overwrite your buffer rather than patching it. Any renderer that accumulates and incrementally parses will eventually show you a half-built structure to the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;The free tier is 10,000 neurons per day. &lt;code&gt;@cf/meta/llama-3.3-70b-instruct-fp8-fast&lt;/code&gt; against a ~700-word system prompt plus a short exchange costs roughly 1,200–1,500 neurons per turn. I found this the honest way: the suite passed all morning, then returned&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AiError: you have used up your daily free allocation of 10,000 neurons
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;on every single call. Latency sat at 5–10 seconds per turn — fast enough to feel real, slow enough that a user will notice. Both numbers are worth publishing, because "free AI chat" otherwise sounds like a promise instead of a budget.&lt;/p&gt;

&lt;p&gt;BYOK doesn't make the model free. It makes the bill yours — which is the only version of "free" that survives contact with a real user base.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Worker source for the chat lives at &lt;code&gt;ai.hartwell-labs.pl&lt;/code&gt;. Everything else I write is public — &lt;a href="https://github.com/BartoszOsiej" rel="noopener noreferrer"&gt;github.com/BartoszOsiej&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>workersai</category>
      <category>byok</category>
      <category>security</category>
    </item>
  </channel>
</rss>
