<?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: Firat Celik</title>
    <description>The latest articles on DEV Community by Firat Celik (@firfircelik).</description>
    <link>https://dev.to/firfircelik</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%2F531676%2F986bfdc9-12db-4931-b41e-5731a6fde406.jpeg</url>
      <title>DEV Community: Firat Celik</title>
      <link>https://dev.to/firfircelik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/firfircelik"/>
    <language>en</language>
    <item>
      <title>DuckDB 2.0 vs Polars vs Spark on One Machine: 150M Rows Under a 4.5 GiB Cap</title>
      <dc:creator>Firat Celik</dc:creator>
      <pubDate>Thu, 22 Oct 2026 08:24:18 +0000</pubDate>
      <link>https://dev.to/firfircelik/duckdb-20-vs-polars-vs-spark-on-one-machine-150m-rows-under-a-45-gib-cap-h38</link>
      <guid>https://dev.to/firfircelik/duckdb-20-vs-polars-vs-spark-on-one-machine-150m-rows-under-a-45-gib-cap-h38</guid>
      <description>&lt;p&gt;DuckDB 2.0.0 shipped on October 21, 2026 (&lt;a href="https://duckdb.org/2026/10/21/announcing-duckdb-200" rel="noopener noreferrer"&gt;announcement&lt;/a&gt;, &lt;a href="https://pypi.org/project/duckdb/2.0.0/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;). The next morning I ran the same single-machine benchmark I had already run on DuckDB 1.5.6: three engines, one 8 vCPU VM, Parquet in and Parquet out, a hard memory cap, and four queries that look like real data engineering work.&lt;/p&gt;

&lt;p&gt;The thesis I started with, and that the numbers support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DuckDB beats Spark-on-one-box on memory-bound DE work.&lt;/strong&gt; It finished every run, at every size, 5 to 12 times faster than local PySpark at 150M rows, and it still finished the heavy queries with a 1 GB memory limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polars hits a wall on window dedup.&lt;/strong&gt; At 150M rows the &lt;code&gt;ROW_NUMBER()&lt;/code&gt; style "latest row per customer" query never finished under the cap. Not at 4.5 GiB, not with an out-of-core budget, not at 9 GiB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polars is not slow.&lt;/strong&gt; On filters, joins and a high-cardinality group by it ties DuckDB or beats it, when it fits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything below is measured on that one box. Where I interpret or give an opinion, I say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contenders
&lt;/h2&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;DuckDB 2.0.0&lt;/th&gt;
&lt;th&gt;Polars 2.0.0&lt;/th&gt;
&lt;th&gt;PySpark 4.2.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;In-process SQL engine&lt;/td&gt;
&lt;td&gt;In-process DataFrame engine, lazy + streaming&lt;/td&gt;
&lt;td&gt;JVM engine, &lt;code&gt;local[8]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How I drove it&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COPY (SQL) TO 'out.parquet'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LazyFrame.sink_parquet()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;spark.sql(...).write.parquet()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory knob&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SET memory_limit = '3GB'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;default (plus one run with an out-of-core budget)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;spark.driver.memory = 3g&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threads&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;8 (&lt;code&gt;POLARS_MAX_THREADS&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Polars 2.0.0 was published on PyPI on &lt;a href="https://pypi.org/project/polars/2.0.0/" rel="noopener noreferrer"&gt;October 6, 2026&lt;/a&gt;, PySpark 4.2.0 on &lt;a href="https://pypi.org/project/pyspark/4.2.0/" rel="noopener noreferrer"&gt;July 14, 2026&lt;/a&gt;. DuckDB 2.0.0 wheels landed on PyPI on October 21, the same day as the &lt;a href="https://duckdb.org/release_calendar" rel="noopener noreferrer"&gt;release calendar&lt;/a&gt; entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hardware and versions&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VM: 8 vCPU Intel Xeon, 15 GiB RAM, no swap, Linux 6.12. It is a shared box, so other processes were running. Every run waited until at least 5 GiB was free and CPU busy was under 30% over a 3 second window.&lt;/li&gt;
&lt;li&gt;Python 3.13.5, OpenJDK 21.0.12, pyarrow 26.0.0.&lt;/li&gt;
&lt;li&gt;DuckDB 2.0.0, Polars 2.0.0, PySpark 4.2.0. The "before" numbers are DuckDB 1.5.6 on the same box and data, run on October 19.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Data.&lt;/strong&gt; A seeded synthetic star schema written with pyarrow: a &lt;code&gt;sales&lt;/code&gt; fact table and a 50,000-row &lt;code&gt;products&lt;/code&gt; dimension. Three sizes: 10M rows (213 MB), 50M (1.1 GB) and 150M (3.2 GB of snappy Parquet, 30 files of 5M rows). About 10 orders per customer, so 150M rows means roughly 15M customers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queries.&lt;/strong&gt; The same SQL for DuckDB and Spark, the same logic as Polars expressions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;q1_filter_agg&lt;/code&gt;: filter on a timestamp and status, group by store and status.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;q2_join_agg&lt;/code&gt;: fact-to-dimension join, group by category and brand.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;q3_window_latest&lt;/code&gt;: latest order per customer with &lt;code&gt;ROW_NUMBER()&lt;/code&gt;. The classic dedup and SCD pattern.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;q4_highcard_agg&lt;/code&gt;: one row per customer (about 15M groups at 150M).
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- q3_window_latest&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
                                 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Method.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every run is a fresh process: interpreter start, import, session, read Parquet, compute, write Parquet. So no warm caches inside the engine, and Spark pays its JVM start every time.&lt;/li&gt;
&lt;li&gt;Engines run strictly one at a time. The engine order rotates per repetition so no engine always goes first.&lt;/li&gt;
&lt;li&gt;5 repetitions at 10M, 3 at 50M and 150M. I report &lt;strong&gt;medians&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A watchdog samples the RSS of the whole process tree every 50 ms and kills the run if it goes above &lt;strong&gt;4.5 GiB&lt;/strong&gt;. That is the cap. It is deliberately tighter than the box, because "fits in a small container" is the question I care about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wall&lt;/strong&gt; is the whole process. &lt;strong&gt;Query&lt;/strong&gt; is read, compute and write only. The difference is setup.&lt;/li&gt;
&lt;li&gt;Correctness: a validation script compares row counts and rounded checksums of every output. Where an engine produced output, all three engines agreed, for every query and size.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results at 150M rows
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;DuckDB 2.0.0&lt;/th&gt;
&lt;th&gt;Polars 2.0.0&lt;/th&gt;
&lt;th&gt;PySpark 4.2.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Filter + aggregate&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0.87 s&lt;/strong&gt; (3/3)&lt;/td&gt;
&lt;td&gt;0.96 s (3/3)&lt;/td&gt;
&lt;td&gt;10.77 s (3/3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Join + aggregate&lt;/td&gt;
&lt;td&gt;1.47 s (3/3)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1.46 s&lt;/strong&gt; (3/3)&lt;/td&gt;
&lt;td&gt;16.10 s (3/3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Window dedup&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;7.92 s&lt;/strong&gt; (3/3)&lt;/td&gt;
&lt;td&gt;killed (0/3)&lt;/td&gt;
&lt;td&gt;46.29 s (2/3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-cardinality group by&lt;/td&gt;
&lt;td&gt;7.91 s (3/3)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;4.21 s&lt;/strong&gt; (1/3)&lt;/td&gt;
&lt;td&gt;39.70 s (3/3)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Median wall seconds, runs that finished under the 4.5 GiB cap in brackets.&lt;/p&gt;

&lt;p&gt;Peak RSS (median of finished runs) tells the other half:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;DuckDB 2.0.0&lt;/th&gt;
&lt;th&gt;Polars 2.0.0&lt;/th&gt;
&lt;th&gt;PySpark 4.2.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Filter + aggregate&lt;/td&gt;
&lt;td&gt;0.77 GiB&lt;/td&gt;
&lt;td&gt;0.94 GiB&lt;/td&gt;
&lt;td&gt;1.85 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Join + aggregate&lt;/td&gt;
&lt;td&gt;0.42 GiB&lt;/td&gt;
&lt;td&gt;1.96 GiB&lt;/td&gt;
&lt;td&gt;1.61 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Window dedup&lt;/td&gt;
&lt;td&gt;3.01 GiB&lt;/td&gt;
&lt;td&gt;above 4.5 (killed)&lt;/td&gt;
&lt;td&gt;3.74 GiB, one run hit 7.51&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-cardinality group by&lt;/td&gt;
&lt;td&gt;3.01 GiB&lt;/td&gt;
&lt;td&gt;4.49 GiB&lt;/td&gt;
&lt;td&gt;4.40 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things stand out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spark on one box pays twice.&lt;/strong&gt; About 5 seconds of every Spark run is setup (median 5.0 to 5.8 s, JVM plus session) before it reads a byte. Then the query itself is slower: query-only time was 7.4x DuckDB's on the filter, 7.9x on the join, 5.2x on the window dedup and 4.3x on the group by. In wall time that is 5x to 12x. And &lt;code&gt;spark.driver.memory = 3g&lt;/code&gt; did not keep the process at 3 GiB: one window dedup run reached 7.51 GiB of process-tree RSS and was killed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polars is fast right up to the cap.&lt;/strong&gt; It tied DuckDB on filter and join and was the fastest engine on the high-cardinality group by (4.21 s against 7.91 s). But only 1 of 3 runs finished: that one peaked at 4.49 GiB, and the other two crossed the cap at 4.50 and 4.55 GiB. Fast and close to the edge is a fine place for a notebook and a bad place for a scheduled job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The window dedup is where Polars stops.&lt;/strong&gt; At 150M rows it was killed in all 3 runs, within a few seconds of starting, at 4.52 to 4.66 GiB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polars and the window wall
&lt;/h2&gt;

&lt;p&gt;I gave Polars two more chances on the 150M window dedup:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Polars 2.0.0, 150M window dedup&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default settings, 4.5 GiB cap (Oct 22)&lt;/td&gt;
&lt;td&gt;0/3, killed at 4.52 to 4.66 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;POLARS_OOC_MEMORY_BUDGET_MB=3072&lt;/code&gt;, 4.5 GiB cap (Oct 22)&lt;/td&gt;
&lt;td&gt;0/3, killed at 4.57 to 4.66 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default settings, &lt;strong&gt;9 GiB&lt;/strong&gt; cap (Oct 19, same Polars 2.0.0)&lt;/td&gt;
&lt;td&gt;0/3, killed at 9.04 to 9.54 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The out-of-core budget variable is one I found in the Polars 2.0.0 source, not a documented knob, so treat that row as "I tried", not as guidance. The same variable did help the group by: with it, Polars finished 3/3 at 4.67 s and 4.18 to 4.30 GiB.&lt;/p&gt;

&lt;p&gt;At smaller sizes the dedup works, but the shape is already visible:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Window dedup&lt;/th&gt;
&lt;th&gt;DuckDB 2.0.0&lt;/th&gt;
&lt;th&gt;Polars 2.0.0&lt;/th&gt;
&lt;th&gt;PySpark 4.2.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10M rows&lt;/td&gt;
&lt;td&gt;0.60 s, 0.65 GiB&lt;/td&gt;
&lt;td&gt;1.83 s, 1.20 GiB&lt;/td&gt;
&lt;td&gt;12.74 s, 2.94 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50M rows&lt;/td&gt;
&lt;td&gt;2.33 s, 2.19 GiB&lt;/td&gt;
&lt;td&gt;10.36 s, 4.46 GiB&lt;/td&gt;
&lt;td&gt;29.06 s, 3.77 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;150M rows&lt;/td&gt;
&lt;td&gt;7.92 s, 3.01 GiB&lt;/td&gt;
&lt;td&gt;killed&lt;/td&gt;
&lt;td&gt;46.29 s, 3.74 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At 50M Polars already used 4.46 GiB for a query DuckDB did in 2.19 GiB, and took 4.4x longer.&lt;/p&gt;

&lt;p&gt;This is the Polars expression I used. It is the direct translation of the SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;polars&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;

&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Float64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;int_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;over&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;descending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scan_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/sales_150m/*.parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sink_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out/polars/150m_q3_window_latest.parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fair caveat: I did not test other Polars formulations of the same dedup (a sort followed by &lt;code&gt;unique(keep="first")&lt;/code&gt;, or a &lt;code&gt;group_by().agg(...)&lt;/code&gt; with &lt;code&gt;sort_by().last()&lt;/code&gt;). One of them may stream better. The point stands for the common case, though: the first thing a team writes is the window, and that one did not fit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update (Oct 22):&lt;/strong&gt; after this went out, Victor Cid (&lt;a href="https://github.com/Vleyked" rel="noopener noreferrer"&gt;@Vleyked&lt;/a&gt;) tested exactly that in &lt;a href="https://github.com/firfircelik/engine-bench/pull/1" rel="noopener noreferrer"&gt;engine-bench PR #1&lt;/a&gt;. His &lt;code&gt;sort&lt;/code&gt; + &lt;code&gt;unique(keep="first")&lt;/code&gt; version drops the in-memory window fallback from the plan and finished 3/3 at 150M rows under the same 4.5 GiB cap, peaking at 2.50 to 2.74 GiB, but only with &lt;code&gt;POLARS_OOC_MEMORY_BUDGET_MB=1024&lt;/code&gt;. With default settings or a 3072 MB budget it still went over the cap. The price is speed: a 90.58 s median against 17.56 s for DuckDB on the same 4 vCPU runner, so roughly 5x slower. His output matched DuckDB row for row (14,999,347 rows). So Polars can do this dedup under the cap if you rewrite it and tune the spill budget, and with a smarter query it gets close to DuckDB (see below). His runs were on different hardware, so they are not in the tables above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second update:&lt;/strong&gt; in &lt;a href="https://github.com/firfircelik/engine-bench/pull/2" rel="noopener noreferrer"&gt;engine-bench PR #2&lt;/a&gt; he filtered each customer down to their max timestamp first and only sorted those rows to break ties. That brought the median down to 14.23 s at 150M rows with default Polars settings, peaking at 4.04 GiB under the same cap, with output matching DuckDB row for row. On his runner DuckDB's reference was a single 18.50 s run, so I'm not calling that a repeated cross-engine win. The real lesson is that the first way you write a Polars dedup can decide whether it finishes at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third update (Oct 23):&lt;/strong&gt; Ritchie Vink, the creator of Polars, pointed out that only DuckDB was told about the memory limit, so Polars did not know it had to stay under 4 GiB, and suggested setting POLARS_OOC_MEMORY_BUDGET_MB for a fairer comparison. I reran every Polars query at 150M rows on the same box under the same 4.5 GiB cap with that budget, next to a DuckDB rerun in the same session: the high-cardinality group by finished 3/3 at 6.17 s and 3.46 GiB with a 3072 MB budget, against 9.56 s for DuckDB (the box was busier than on Oct 22 and DuckDB ran slower too, so compare within this session, not with the tables above; the Oct 22 run with the same budget gave 4.67 s). The window dedup with over(order_by) still went over the cap in every run at 3072, 1024 and 512 MB, Victor's sort plus unique version fit only at 1024 MB (31.28 s, 2.29 GiB), and the max-timestamp rewrite finished 3/3 on default settings (5.61 s, 4.02 GiB). Ritchie says Polars main now spills the group by correctly and that this ships in the next release, which was not on PyPI yet when I ran this, so I will rerun when it ships. Raw results and output checks are in &lt;a href="https://github.com/firfircelik/engine-bench/pull/3" rel="noopener noreferrer"&gt;engine-bench PR #3&lt;/a&gt; and in results/polars_ooc_budget_2026-10-23 on the polars-ooc-budget-2026-10-23 branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third update (Oct 23):&lt;/strong&gt; Ritchie Vink, the creator of Polars, pointed out that only DuckDB was told about the memory limit, so Polars did not know it had to stay under 4 GiB, and suggested setting POLARS_OOC_MEMORY_BUDGET_MB for a fairer comparison. I reran every Polars query at 150M rows on the same box under the same 4.5 GiB cap with that budget, next to a DuckDB rerun in the same session: the high-cardinality group by finished 3/3 at 6.17 s and 3.46 GiB with a 3072 MB budget, against 9.56 s for DuckDB (the box was busier than on Oct 22 and DuckDB ran slower too, so compare within this session, not with the tables above; the Oct 22 run with the same budget gave 4.67 s). The window dedup with over(order_by) still went over the cap in every run at 3072, 1024 and 512 MB, Victor's sort plus unique version fit only at 1024 MB (31.28 s, 2.29 GiB), and the max-timestamp rewrite finished 3/3 on default settings (5.61 s, 4.02 GiB). Ritchie says Polars main now spills the group by correctly and that this ships in the next release, which was not on PyPI yet when I ran this, so I will rerun when it ships. Raw results and output checks are in &lt;a href="https://github.com/firfircelik/engine-bench/pull/3" rel="noopener noreferrer"&gt;engine-bench PR #3&lt;/a&gt; and in results/polars_ooc_budget_2026-10-23 on the polars-ooc-budget-2026-10-23 branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  DuckDB under pressure: memory_limit = 1 GB
&lt;/h2&gt;

&lt;p&gt;The interesting DuckDB question is not "how fast with room to spare" but "what happens when the box is small". Same 150M rows, &lt;code&gt;memory_limit&lt;/code&gt; cut to 1 GB:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;150M, &lt;code&gt;memory_limit = 1GB&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Wall (median)&lt;/th&gt;
&lt;th&gt;Peak RSS&lt;/th&gt;
&lt;th&gt;Finished&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Window dedup&lt;/td&gt;
&lt;td&gt;14.97 s&lt;/td&gt;
&lt;td&gt;1.12 GiB&lt;/td&gt;
&lt;td&gt;3/3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-cardinality group by&lt;/td&gt;
&lt;td&gt;13.94 s&lt;/td&gt;
&lt;td&gt;1.08 GiB&lt;/td&gt;
&lt;td&gt;3/3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Window dedup, &lt;code&gt;preserve_insertion_order = false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;14.94 s&lt;/td&gt;
&lt;td&gt;1.12 GiB&lt;/td&gt;
&lt;td&gt;3/3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Group by, &lt;code&gt;preserve_insertion_order = false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;13.88 s&lt;/td&gt;
&lt;td&gt;1.08 GiB&lt;/td&gt;
&lt;td&gt;3/3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With a third of the memory, DuckDB spilled to disk and still beat Spark's unconstrained run by about 3x on both queries, at a quarter to a third of Spark's peak RSS. The DuckDB docs list window functions with &lt;code&gt;PARTITION BY ... ORDER BY&lt;/code&gt; among the blocking operators it can process larger than memory (&lt;a href="https://duckdb.org/docs/current/guides/performance/how_to_tune_workloads" rel="noopener noreferrer"&gt;How to Tune Workloads&lt;/a&gt;). This run is that sentence, measured.&lt;/p&gt;

&lt;p&gt;The setup is three lines:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET memory_limit = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1GB&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET threads = 8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET temp_directory = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/mnt/fast-local-disk/duckdb_tmp&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# where spills go
&lt;/span&gt;&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    COPY (
        SELECT customer_id, order_id, ts, qty * price AS amount
        FROM (
            SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id
                                         ORDER BY ts DESC, order_id DESC) AS rn
            FROM read_parquet(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data/sales_150m/*.parquet&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
        )
        WHERE rn = 1
    ) TO &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;out/latest_per_customer.parquet&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; (FORMAT parquet)
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  DuckDB 1.5.6 vs 2.0.0
&lt;/h2&gt;

&lt;p&gt;Same box, same data, same scripts. 1.5.6 ran on October 19, 2.0.0 on October 22. Median wall seconds at 150M:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;1.5.6&lt;/th&gt;
&lt;th&gt;2.0.0&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory_limit = 3GB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Window dedup&lt;/td&gt;
&lt;td&gt;9.09&lt;/td&gt;
&lt;td&gt;7.92&lt;/td&gt;
&lt;td&gt;-13%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory_limit = 3GB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Group by&lt;/td&gt;
&lt;td&gt;9.61&lt;/td&gt;
&lt;td&gt;7.91&lt;/td&gt;
&lt;td&gt;-18%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory_limit = 1GB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Window dedup&lt;/td&gt;
&lt;td&gt;17.79&lt;/td&gt;
&lt;td&gt;14.97&lt;/td&gt;
&lt;td&gt;-16%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory_limit = 1GB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Group by&lt;/td&gt;
&lt;td&gt;22.06&lt;/td&gt;
&lt;td&gt;13.94&lt;/td&gt;
&lt;td&gt;-37%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1GB, no insertion order&lt;/td&gt;
&lt;td&gt;Window dedup&lt;/td&gt;
&lt;td&gt;14.85&lt;/td&gt;
&lt;td&gt;14.94&lt;/td&gt;
&lt;td&gt;+1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1GB, no insertion order&lt;/td&gt;
&lt;td&gt;Group by&lt;/td&gt;
&lt;td&gt;15.15&lt;/td&gt;
&lt;td&gt;13.88&lt;/td&gt;
&lt;td&gt;-8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The useful finding is the last four rows together. On 1.5.6, &lt;code&gt;SET preserve_insertion_order = false&lt;/code&gt; was worth 16% on the dedup and 31% on the group by under a 1 GB limit. On 2.0.0 the default already lands where the tuned setting did, and the flag changes almost nothing for these two queries. If you carry that setting in job configs as a performance trick, re-measure after upgrading. It still matters for what it was designed for: the docs recommend it when large imports or exports run out of memory, and it lets DuckDB reorder rows without an &lt;code&gt;ORDER BY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Not everything improved. The 50M window dedup was slower on 2.0.0 (1.94 s to 2.33 s), and peak RSS for the light queries went up (150M filter: 0.26 to 0.77 GiB, join: 0.23 to 0.42 GiB), while the heavy queries went down (window dedup: 3.47 to 3.01 GiB). I have not investigated why. The two runs were also three days apart on a shared box, so treat differences under about 10 to 15% as noise. The 1 GB group by drop is far outside that.&lt;/p&gt;

&lt;p&gt;The release itself is mostly a feature release: Quack and &lt;code&gt;CONNECT&lt;/code&gt; for client-server use, asynchronous I/O, a first-class &lt;code&gt;VARIANT&lt;/code&gt;, triggers, a PEG parser by default (&lt;a href="https://duckdb.org/2026/10/21/announcing-duckdb-200" rel="noopener noreferrer"&gt;announcement&lt;/a&gt;). It is an LTS line supported until at least October 2027. One migration note matters for pipelines: database files written by 2.0 cannot be read by versions before 2.0. My benchmark only reads and writes Parquet, so it never touched that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Spark still wins
&lt;/h2&gt;

&lt;p&gt;None of this says "replace Spark". It says Spark on one box is the wrong comparison for small and medium jobs. Spark still wins when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The data does not fit one node's disk and memory, or the deadline does not fit one node's cores.&lt;/strong&gt; This test stops at 3.2 GB of Parquet on purpose. DuckDB spills well, but one machine is still one machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The platform is built around it.&lt;/strong&gt; On Databricks and Fabric, Spark is where Unity Catalog or OneLake governance, Delta writes with concurrent writers, lineage, job orchestration and streaming already live. A DuckDB step outside that boundary is a step you have to secure, monitor and explain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cluster is already running.&lt;/strong&gt; The 5 second JVM start I measured per run disappears in a warm interactive cluster or a long job. The query-only gap (4x to 8x here) does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The team is a Spark team.&lt;/strong&gt; One engine everyone can debug at 02:00 is worth more than a faster engine one person understands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My opinion as a Lead DE on Azure, Databricks and Fabric: the line is not the engine, it is the size of the job. Most jobs I see are tens of millions of rows dressed up as big data and started on a cluster. For those (file-to-file transforms before landing, validation and reconciliation checks, small dimension builds, CI tests for SQL logic) a single-node engine with a memory limit is the cheaper and more predictable choice. Anything that writes governed Delta tables stays on the platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes I hit, so you don't have to
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Engine memory settings are not process memory.&lt;/strong&gt; Spark's 3g driver heap still produced a 7.51 GiB process. DuckDB's whole process stayed close to its &lt;code&gt;memory_limit&lt;/code&gt;: about 3.0 GiB peak at a 3 GB limit, 1.1 GiB at 1 GB. Size containers on measured RSS, not on the setting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Streaming" does not mean bounded.&lt;/strong&gt; Polars streamed the filter, join and group by fine. The window dedup with an &lt;code&gt;over(order_by=...)&lt;/code&gt; did not stay bounded, even with 9 GiB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Close to the cap is a failure that hasn't happened yet.&lt;/strong&gt; Polars' group by finished in 1 of 3 runs, peaking at 4.49 GiB against a 4.5 GiB cap. Same query, same data, same version. Schedule that and it fails some nights.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared hosts lie.&lt;/strong&gt; One Spark run at 10M was killed because free memory on the whole box fell under my 1 GiB safety floor while Spark itself was at 1.6 GiB. That was the neighbours, not Spark. Gate each run on free memory and CPU, and record both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate outputs, not just timings.&lt;/strong&gt; Every engine produced the same row counts and checksums where it finished. Without that check, a fast wrong answer looks like a win.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;Everything is public at &lt;a href="https://github.com/firfircelik/engine-bench" rel="noopener noreferrer"&gt;firfircelik/engine-bench&lt;/a&gt;: scripts, raw results, pinned versions and the query plans. Here are the parts that matter. The data generator is seeded (&lt;code&gt;SEED = 20261019&lt;/code&gt;) and writes 5M-row chunks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_rng&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;SEED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_index&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&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="mi"&gt;270&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&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="mi"&gt;50_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;store_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&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="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;int16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;int16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;500.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STATUSES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sales_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;m/part-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chunk_index&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;row_group_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snappy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spark session, the only non-default settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;spark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SparkSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;master&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local[8]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.driver.memory&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3g&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.local.dir&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TMP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.ui.enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.session.timeZone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrCreate&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the watchdog, which is the part I would copy into any benchmark of your own. It measures the whole process tree, because JVMs and thread pools do not show up in the parent's RSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psutil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;GIB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tree_rss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;children&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recursive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memory_info&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;rss&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;psutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoSuchProcess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;

&lt;span class="n"&gt;proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;peak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tree_rss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;peak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rss&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;4.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;GIB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;children&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recursive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;On one 8 vCPU machine with a 4.5 GiB cap, DuckDB 2.0.0 was the only engine that finished every run of every query at every size, and it did the heavy ones with 1 GB. Polars matched or beat it on the light queries and the group by, and could not finish the window dedup at 150M even with 9 GiB. Local Spark finished almost everything, 5x to 12x slower, and its memory setting did not bound its memory.&lt;/p&gt;

&lt;p&gt;If your job fits on one machine, measure it on one machine before you start a cluster for it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://duckdb.org/2026/10/21/announcing-duckdb-200" rel="noopener noreferrer"&gt;Announcing DuckDB 2.0.0 LTS "Cinnamon Teal"&lt;/a&gt;, DuckDB team, October 21, 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://duckdb.org/release_calendar" rel="noopener noreferrer"&gt;DuckDB release calendar&lt;/a&gt; (2.0.0 LTS on 2026-10-21, 1.5.6 on 2026-09-28)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pypi.org/project/duckdb/2.0.0/" rel="noopener noreferrer"&gt;duckdb 2.0.0 on PyPI&lt;/a&gt;, &lt;a href="https://pypi.org/project/polars/2.0.0/" rel="noopener noreferrer"&gt;polars 2.0.0 on PyPI&lt;/a&gt;, &lt;a href="https://pypi.org/project/pyspark/4.2.0/" rel="noopener noreferrer"&gt;pyspark 4.2.0 on PyPI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://duckdb.org/docs/current/guides/performance/how_to_tune_workloads" rel="noopener noreferrer"&gt;DuckDB: How to Tune Workloads&lt;/a&gt; (larger-than-memory operators, &lt;code&gt;preserve_insertion_order&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://duckdb.org/docs/current/sql/dialect/order_preservation" rel="noopener noreferrer"&gt;DuckDB: Order Preservation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;All timings and memory numbers: my own runs on October 19 (DuckDB 1.5.6, and the 9 GiB Polars and Spark runs) and October 22, 2026 (DuckDB 2.0.0, Polars 2.0.0, PySpark 4.2.0).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>duckdb</category>
      <category>dataengineering</category>
      <category>python</category>
      <category>spark</category>
    </item>
    <item>
      <title>FlipPrice: project report, side-hustle pricing copilot for vintage resellers</title>
      <dc:creator>Firat Celik</dc:creator>
      <pubDate>Tue, 13 Oct 2026 21:47:16 +0000</pubDate>
      <link>https://dev.to/firfircelik/flipprice-a-side-hustle-pricing-copilot-for-vintage-resellers-457m</link>
      <guid>https://dev.to/firfircelik/flipprice-a-side-hustle-pricing-copilot-for-vintage-resellers-457m</guid>
      <description>&lt;h1&gt;
  
  
  FlipPrice
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Can Open Models Price Second-Hand Goods Well Enough for Resellers?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Week 2 · Side Hustle Project Report&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/firfircelik/flipprice/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FLicense-MIT-blue.svg" alt="License: MIT" width="82" height="20"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/firfircelik/flipprice" rel="noopener noreferrer"&gt;https://github.com/firfircelik/flipprice&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Can an open-model stack turn a second-hand item into a usable listing, a price estimate, and estimated net proceeds — without relying on paid, per-request AI APIs?&lt;/p&gt;

&lt;p&gt;That is the question behind &lt;strong&gt;FlipPrice&lt;/strong&gt;, a lightweight pricing and listing assistant for vintage resellers and second-hand sellers.&lt;/p&gt;

&lt;p&gt;The idea is simple: researching comparable sold items takes time, while per-listing AI fees accumulate. FlipPrice explores a local-first alternative that separates listing generation from price estimation.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Problem
&lt;/h2&gt;

&lt;p&gt;Resellers listing 20–50 items a week face two recurring challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pricing uncertainty:&lt;/strong&gt; researching comparable sold items takes time, and asking prices do not necessarily reflect realised sale prices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accumulating AI costs:&lt;/strong&gt; at an illustrative $0.05–$0.15 per listing, API costs can add up before a product sells.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Listings per week&lt;/th&gt;
&lt;th&gt;Approx. listings per month&lt;/th&gt;
&lt;th&gt;Illustrative monthly AI cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;$4.35–$13.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;217&lt;/td&gt;
&lt;td&gt;$10.85–$32.55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;867&lt;/td&gt;
&lt;td&gt;$43.35–$130.05&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Estimates use 52 weeks divided by 12 months. Actual costs depend on provider pricing and token usage.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The goal: generate a listing draft, estimate a selling price, and calculate expected net proceeds at near-zero marginal model-inference cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Architecture
&lt;/h2&gt;

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

&lt;p&gt;FlipPrice uses a lightweight FastAPI service and separates language generation from numerical price estimation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Listing generation: Gemma 3 + Ollama
&lt;/h3&gt;

&lt;p&gt;Gemma 3 runs locally through Ollama and generates listing copy from a constrained prompt. Output stripping and a brand-first title rule help reduce unwanted preambles and improve consistency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pricing: Ridge with an optional TabPFN path
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ridge regression:&lt;/strong&gt; the implemented fallback pricing path, using target-encoded features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN:&lt;/strong&gt; an optional tabular-model path. The code is implemented, but live verification is pending because the required license token is unavailable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Training data:&lt;/strong&gt; a 20-row seed dataset, currently suitable for early development rather than proving market-wide pricing accuracy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The separation is intentional: a language model can write convincing copy without knowing the correct market price.&lt;/p&gt;

&lt;h3&gt;
  
  
  API, storage, and validation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI for the application layer.&lt;/li&gt;
&lt;li&gt;Local JSON storage, with MongoDB Atlas as a possible upgrade.&lt;/li&gt;
&lt;li&gt;Condition validation through an enum.&lt;/li&gt;
&lt;li&gt;Non-negative cost validation.&lt;/li&gt;
&lt;li&gt;Photo uploads capped at 2 MB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are baseline application safeguards, not a complete production security assessment.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Initial Results
&lt;/h2&gt;

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

&lt;p&gt;Three example items were processed through the prototype using live Gemma 3 on an Apple M2 machine.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Purchase cost&lt;/th&gt;
&lt;th&gt;Estimated P50 sale price&lt;/th&gt;
&lt;th&gt;Estimated net proceeds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Canon AE-1&lt;/td&gt;
&lt;td&gt;$60.00&lt;/td&gt;
&lt;td&gt;$286.34&lt;/td&gt;
&lt;td&gt;$189.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Levi's 501&lt;/td&gt;
&lt;td&gt;$15.00&lt;/td&gt;
&lt;td&gt;$108.85&lt;/td&gt;
&lt;td&gt;$79.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sony WH-1000XM3&lt;/td&gt;
&lt;td&gt;$40.00&lt;/td&gt;
&lt;td&gt;$116.82&lt;/td&gt;
&lt;td&gt;$61.63&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;These are recorded prototype outputs, not independently verified market values or guaranteed resale outcomes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The experiment shows that the workflow can produce item-specific estimates. It does not yet demonstrate pricing accuracy. That requires independent sold-price data, a held-out test set, and explicit error metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Latency
&lt;/h2&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Observed latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Listing draft generation&lt;/td&gt;
&lt;td&gt;6.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing, cold run&lt;/td&gt;
&lt;td&gt;5.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing, warm run&lt;/td&gt;
&lt;td&gt;9 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline round trip&lt;/td&gt;
&lt;td&gt;1.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are initial development-environment measurements. The number of repetitions, percentile distribution, and complete test procedure still need to be standardised.&lt;/p&gt;

&lt;p&gt;The main observation is the difference between cold and warm pricing. Reusing loaded components and avoiding unnecessary initialisation appear to be important optimisation opportunities.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Debugging Notes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Random Forest inverted the expected condition relationship
&lt;/h3&gt;

&lt;p&gt;With only 20 training rows, Random Forest returned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;like_new&lt;/code&gt;: 97.51&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fair&lt;/code&gt;: 103.36&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model assigned a higher estimate to the fair-condition example. This exposed a weakness in the current model and dataset, not proof that Random Forest is inherently unsuitable for resale pricing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Change:&lt;/strong&gt; Ridge regression replaced the fallback model, producing a cleaner gradient in the tested examples. A regression test now guards against this specific failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  TabPFN cold-start overhead
&lt;/h3&gt;

&lt;p&gt;The TabPFN import path added approximately 15 seconds of cold-start overhead without a license token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Change:&lt;/strong&gt; The path was gated on token availability, reducing the observed cold-path time from approximately 17 seconds to 5 seconds in the measured setup. Live TabPFN pricing remains unverified.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unwanted title preambles
&lt;/h3&gt;

&lt;p&gt;Small models sometimes added introductory text to generated titles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Change:&lt;/strong&gt; Output stripping and a brand-first title rule improved consistency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tests
&lt;/h3&gt;

&lt;p&gt;The automated suite reports &lt;strong&gt;8/8 passing tests&lt;/strong&gt;. This supports basic functional correctness but does not establish pricing accuracy or production readiness.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Unit Economics
&lt;/h2&gt;

&lt;p&gt;Local inference removes the separate hosted-model inference fee for each listing, but it does not make the system completely free to operate.&lt;/p&gt;

&lt;p&gt;Costs still include hardware, electricity, maintenance, storage, development, and any external services.&lt;/p&gt;

&lt;p&gt;The business case therefore depends on two questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Can local inference reduce the marginal cost of preparing listings?&lt;/li&gt;
&lt;li&gt;Can the price estimates improve reseller decisions enough to justify adoption?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The architecture supports the first hypothesis. The second remains unvalidated.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small dataset:&lt;/strong&gt; 20 rows cannot adequately represent the diversity of second-hand markets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No independent accuracy benchmark:&lt;/strong&gt; MAE, median absolute error, category-level errors, and interval coverage have not yet been reported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN is not live-verified:&lt;/strong&gt; its code path exists, but the required license token is unavailable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No external users:&lt;/strong&gt; results are agent-run dogfooding simulations documented in &lt;code&gt;FIRST_USER_TEST.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline operation needs broader verification:&lt;/strong&gt; the complete workflow must be tested with network access disabled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commercial value is unproven:&lt;/strong&gt; time saved, realised sale prices, and willingness to pay have not been measured.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. What Comes Next?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Benchmark pricing accuracy
&lt;/h3&gt;

&lt;p&gt;Build a representative dataset of completed sales and evaluate against a held-out test set. Report MAE, median absolute error, suitable percentage-error metrics, and performance by category and condition. Compare against simple baselines such as category medians.&lt;/p&gt;

&lt;p&gt;Prevent duplicate and near-duplicate listings from leaking across training and evaluation.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Run a real reseller pilot
&lt;/h3&gt;

&lt;p&gt;Recruit a small group of second-hand sellers and measure listing preparation time, draft acceptance, agreement with suggested prices, eventual sale prices, and willingness to pay.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Standardise performance tests
&lt;/h3&gt;

&lt;p&gt;Document hardware, model configuration, repetition counts, and latency percentiles. Test offline operation with external network access disabled.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Validate TabPFN
&lt;/h3&gt;

&lt;p&gt;If access becomes available, compare TabPFN, Ridge, and simple baselines on the same held-out dataset. Keep the fallback until the alternative is verified.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Test willingness to pay
&lt;/h3&gt;

&lt;p&gt;Once pricing quality and time savings are demonstrated, test a simple commercial offer. The important question is whether the value delivered exceeds the cost and uncertainty of the seller's existing process.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Conclusion
&lt;/h2&gt;

&lt;p&gt;FlipPrice demonstrates a local-first prototype that combines open-model listing generation with a separate tabular pricing layer.&lt;/p&gt;

&lt;p&gt;The initial run produced listing drafts and item-specific price and net-proceeds estimates. Warm pricing latency reached 9 ms in the measured setup, and all eight automated tests passed.&lt;/p&gt;

&lt;p&gt;These results support the feasibility of the implementation. They do not yet prove reliable market-price prediction or commercial demand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The next milestone is evidence:&lt;/strong&gt; validate estimates against real completed sales and test the workflow with actual resellers.&lt;/p&gt;

&lt;p&gt;If FlipPrice can consistently save time and produce estimates sellers trust, its local-first architecture could provide a meaningful cost advantage. If it cannot, low inference cost alone will not make it a viable product.&lt;/p&gt;

&lt;p&gt;That is what the next iteration needs to answer.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a href="https://github.com/firfircelik/flipprice" rel="noopener noreferrer"&gt;FlipPrice on GitHub&lt;/a&gt; · &lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FlipPrice is an experimental project. Estimates are informational and are not guaranteed resale values.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>Python 3.15 Is Not a Speed Release for Data Teams. It Is an Operations Release.</title>
      <dc:creator>Firat Celik</dc:creator>
      <pubDate>Mon, 12 Oct 2026 07:12:22 +0000</pubDate>
      <link>https://dev.to/firfircelik/python-315-is-not-a-speed-release-for-data-teams-it-is-an-operations-release-5fbo</link>
      <guid>https://dev.to/firfircelik/python-315-is-not-a-speed-release-for-data-teams-it-is-an-operations-release-5fbo</guid>
      <description>&lt;p&gt;Python 3.15.0 shipped on October 9, 2026 (&lt;a href="https://blog.python.org/2026/10/python-3150-final-is-here/" rel="noopener noreferrer"&gt;release post&lt;/a&gt;). The headline number most people will repeat is in the JIT section of &lt;a href="https://docs.python.org/3.15/whatsnew/3.15.html" rel="noopener noreferrer"&gt;What's New&lt;/a&gt;: a "7-8% geometric mean performance improvement" over the standard interpreter on x86-64 Linux, and 11-12% on AArch64 macOS.&lt;/p&gt;

&lt;p&gt;For a data team, the tempting reading is "upgrade the job images, pipelines get faster." I installed 3.15.0 on a VM, ran a small pandas/pyarrow workload with the JIT off and on, and then spent most of my time on three features that do not touch throughput at all. Those three are the ones that change how you run Python next week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thesis
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For data teams, Python 3.15 is an operations release, not a speed release.&lt;/strong&gt; The features that matter are about startup (PEP 810 lazy imports), observability (the Tachyon sampling profiler and frame pointers on by default) and supply chain (PEP 829 &lt;code&gt;.start&lt;/code&gt; files replacing &lt;code&gt;import&lt;/code&gt; lines in &lt;code&gt;.pth&lt;/code&gt; files).&lt;/p&gt;

&lt;p&gt;Each of them also has a trap that the release notes do not spell out. I hit all of them on the VM, and they are documented below with the commands to reproduce them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The antithesis, at full strength
&lt;/h2&gt;

&lt;p&gt;The best counter-case is not silly. It goes like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The JIT is real now.&lt;/strong&gt; What's New lists a new tracing frontend, basic register allocation, more constant propagation and reference count elimination. The pyperformance range is wide, "from roughly 15% slowdown to over 100% speedup", but the geometric mean is positive on both platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data engineers write more Python-level loops than they admit.&lt;/strong&gt; Row-wise &lt;code&gt;apply&lt;/code&gt;, per-record parsing, UDFs, validation code. That is exactly the code a JIT can see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free-threading got a stable ABI.&lt;/strong&gt; PEP 803 adds &lt;code&gt;abi3t&lt;/code&gt;: extensions that target the Stable ABI can now be compiled so they are compatible with free-threaded builds. That is the long road to real multi-core Python for CPU-bound pipeline code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the antithesis is: the startup and tooling features are nice, but the throughput work is what will show up on the cloud bill.&lt;/p&gt;

&lt;p&gt;Here is what that looked like on the VM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;Everything below ran on one shared VM: 8 vCPU Intel Xeon, 15 GiB RAM, Debian 13, kernel 6.12. Python 3.15.0 is the uv-managed build (Clang 22, tail-calling interpreter, JIT compiled in but off by default, turned on with &lt;code&gt;PYTHON_JIT=1&lt;/code&gt;). Libraries: numpy 2.5.4, pandas 3.0.6, pyarrow 26.0.0, all installed from &lt;code&gt;cp315&lt;/code&gt; wheels three days after the release. The VM is shared with other workloads, so I report medians of repeated fresh processes and repeat whole rounds.&lt;/p&gt;

&lt;p&gt;One thing I did not expect: the VM had uv 0.12.24, and &lt;code&gt;uv python install 3.15&lt;/code&gt; installed &lt;strong&gt;3.15.0rc3&lt;/strong&gt;, not the final release. uv 0.13.0 installed 3.15.0. If your images resolve "3.15" through an older tool, check &lt;code&gt;python -VV&lt;/code&gt; before you trust anything. That matters again in the profiler section.&lt;/p&gt;

&lt;h3&gt;
  
  
  JIT off vs on
&lt;/h3&gt;

&lt;p&gt;Three workloads, timed in-process, 7 fresh processes per setting, two rounds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;JIT off (median)&lt;/th&gt;
&lt;th&gt;JIT on (median)&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pure-Python parsing loop, 1M rows&lt;/td&gt;
&lt;td&gt;337.6 / 332.4 ms&lt;/td&gt;
&lt;td&gt;309.7 / 304.6 ms&lt;/td&gt;
&lt;td&gt;about 8% faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pandas row-wise &lt;code&gt;apply&lt;/code&gt;, 100k rows&lt;/td&gt;
&lt;td&gt;1176.9 / 1183.6 ms&lt;/td&gt;
&lt;td&gt;1126.8 / 1144.3 ms&lt;/td&gt;
&lt;td&gt;about 3 to 4% faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pandas vectorized &lt;code&gt;groupby().sum()&lt;/code&gt;, 2M rows&lt;/td&gt;
&lt;td&gt;63.5 / 76.3 ms&lt;/td&gt;
&lt;td&gt;63.3 / 71.4 ms&lt;/td&gt;
&lt;td&gt;within noise (single runs ranged 60 to 103 ms)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;/p&gt;
  jit_bench.py (the three workloads)
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Time three data-team-shaped workloads in-process. Run with PYTHON_JIT=0 or 1.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pure_python_parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;97&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;vectorized_groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2_000_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_rng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&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="mi"&gt;50_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;iloc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rowwise_apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; User&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;+spam@Example.com &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)]})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+spam&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;iloc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="n"&gt;WORKLOADS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pure_python_parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pure_python_parse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vectorized_groupby&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;vectorized_groupby&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rowwise_apply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rowwise_apply&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WORKLOADS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# warm-up
&lt;/span&gt;&lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Run it as &lt;code&gt;PYTHON_JIT=0 python jit_bench.py pure_python_parse&lt;/code&gt; and &lt;code&gt;PYTHON_JIT=1 ...&lt;/code&gt;, several times each.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;What the numbers support (interpretation): the JIT does what the release notes say for Python-level code. The pure-Python loop landed right in the 7-8% band. But a pipeline whose hot path is already in numpy, pandas or Arrow does not get faster, because the JIT never sees that code. And the row-wise &lt;code&gt;apply&lt;/code&gt; is the honest middle: 3 to 4% is real, but the fix for a 1.2 second &lt;code&gt;apply&lt;/code&gt; is to vectorize it, not to wait for a JIT.&lt;/p&gt;

&lt;p&gt;The free-threading ABI is a packaging milestone. What's New says moving an extension to &lt;code&gt;abi3t&lt;/code&gt; "usually requires some non-trivial changes to the source code", and it pays off only when the extensions you depend on ship those wheels and you run a free-threaded build (&lt;code&gt;cp315t&lt;/code&gt;). That is worth tracking, not something you collect by bumping a base image.&lt;/p&gt;

&lt;p&gt;So the antithesis is partly right: there is a free win for Python-level code. It is small, it is opt-in, and it is experimental. The three features below change your runbooks this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Lazy imports fix startup, not throughput
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The wrong assumption:&lt;/strong&gt; "lazy imports make Python faster."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mechanics:&lt;/strong&gt; &lt;code&gt;lazy import pandas as pd&lt;/code&gt; binds &lt;code&gt;pd&lt;/code&gt; to a lightweight proxy. The module is loaded the first time the name is used, a step PEP 810 calls reification. If loading fails, the error is raised at first use, and the traceback shows both places. Lazy imports are only allowed at module scope: inside a function, a class body or a &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;except&lt;/code&gt; they are a &lt;code&gt;SyntaxError&lt;/code&gt;. I confirmed the &lt;code&gt;try&lt;/code&gt; case on 3.15.0, which rules out the classic &lt;code&gt;try: import orjson as json&lt;/code&gt; fallback.&lt;/p&gt;

&lt;p&gt;I wrote a toy pipeline CLI, &lt;code&gt;pipectl&lt;/code&gt;, with the imports most data tools have at the top: numpy, pandas, pyarrow, requests. It has a &lt;code&gt;version&lt;/code&gt; command, a &lt;code&gt;check&lt;/code&gt; command that validates a JSON config, and a &lt;code&gt;run&lt;/code&gt; command that aggregates 2M rows and writes Parquet. The lazy variant changes only the import lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;lazy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;lazy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="n"&gt;lazy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="n"&gt;lazy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pyarrow&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;
&lt;span class="n"&gt;lazy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pyarrow.parquet&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;
&lt;span class="n"&gt;lazy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each command was run 30 times in a fresh process after warm-up, in three clean rounds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Eager (median per round)&lt;/th&gt;
&lt;th&gt;Lazy (median per round)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;python -c pass&lt;/code&gt; (floor)&lt;/td&gt;
&lt;td&gt;9.3 ms&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pipectl version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;351.4 / 381.2 / 344.4 ms&lt;/td&gt;
&lt;td&gt;20.0 / 19.6 / 19.9 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pipectl check cfg.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;352.1 / 342.7 / 352.1 ms&lt;/td&gt;
&lt;td&gt;21.2 / 21.1 / 20.8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pipectl run&lt;/code&gt; (2M rows)&lt;/td&gt;
&lt;td&gt;453.2 / 441.0 / 450.8 ms&lt;/td&gt;
&lt;td&gt;400.5 / 392.8 / 400.5 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;-X importtime&lt;/code&gt; shows where the eager 350 ms goes: pandas about 180 ms cumulative, numpy about 40 ms, requests about 35 ms (medians of three runs, and importtime adds its own overhead).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;version&lt;/code&gt; and &lt;code&gt;check&lt;/code&gt; rows are the point of the feature: about 94% less wall time, because those commands never touch pandas. That is more than the "50-70% in practice" that &lt;a href="https://peps.python.org/pep-0810/" rel="noopener noreferrer"&gt;PEP 810&lt;/a&gt; cites for command-line tools, and it should be: my CLI is the extreme case where the cheap commands use none of the heavy imports.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;run&lt;/code&gt; row is the one to read twice. Lazy saved about 50 ms, so did it make the work faster? No. I ran a third variant, eager but with the &lt;code&gt;import requests&lt;/code&gt; line deleted:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;
&lt;code&gt;pipectl run&lt;/code&gt;, 20 runs each&lt;/th&gt;
&lt;th&gt;Median&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;eager&lt;/td&gt;
&lt;td&gt;454.9 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;eager without &lt;code&gt;import requests&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;410.4 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lazy&lt;/td&gt;
&lt;td&gt;413.3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The whole gain is the import that the &lt;code&gt;run&lt;/code&gt; path never uses. &lt;strong&gt;Lazy imports remove the imports you don't use. They don't make the ones you do use any cheaper.&lt;/strong&gt; If your job imports pandas and uses pandas, it pays the full 180 ms either way.&lt;/p&gt;

&lt;p&gt;Where this pays off for data teams (interpretation): CLIs, &lt;code&gt;--help&lt;/code&gt; in CI, pre-commit hooks, tests that import a package to run one function, notebook kernels, and anything that gets parsed often. The Airflow docs already warn that "import statements also count as top-level code" in Dag files and that expensive imports there hurt the scheduler (&lt;a href="https://airflow.apache.org/docs/apache-airflow/stable/best-practices.html#top-level-python-code" rel="noopener noreferrer"&gt;Airflow best practices&lt;/a&gt;). The &lt;code&gt;lazy&lt;/code&gt; keyword looks built for that pattern. I did not test it inside Airflow, and you need an Airflow version that supports 3.15 first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trap 1: the global switch broke pandas
&lt;/h3&gt;

&lt;p&gt;3.15 also has an interpreter-wide mode: &lt;code&gt;-X lazy_imports=all&lt;/code&gt; or &lt;code&gt;PYTHON_LAZY_IMPORTS=all&lt;/code&gt; makes every module-level import in every module potentially lazy, third-party code included. It is tempting to flip it in a job image and collect the startup win without touching code. On 3.15.0 with pandas 3.0.6 and numpy 2.5.4, that failed 5 out of 5 times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ PYTHON_LAZY_IMPORTS=all python -c "import pandas as pd; pd.DataFrame({'a': [1]})"
  File ".../pandas/_typing.py", line 401, in &amp;lt;module&amp;gt;
    TakeIndexer: TypeAlias = Sequence[int] | Sequence[np.integer] | npt.NDArray[np.integer]
                                                                    ~~~~~~~~~~~^^^^^^^^^^^^
TypeError: 'lazy_import' object is not subscriptable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The minimal trigger is &lt;code&gt;npt.NDArray[np.integer]&lt;/code&gt; with &lt;code&gt;numpy.typing&lt;/code&gt; imported under the global mode. numpy alone and requests alone worked. A tiny pure-Python package with the same re-export chain worked too, so this looks specific to how &lt;code&gt;numpy.typing&lt;/code&gt; (which defines a module-level &lt;code&gt;__getattr__&lt;/code&gt;) interacts with global laziness. I did not chase it further. The lesson does not depend on the root cause: the global mode changes the import semantics of code you did not write, and the &lt;code&gt;lazy&lt;/code&gt; keyword in your own entry point does not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trap 2: side-effect imports silently stop happening
&lt;/h3&gt;

&lt;p&gt;Plenty of data code imports a module only for what it does at import time: registering a pandas accessor, a SQLAlchemy dialect, a plugin. Linters flag these as unused, and they are the first thing someone will mark &lt;code&gt;lazy&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;lazy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="n"&gt;lazy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dq_accessors&lt;/span&gt;  &lt;span class="c1"&gt;# noqa: F401  (registers df.dq at import time)
&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;null_ratio&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AttributeError: 'DataFrame' object has no attribute 'dq'. Did you mean '.eq' instead of '.dq'?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The name &lt;code&gt;dq_accessors&lt;/code&gt; is never used, so it is never reified, and the registration never runs. The eager version prints &lt;code&gt;{'a': 0.333, 'b': 0.667}&lt;/code&gt;. PEP 810 says this directly: side effects are "deferred until first use", and it recommends explicit initialization functions instead of import-time registration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trap 3: the PEP's kill switch does not exist in 3.15.0
&lt;/h3&gt;

&lt;p&gt;PEP 810 describes a third mode, &lt;code&gt;"none"&lt;/code&gt;, to force every import eager. The shipped 3.15.0 accepts only &lt;code&gt;normal&lt;/code&gt; and &lt;code&gt;all&lt;/code&gt;, which matches the &lt;a href="https://docs.python.org/3.15/library/sys.html" rel="noopener noreferrer"&gt;sys docs&lt;/a&gt;. Copying the PEP is not harmless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ PYTHON_LAZY_IMPORTS=none python -c pass
Fatal Python error: config_init_lazy_imports: PYTHON_LAZY_IMPORTS: invalid value; expected 'all' or 'normal'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code 1, before any of your code runs. &lt;code&gt;sys.set_lazy_imports("none")&lt;/code&gt; raises &lt;code&gt;ValueError&lt;/code&gt;. The PEP page itself now says it is "a historical document". If you want an emergency "everything eager" switch, a filter that returns &lt;code&gt;False&lt;/code&gt; does it (verified: &lt;code&gt;json&lt;/code&gt; loads at the &lt;code&gt;lazy import&lt;/code&gt; line):&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_lazy_imports_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fromlist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Runbook: lazy imports
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Measure first: &lt;code&gt;python -X importtime -m yourtool --help 2&amp;gt; imports.txt&lt;/code&gt;, then sort by the cumulative column.&lt;/li&gt;
&lt;li&gt;Mark only the heavy imports in your own entry point &lt;code&gt;lazy&lt;/code&gt;. Leave third-party internals alone.&lt;/li&gt;
&lt;li&gt;Keep side-effect imports eager, or replace them with an explicit &lt;code&gt;register()&lt;/code&gt; call.&lt;/li&gt;
&lt;li&gt;Do not set &lt;code&gt;PYTHON_LAZY_IMPORTS=all&lt;/code&gt; in a job image unless the full test suite passes under it.&lt;/li&gt;
&lt;li&gt;If you must support 3.14 too, use &lt;code&gt;__lazy_modules__ = ["pandas", ...]&lt;/code&gt; with normal &lt;code&gt;import&lt;/code&gt; statements. 3.15 treats those imports as lazy, and older versions just see a module variable and import eagerly (checked on 3.15.0 and 3.13.5 with &lt;code&gt;json&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  2. Tachyon: attach to the stuck job by PID, and read the error rate
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The wrong assumption:&lt;/strong&gt; "a sampling profiler is free and always right."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mechanics:&lt;/strong&gt; &lt;code&gt;profiling.sampling&lt;/code&gt; (PEP 799, named Tachyon) reads the target process's memory from outside. It can &lt;code&gt;run&lt;/code&gt; a script, &lt;code&gt;attach&lt;/code&gt; to a PID for a time window, or &lt;code&gt;dump&lt;/code&gt; a single traceback-style snapshot of a live process. Output formats include pstats tables, collapsed stacks, flame graphs, Firefox Profiler and a live TUI. The &lt;a href="https://docs.python.org/3.15/library/profiling.sampling.html" rel="noopener noreferrer"&gt;docs&lt;/a&gt; list the constraints that matter in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same Python minor version for profiler and target. If either side is a pre-release, both must be the exact same version.&lt;/li&gt;
&lt;li&gt;Permissions. On Linux, root, &lt;code&gt;CAP_SYS_PTRACE&lt;/code&gt;, or a Yama &lt;code&gt;ptrace_scope&lt;/code&gt; of 0. The docs note the default &lt;code&gt;ptrace_scope&lt;/code&gt; of 1 restricts ptrace to parent processes.&lt;/li&gt;
&lt;li&gt;Non-blocking mode (the default) never stops the target, but "can occasionally produce incomplete or inconsistent stack traces". &lt;code&gt;--blocking&lt;/code&gt; pauses the target for each sample.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote the classic slow nightly job: a pandas row-wise &lt;code&gt;apply&lt;/code&gt; with a per-row Python function, looping forever, and pretended I did not know why it was slow.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  slow_job.py
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;A &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stuck&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; nightly job: a row-wise pandas apply that nobody profiled.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+spam&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enrich&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email_norm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalize_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_rng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200_000&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; User&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;+spam@Example.com &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&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="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;enrich&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The first thing you want during an incident is "what is it doing right now":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python -m profiling.sampling dump --blocking 440208
Stack dump for PID 440208, thread 440208 (main thread, has GIL; most recent call last):
  File "slow_job.py", line 31, in &amp;lt;module&amp;gt;
    main()
  File "slow_job.py", line 26, in main
    enrich(df.copy())
  File "slow_job.py", line 14, in enrich
    df["email_norm"] = df.apply(normalize_email, axis=1)
  ...
  File "slow_job.py", line 10, in normalize_email
    return row["email"].strip().lower().replace("+spam", "")
  File ".venv/lib/python3.15/site-packages/pandas/core/series.py", line 959, in Series.__getitem__
    return self._get_value(key)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No restart, no code change, and the culprit is on screen. On this kernel there is no Yama module (&lt;code&gt;/proc/sys/kernel/yama/ptrace_scope&lt;/code&gt; does not exist), so attaching as the same unprivileged user just worked. On a host with the default &lt;code&gt;ptrace_scope&lt;/code&gt; of 1, plan for root or &lt;code&gt;CAP_SYS_PTRACE&lt;/code&gt; before the incident, not during it.&lt;/p&gt;

&lt;p&gt;Then the part I did not expect. Without &lt;code&gt;--blocking&lt;/code&gt;, &lt;code&gt;dump&lt;/code&gt; failed 8 times out of 20 with &lt;code&gt;RuntimeError: Failed to parse initial frame in chain&lt;/code&gt;. With &lt;code&gt;--blocking&lt;/code&gt; it succeeded 20 out of 20. For &lt;code&gt;attach&lt;/code&gt;, the profiler prints an &lt;code&gt;Error rate&lt;/code&gt; line, the percentage of samples it could not read. I measured it on a fixed workload (median of six &lt;code&gt;apply&lt;/code&gt; rounds per run, 1 kHz default rate, three repetitions):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Target round time&lt;/th&gt;
&lt;th&gt;Error rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No profiler&lt;/td&gt;
&lt;td&gt;1170 / 1150 / 1186 ms&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;attach&lt;/code&gt; (non-blocking, default)&lt;/td&gt;
&lt;td&gt;1179 / 1180 / 1150 ms&lt;/td&gt;
&lt;td&gt;67.8% / 62.0% / 63.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;attach --blocking&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1606 / 1760 / 1657 ms&lt;/td&gt;
&lt;td&gt;1.5% / 1.6% / 1.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both halves of the docs are true at once. Non-blocking mode had no overhead I could measure on the target. And on this VM, 62 to 68% of its samples were unreadable in this test (46 to 50% in two earlier 5-second runs against slow_job.py, 38% against a pure-Python recursive function). Blocking mode was clean, and it made the job 37 to 53% slower while attached.&lt;/p&gt;

&lt;p&gt;Interpretation: treat a non-blocking profile like a survey with a response rate. If 60% of samples failed, the remaining 40% may still point at the right function (here the row-wise &lt;code&gt;apply&lt;/code&gt; dominated either way), but check the error rate before you trust percentages. Use &lt;code&gt;dump --blocking&lt;/code&gt; for a one-shot snapshot, where a single pause costs nothing, and keep &lt;code&gt;--blocking&lt;/code&gt; profiles short on anything user-facing.&lt;/p&gt;

&lt;p&gt;And the version rule bit me right away. A job started on the 3.15.0rc3 that the older uv had installed could not be inspected from 3.15.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Can't attach to a pre-release Python interpreter from a process running a different Python version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From rc3 to rc3 it worked. Pin the profiler to the job image's exact interpreter, which in practice means running it from inside that image.&lt;/p&gt;

&lt;p&gt;The quieter observability change is &lt;a href="https://peps.python.org/pep-0831/" rel="noopener noreferrer"&gt;PEP 831&lt;/a&gt;: CPython is now built with frame pointers by default. What's New says this makes native stack unwinding "faster and more reliable" for system profilers, debuggers, crash analysis tools and eBPF-based tools. The uv build I used has &lt;code&gt;-fno-omit-frame-pointer&lt;/code&gt; in its &lt;code&gt;CFLAGS&lt;/code&gt;. The catch, in the release notes' words: "A single native component built without frame pointers can break stack unwinding for the whole Python process." Your own Rust or C extensions need the same flags if you want mixed native and Python profiles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Runbook: profiling a stuck job
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;python -m profiling.sampling dump --blocking PID&lt;/code&gt; first. One snapshot, one short pause.&lt;/li&gt;
&lt;li&gt;Then &lt;code&gt;python -m profiling.sampling attach -d 30 --flamegraph -o job.html PID&lt;/code&gt;, and read &lt;code&gt;Error rate&lt;/code&gt; before the flame graph.&lt;/li&gt;
&lt;li&gt;Run the profiler with the job's exact interpreter. A pre-release on either side means exact versions only.&lt;/li&gt;
&lt;li&gt;Decide now who is allowed &lt;code&gt;CAP_SYS_PTRACE&lt;/code&gt; on worker hosts, and where.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;.pth&lt;/code&gt; import lines become &lt;code&gt;.start&lt;/code&gt; files: auditable, not safe
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The wrong assumption:&lt;/strong&gt; "a package can only run code when I import it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mechanics:&lt;/strong&gt; at startup the &lt;code&gt;site&lt;/code&gt; module reads every &lt;code&gt;.pth&lt;/code&gt; file in site-packages. Lines are paths to add to &lt;code&gt;sys.path&lt;/code&gt;, except lines that start with &lt;code&gt;import&lt;/code&gt;, which are executed with &lt;code&gt;exec()&lt;/code&gt;. Every Python process in that environment runs them: &lt;code&gt;python -c pass&lt;/code&gt;, pip, pytest, the notebook kernel, the scheduler. PEP 829 calls this "a broad attack surface".&lt;/p&gt;

&lt;p&gt;This is not theoretical. The advisory for the compromised litellm 1.82.7 and 1.82.8 releases (&lt;a href="https://osv.dev/vulnerability/PYSEC-2026-2" rel="noopener noreferrer"&gt;PYSEC-2026-2&lt;/a&gt;, published March 24, 2026) describes credential-harvesting malware, and its references point to a &lt;code&gt;litellm_init.pth&lt;/code&gt; file inside the 1.82.8 package.&lt;/p&gt;

&lt;p&gt;And it is not only malware. &lt;code&gt;setuptools&lt;/code&gt; 84.0.0, installed fresh on the VM, still ships &lt;code&gt;distutils-precedence.pth&lt;/code&gt;, whose single line starts with &lt;code&gt;import os; ...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What &lt;a href="https://peps.python.org/pep-0829/" rel="noopener noreferrer"&gt;PEP 829&lt;/a&gt; and the &lt;a href="https://docs.python.org/3.15/library/site.html" rel="noopener noreferrer"&gt;site docs&lt;/a&gt; change in 3.15:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A new &lt;code&gt;name.start&lt;/code&gt; file lists entry points as &lt;code&gt;pkg.mod:callable&lt;/code&gt;. They run after all &lt;code&gt;.pth&lt;/code&gt; path lines are applied.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;name.start&lt;/code&gt; exists next to &lt;code&gt;name.pth&lt;/code&gt;, the &lt;code&gt;import&lt;/code&gt; lines in that &lt;code&gt;.pth&lt;/code&gt; are ignored.&lt;/li&gt;
&lt;li&gt;Without a matching &lt;code&gt;.start&lt;/code&gt;, &lt;code&gt;import&lt;/code&gt; lines still run in 3.15, and the deprecation message appears only with &lt;code&gt;-v&lt;/code&gt;. The docs say they will be "silently ignored in Python 3.18 and 3.19", with a warning in 3.20.&lt;/li&gt;
&lt;li&gt;Errors on one line no longer abort the rest of the file. A failing entry point prints a traceback and startup continues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the VM, with a demo &lt;code&gt;zz_demo.pth&lt;/code&gt; containing a path line and an &lt;code&gt;import&lt;/code&gt; line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python -c "import sys; print('/tmp/extra_path_dir' in sys.path)"
[zz_demo.pth] import line executed at startup
True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;python -v&lt;/code&gt; shows the new deprecation notice for both the demo file and setuptools' file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import lines in .../site-packages/distutils-precedence.pth are deprecated, use entry points in a .../site-packages/distutils-precedence.start file instead.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After adding &lt;code&gt;zz_demo.start&lt;/code&gt; with one line, &lt;code&gt;zz_demo_boot:init&lt;/code&gt;, the &lt;code&gt;import&lt;/code&gt; line is suppressed and the entry point runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import lines in .../zz_demo.pth are suppressed due to matching .../zz_demo.start file.
Executing entry point: zz_demo_boot:init from .../zz_demo.start
[zz_demo.start] entry point zz_demo_boot:init called
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the part to be precise about. A &lt;code&gt;.start&lt;/code&gt; file still runs code at every interpreter start. A malicious wheel can ship one just as easily as a &lt;code&gt;.pth&lt;/code&gt;. The PEP says so: "The overall pre-start code execution attack surface is not eliminated by this PEP." What 3.15 gives you is a narrower, auditable shape: startup code is a named callable in an importable module, it goes through the import system so audit hooks (PEP 578) can see it, and "you can tell by listing the files in the site-dir, exactly where arbitrary code execution occurs."&lt;/p&gt;

&lt;p&gt;That listing is the useful part, and you can do it today on any Python version:&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  startup_audit.py (runs on Python 3.9+)
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;List everything that can run code at interpreter start in this environment.

Run it with the interpreter you want to audit:
    .venv/bin/python startup_audit.py
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;dirs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromkeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getsitepackages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getusersitepackages&lt;/span&gt;&lt;span class="p"&gt;()]))&lt;/span&gt;
&lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dirs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_dir&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.pth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8-sig&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;shadow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_suffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skipped on 3.15+ (matching .start), runs on older Pythons&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;shadow&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RUNS on every start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  [&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;    &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;110&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  [.start entry points]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8-sig&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;    &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitecustomize.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usercustomize.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  [runs on every start]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; startup code hook(s) found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Output in the demo environment (with setuptools installed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.../site-packages/distutils-precedence.pth  [RUNS on every start]
    import os; var = 'SETUPTOOLS_USE_DISTUTILS'; enabled = os.environ.get(var, 'local') == 'local'; enabled and __
.../site-packages/zz_demo.pth  [skipped on 3.15+ (matching .start), runs on older Pythons]
    import sys; sys.stderr.write("[zz_demo.pth] import line executed at startup\n")
.../site-packages/zz_demo.start  [.start entry points]
    zz_demo_boot:init
3.15.0: 3 startup code hook(s) found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most &lt;code&gt;.pth&lt;/code&gt; files are harmless. An editable install of a src-layout package with setuptools wrote a path-only &lt;code&gt;.pth&lt;/code&gt; on the VM, with no code at all. The ones that execute code are few enough to put on an allowlist.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;python -S&lt;/code&gt; disables both mechanisms (I checked), but it also skips site-packages entirely, so it is a debugging tool, not a production setting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Runbook: startup code
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Run the audit in your image build and fail the build on any startup hook that is not on an allowlist.&lt;/li&gt;
&lt;li&gt;If you maintain a package with a &lt;code&gt;.pth&lt;/code&gt; &lt;code&gt;import&lt;/code&gt; line, move the code into a zero-argument function and add a matching &lt;code&gt;.start&lt;/code&gt; file. Keep the &lt;code&gt;.pth&lt;/code&gt; line as &lt;code&gt;import pkg.mod; pkg.mod.callable()&lt;/code&gt; while you still support 3.14.&lt;/li&gt;
&lt;li&gt;Treat a new &lt;code&gt;.start&lt;/code&gt; file in a dependency diff like a new &lt;code&gt;postinstall&lt;/code&gt; script in npm: a reason to read the release.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What to do this week
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If you run...&lt;/th&gt;
&lt;th&gt;Do this&lt;/th&gt;
&lt;th&gt;Skip this&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Python CLIs, CI tooling, notebook kernels&lt;/td&gt;
&lt;td&gt;Measure with &lt;code&gt;-X importtime&lt;/code&gt;, add &lt;code&gt;lazy&lt;/code&gt; to your entry point's heavy imports&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PYTHON_LAZY_IMPORTS=all&lt;/code&gt; in images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long batch jobs on VMs or Kubernetes&lt;/td&gt;
&lt;td&gt;Install the exact job interpreter in the image, rehearse &lt;code&gt;dump --blocking&lt;/code&gt; and &lt;code&gt;attach&lt;/code&gt; on a staging job, sort out &lt;code&gt;CAP_SYS_PTRACE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Trusting a profile without reading &lt;code&gt;Error rate&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anything that installs from PyPI&lt;/td&gt;
&lt;td&gt;Add the startup audit to the image build, review new &lt;code&gt;.pth&lt;/code&gt; and &lt;code&gt;.start&lt;/code&gt; files in dependency updates&lt;/td&gt;
&lt;td&gt;Assuming &lt;code&gt;.start&lt;/code&gt; files make startup code safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python-heavy loops or row-wise UDFs&lt;/td&gt;
&lt;td&gt;Try &lt;code&gt;PYTHON_JIT=1&lt;/code&gt; in a benchmark, expect single digits&lt;/td&gt;
&lt;td&gt;Counting on the JIT to fix a row-wise &lt;code&gt;apply&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The JIT will show up in your benchmarks as a few percent. Lazy imports, Tachyon and &lt;code&gt;.start&lt;/code&gt; files will show up in your incident timeline: the CLI that starts in 20 ms instead of 350, the stuck job you can inspect without a restart, and the list of files that run code before your &lt;code&gt;main()&lt;/code&gt; does. Upgrade for those, and measure the rest.&lt;/p&gt;

</description>
      <category>python</category>
      <category>dataengineering</category>
      <category>performance</category>
      <category>security</category>
    </item>
    <item>
      <title>Kafka Just Learned to Be a Queue. Postgres Has Been One All Along.</title>
      <dc:creator>Firat Celik</dc:creator>
      <pubDate>Mon, 05 Oct 2026 07:17:38 +0000</pubDate>
      <link>https://dev.to/firfircelik/kafka-just-learned-to-be-a-queue-postgres-has-been-one-all-along-2i9g</link>
      <guid>https://dev.to/firfircelik/kafka-just-learned-to-be-a-queue-postgres-has-been-one-all-along-2i9g</guid>
      <description>&lt;p&gt;On February 17, 2026, Apache Kafka 4.2 shipped with this line in the &lt;a href="https://kafka.apache.org/blog/2026/02/17/apache-kafka-4.2.0-release-announcement/" rel="noopener noreferrer"&gt;release announcement&lt;/a&gt;: "Kafka Queues (Share Groups) is now production-ready."&lt;/p&gt;

&lt;p&gt;That sentence quietly reopens one of backend engineering's oldest arguments. For years, "should my job queue be Postgres or Kafka?" had a lazy answer from both camps. The Kafka camp said Kafka scales. The Postgres camp said Kafka is not a queue: your consumer count is capped by your partition count, there is no per-message ack, and one poison message blocks everything behind it in its partition.&lt;/p&gt;

&lt;p&gt;The second argument just lost most of its teeth. So this post rebuilds the comparison from primary sources and two small demos you can run in a few minutes: a Postgres &lt;code&gt;SKIP LOCKED&lt;/code&gt; queue in Python, and a Kafka share group in Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thesis
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If a job is born inside a database transaction, Postgres is still the better queue in 2026.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kafka share groups are real, well designed, and production-ready on the broker. But they answer a different question: "I already run Kafka, and now I also need queue semantics." They do not answer "I need a queue." Those two questions have different winners, and a team that does not already run Kafka would be adopting a whole platform to get one feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The antithesis, at full strength
&lt;/h2&gt;

&lt;p&gt;Here is the best case against that thesis. It is a good one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-932%3A+Queues+for+Kafka" rel="noopener noreferrer"&gt;KIP-932&lt;/a&gt; adds a new kind of group, the share group, and with it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consumers beyond partition count.&lt;/strong&gt; "The number of consumers in a share group can exceed the number of partitions in a topic." The old trick of over-partitioning just to get parallelism goes away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-record acknowledgement.&lt;/strong&gt; A consumer can accept, release (retry), or reject (unprocessable) each record. Kafka 4.2 added a fourth option, RENEW, to extend the lock for slow work (&lt;a href="https://kafka.apache.org/blog/2026/02/17/apache-kafka-4.2.0-release-announcement/" rel="noopener noreferrer"&gt;KIP-1222&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery counting with a poison-message limit.&lt;/strong&gt; After the configured number of attempts (default 5, per the &lt;a href="https://kafka.apache.org/43/configuration/broker-configs/" rel="noopener noreferrer"&gt;4.3 broker configs&lt;/a&gt;), a record is archived instead of redelivered forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No queue depth limit and replay.&lt;/strong&gt; In the KIP's own words: "queues done in a Kafka way, with no maximum queue depth and the ability to reset to a specific time for point-in-time recovery."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Many readers, one log.&lt;/strong&gt; Several share groups can consume the same topic independently, next to ordinary consumer groups.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the Postgres side has a well-known way to rot: MVCC. In the demo below, a single idle session holding an old snapshot made the median claim latency go from &lt;strong&gt;about 0.27 ms to about 15 ms&lt;/strong&gt; on the same queue. That is not a theoretical concern. It is one forgotten &lt;code&gt;BEGIN&lt;/code&gt; away.&lt;/p&gt;

&lt;p&gt;So the antithesis is: Kafka now does queues properly, scales past one box, keeps history, and does not degrade because someone left a psql session open. The answer to that lives in the failure modes on both sides.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually shipped (facts, as of October 5, 2026)
&lt;/h2&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;Kafka share groups (KIP-932)&lt;/th&gt;
&lt;th&gt;Postgres &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; queue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit of work&lt;/td&gt;
&lt;td&gt;Record in a topic partition&lt;/td&gt;
&lt;td&gt;Row in a table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claim&lt;/td&gt;
&lt;td&gt;Broker-side acquisition lock, default 30 s (&lt;code&gt;share.record.lock.duration.ms&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Row lock held by your transaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ack&lt;/td&gt;
&lt;td&gt;ACCEPT / RELEASE / REJECT / RENEW&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;COMMIT&lt;/code&gt; (and delete or update the row)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crash behavior&lt;/td&gt;
&lt;td&gt;Lock expires (or the share session closes), record becomes available again&lt;/td&gt;
&lt;td&gt;Transaction aborts, lock is released, row is visible again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery guarantee&lt;/td&gt;
&lt;td&gt;At-least-once (&lt;a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-932%3A+Queues+for+Kafka" rel="noopener noreferrer"&gt;KIP-932&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;At-least-once for external side effects; atomic with any writes in the same database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ordering&lt;/td&gt;
&lt;td&gt;Not guaranteed across batches, especially on redelivery&lt;/td&gt;
&lt;td&gt;Whatever your &lt;code&gt;ORDER BY&lt;/code&gt; gives you, best effort under concurrency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poison messages&lt;/td&gt;
&lt;td&gt;Delivery count limit, default 5&lt;/td&gt;
&lt;td&gt;You write it (an &lt;code&gt;attempts&lt;/code&gt; column)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dead-letter queue&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://cwiki.apache.org/confluence/spaces/KAFKA/pages/429064575/Release+Plan+4.4.0" rel="noopener noreferrer"&gt;KIP-1191&lt;/a&gt; listed as complete for 4.4.0, which is not released yet&lt;/td&gt;
&lt;td&gt;You write it (a table)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Client support matters more than broker support, because your code talks to a client:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Java:&lt;/strong&gt; &lt;code&gt;KafkaShareConsumer&lt;/code&gt; is the reference client (&lt;a href="https://kafka.apache.org/42/javadoc/org/apache/kafka/clients/consumer/KafkaShareConsumer.html" rel="noopener noreferrer"&gt;javadoc&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go:&lt;/strong&gt; franz-go added "full support for KIP-932 share groups" in v1.21.0 (&lt;a href="https://github.com/twmb/franz-go/blob/master/CHANGELOG.md" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;). The demo below uses v1.22.1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python and C:&lt;/strong&gt; librdkafka and confluent-kafka-python 2.15.0 (June 30, 2026) ship the share consumer as &lt;strong&gt;Preview&lt;/strong&gt;, with an explicit note that it "should not be used in production environments" (&lt;a href="https://github.com/confluentinc/confluent-kafka-python/releases/tag/v2.15.0" rel="noopener noreferrer"&gt;confluent-kafka-python v2.15.0&lt;/a&gt;, &lt;a href="https://github.com/confluentinc/librdkafka/releases/tag/v2.15.0" rel="noopener noreferrer"&gt;librdkafka v2.15.0&lt;/a&gt;). Confluent's own GA post says non-Java client support is "targeted for the second half of 2026" (&lt;a href="https://www.confluent.io/blog/kafka-queue-semantics-share-consumer-ga/" rel="noopener noreferrer"&gt;Confluent blog&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your workers are Python on the mainstream Confluent client, Kafka queues are explicitly not for production today. That alone settles the question for a lot of data teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Postgres queue is one SQL statement
&lt;/h2&gt;

&lt;p&gt;Here is the entire claim step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
    &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="k"&gt;next&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it inside a transaction, do the work, write the result, &lt;code&gt;COMMIT&lt;/code&gt;. The commit is the ack. If the worker dies before committing, Postgres rolls the transaction back, the rows were never deleted, and another worker picks them up. That is the same contract as a share group's acquisition lock, except the "lock timeout" is "your connection died."&lt;/p&gt;

&lt;p&gt;The Postgres docs are blunt about what &lt;code&gt;SKIP LOCKED&lt;/code&gt; is for: "Skipping locked rows provides an inconsistent view of the data, so this is not suitable for general purpose work, but can be used to avoid lock contention with multiple consumers accessing a queue-like table" (&lt;a href="https://www.postgresql.org/docs/current/sql-select.html" rel="noopener noreferrer"&gt;SELECT docs&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo A: does SKIP LOCKED actually matter?
&lt;/h3&gt;

&lt;p&gt;Eight worker threads drain 20,000 jobs in batches of 50. Each worker writes a row into a &lt;code&gt;done&lt;/code&gt; table in the same transaction as the dequeue. The script asserts zero duplicates and zero lost jobs after every run. Each script execution does three alternating runs per cell and reports the median; the table shows the range of those medians across four executions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Simulated work per batch (lock held)&lt;/th&gt;
&lt;th&gt;&lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;plain &lt;code&gt;FOR UPDATE&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 ms&lt;/td&gt;
&lt;td&gt;14,400 to 17,600 jobs/s&lt;/td&gt;
&lt;td&gt;13,200 to 32,800 jobs/s (about 32,000 in 3 of 4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 ms&lt;/td&gt;
&lt;td&gt;14,000 to 17,600 jobs/s&lt;/td&gt;
&lt;td&gt;7,100 to 7,200 jobs/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Duplicates: 0. Lost: 0. In every run, for both variants.&lt;/p&gt;

&lt;p&gt;The first row is the surprise: with near-zero lock hold time, plain &lt;code&gt;FOR UPDATE&lt;/code&gt; was usually about twice as fast. A plausible explanation (interpretation, not measured): each &lt;code&gt;SKIP LOCKED&lt;/code&gt; claim steps over the rows the other seven workers hold, while a blocked worker waits very briefly and gets its rows. The second row is the one that matters in production. Once workers hold the claim while doing real work, the blocking version collapses to about 7,000 jobs/s because everyone queues behind the head of the table, and &lt;code&gt;SKIP LOCKED&lt;/code&gt; comes out 2 to 2.5 times ahead. Both variants lost nothing. &lt;code&gt;SKIP LOCKED&lt;/code&gt; is about throughput under real work, not correctness.&lt;/p&gt;

&lt;p&gt;One subtle thing the script surfaced: with &lt;code&gt;SKIP LOCKED&lt;/code&gt;, a worker can get zero rows while jobs still exist, because everything left is claimed by someone else. It happened between 2 and 36 times per run. Treat an empty claim as "back off and poll again," never as "the queue is empty, exit."&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo B: kill a worker mid-batch
&lt;/h3&gt;

&lt;p&gt;A session claims 10 of 100 jobs and never commits. While it holds them, other sessions can claim 90. After &lt;code&gt;pg_terminate_backend()&lt;/code&gt; on that session (the database-side equivalent of &lt;code&gt;kill -9&lt;/code&gt; on the worker), all 100 are claimable again. No lease column, no reaper job, as long as jobs are short (more on that below).&lt;/p&gt;

&lt;h3&gt;
  
  
  The real advantage: the job and the data commit together
&lt;/h3&gt;

&lt;p&gt;This is the part Kafka cannot match without extra machinery. If your API inserts a user and enqueues "send welcome email" in the same transaction, the job exists if and only if the user exists. &lt;a href="https://riverqueue.com/" rel="noopener noreferrer"&gt;River&lt;/a&gt;, a Go job queue backed by Postgres, makes this its core argument (&lt;a href="https://riverqueue.com/docs/transactional-enqueueing" rel="noopener noreferrer"&gt;River docs&lt;/a&gt;): with a separate queue store you either enqueue after commit and lose jobs on a crash, or enqueue before commit and have workers race a row they cannot see yet.&lt;/p&gt;

&lt;p&gt;The mainstream already voted on this. Solid Queue "is configured by default in new Rails 8 applications" and "leverages the &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; clause" (&lt;a href="https://github.com/rails/solid_queue" rel="noopener noreferrer"&gt;Solid Queue README&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Postgres queue dies: one old snapshot
&lt;/h2&gt;

&lt;p&gt;A queue table is the worst-case MVCC workload: every row is inserted once and deleted once. Postgres does not remove a deleted row immediately; the &lt;a href="https://www.postgresql.org/docs/current/routine-vacuuming.html" rel="noopener noreferrer"&gt;routine vacuuming docs&lt;/a&gt; explain that "the row version must not be deleted while it is still potentially visible to other transactions." If anything holds an old snapshot, vacuum cannot clean, and the dead rows pile up right where your claim query starts scanning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo C: churn 200,000 jobs, then measure
&lt;/h3&gt;

&lt;p&gt;The script pushes 200,000 jobs through the queue, enqueues 5,000 fresh ones, runs &lt;code&gt;VACUUM (VERBOSE)&lt;/code&gt;, and times 200 claims of 10 jobs each. Then it repeats the whole thing with one extra session sitting &lt;code&gt;idle in transaction&lt;/code&gt; in &lt;code&gt;REPEATABLE READ&lt;/code&gt;, which is exactly what a forgotten report query or a paused migration looks like.&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;Median claim latency&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;VACUUM VERBOSE&lt;/code&gt; says&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No old snapshot&lt;/td&gt;
&lt;td&gt;0.269 ms / 0.272 ms&lt;/td&gt;
&lt;td&gt;40,293 / 37,680 removed, 0 dead but not yet removable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One idle &lt;code&gt;REPEATABLE READ&lt;/code&gt; session&lt;/td&gt;
&lt;td&gt;15.754 ms / 14.961 ms&lt;/td&gt;
&lt;td&gt;0 removed, 200,000 dead but not yet removable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(Two executions of the script, shown as first / second. In the no-snapshot case autovacuum had already cleaned most of the dead rows before the manual &lt;code&gt;VACUUM&lt;/code&gt; ran.)&lt;/p&gt;

&lt;p&gt;Same table, same query, same hardware. Roughly 55 to 58 times slower per claim, and the dead rows keep accumulating with every job processed while that session lives. Nothing errors. Your queue just gets slower until someone notices.&lt;/p&gt;

&lt;p&gt;Things that hold old snapshots in real systems, all documented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Long-running or idle-in-transaction sessions (&lt;a href="https://www.postgresql.org/docs/current/routine-vacuuming.html" rel="noopener noreferrer"&gt;vacuum docs&lt;/a&gt; tell you to find them via &lt;code&gt;pg_stat_activity&lt;/code&gt; and &lt;code&gt;backend_xmin&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Old replication slots (same page).&lt;/li&gt;
&lt;li&gt;A hot standby with &lt;code&gt;hot_standby_feedback&lt;/code&gt; on, which "can cause database bloat on the primary for some workloads" (&lt;a href="https://www.postgresql.org/docs/current/runtime-config-replication.html" rel="noopener noreferrer"&gt;replication config docs&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mitigations that actually work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set &lt;code&gt;idle_in_transaction_session_timeout&lt;/code&gt; for application and human roles, and &lt;code&gt;transaction_timeout&lt;/code&gt; (added in &lt;a href="https://www.postgresql.org/docs/17/release-17.html" rel="noopener noreferrer"&gt;PostgreSQL 17&lt;/a&gt;) where it is safe. Both are documented in &lt;a href="https://www.postgresql.org/docs/current/runtime-config-client.html" rel="noopener noreferrer"&gt;client connection defaults&lt;/a&gt;, including the warning to be careful with connection poolers.&lt;/li&gt;
&lt;li&gt;Alert on "dead but not yet removable" and on the age of the oldest &lt;code&gt;backend_xmin&lt;/code&gt;, not just on queue depth.&lt;/li&gt;
&lt;li&gt;Keep analytics off the primary. An extra test on the same box: an old snapshot held in a &lt;em&gt;different database&lt;/em&gt; on the same cluster did not block cleanup of the queue table (50,000 removed), while the same snapshot in the &lt;em&gt;same database&lt;/em&gt; blocked all of it (50,000 dead but not yet removable). That is a real escape hatch, but it has a price: a queue in its own database can no longer commit atomically with your application tables, which was the main reason to use Postgres in the first place.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The trap: your worker can be the long transaction
&lt;/h3&gt;

&lt;p&gt;The claim pattern above holds the transaction open while the job runs. For a 50 ms job that is fine. For a 5 minute job, the worker itself is the old transaction. A quick check on the same box: a worker sitting &lt;code&gt;idle in transaction&lt;/code&gt; after its &lt;code&gt;DELETE ... RETURNING&lt;/code&gt; (plain &lt;code&gt;READ COMMITTED&lt;/code&gt;, nothing exotic) left 50,000 freshly deleted rows in another table of the same database "dead but not yet removable." So long jobs need a lease instead: claim and commit in a short transaction, work outside any transaction, then ack only if you still own the lease.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- claim: short transaction, commit right away&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;locked_until&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'5 minutes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;lease_token&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;attempts&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;locked_until&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;locked_until&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lease_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- ack: only the current lease holder can delete the job&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;lease_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tested on the box: when a lease expired and a second worker re-claimed the job, the first worker's late ack deleted 0 rows and the second worker's ack deleted 1. Notice what you just built: an acquisition lock with a timeout and a delivery count. That is a share group, in SQL. You also gave up the "job commits with the data" property for the work itself, so this path needs idempotent jobs, exactly like Kafka.&lt;/p&gt;

&lt;h3&gt;
  
  
  A note on LISTEN/NOTIFY
&lt;/h3&gt;

&lt;p&gt;If you use &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt; to wake workers, know the documented edges (&lt;a href="https://www.postgresql.org/docs/current/sql-notify.html" rel="noopener noreferrer"&gt;NOTIFY docs&lt;/a&gt;): notifications are delivered only when the sending transaction commits, payloads must be shorter than 8000 bytes by default, and the notification queue (8 GB in a standard install) cannot be cleaned while a listening session sits in a long transaction. If that queue fills, "transactions calling NOTIFY will fail at commit." Use NOTIFY as a doorbell, never as the queue itself. PostgreSQL 19, with GA scheduled for October 29, 2026 (&lt;a href="https://www.postgresql.org/message-id/915517c4-d0ee-49f9-bbeb-c683ef2b9d3f%40postgresql.org" rel="noopener noreferrer"&gt;release dates&lt;/a&gt;), lists an improvement to "only wake up backends that are listening to specified notifications" in its &lt;a href="https://www.postgresql.org/docs/19/release-19.html" rel="noopener noreferrer"&gt;draft release notes&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Kafka share groups bite
&lt;/h2&gt;

&lt;p&gt;These are less about slow decay and more about semantics you have to design around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The lock timeout is a duplicate-work timer.&lt;/strong&gt; The default acquisition lock is 30 seconds. If your job takes 45, the broker hands the record to another consumer while the first one is still working. Use RENEW for long jobs, and make processing idempotent anyway. The &lt;a href="https://pkg.go.dev/github.com/twmb/franz-go/pkg/kgo" rel="noopener noreferrer"&gt;franz-go &lt;code&gt;kgo&lt;/code&gt; docs&lt;/a&gt; put it plainly: "Share group consumers are at-least-once regardless: any TCP hiccup can cause re-delivery, so user processing must be idempotent."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Ordering is gone.&lt;/strong&gt; KIP-932: "The records in a share-partition can be delivered out of order to a consumer, in particular when redeliveries occur." If you need per-key ordering, you want a classic consumer group, not a share group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A new share group starts at the end.&lt;/strong&gt; &lt;code&gt;share.auto.offset.reset&lt;/code&gt; defaults to &lt;code&gt;latest&lt;/code&gt; (&lt;a href="https://kafka.apache.org/43/configuration/group-configs/" rel="noopener noreferrer"&gt;group configs&lt;/a&gt;). Create a share group on a topic that already has a backlog and, unless you set &lt;code&gt;earliest&lt;/code&gt;, your workers will skip all of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Size-based retention can delete work you have not done.&lt;/strong&gt; From the KIP: retention based on size "does potentially silently remove records that were eligible for delivery." A Postgres table does not delete your unprocessed jobs because the disk got full.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The unit of sharing is the record batch.&lt;/strong&gt; The KIP says it directly: "the natural unit of sharing is the record batch." In the Go demo below, four consumers share one partition. With default fetch settings, one consumer got 949 of 1,000 jobs and two got nothing. With &lt;code&gt;ShareMaxRecordsStrict()&lt;/code&gt; (the client side of the strict mode added by KIP-1206 in 4.2), the split was 246 / 251 / 252 / 251. Caveat: this ran against kfake, franz-go's in-process fake broker, not a real cluster, so treat the exact split as illustrative. The behavior matches what the KIP describes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. No atomicity with your database.&lt;/strong&gt; Writing a row to Postgres and producing to Kafka are two systems. The standard fix is the transactional outbox: write the event to an outbox table in the same transaction, then let CDC (for example the &lt;a href="https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html" rel="noopener noreferrer"&gt;Debezium outbox event router&lt;/a&gt;) publish it. Notice what that means: the reliable way to feed a Kafka queue from an OLTP app starts with a Postgres table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. In-flight work is capped per partition.&lt;/strong&gt; &lt;code&gt;group.share.partition.max.record.locks&lt;/code&gt; defaults to 2000 per share-partition (&lt;a href="https://kafka.apache.org/43/configuration/broker-configs/" rel="noopener noreferrer"&gt;4.3 broker configs&lt;/a&gt;). Slow consumers holding locks can stall fetching on that partition until locks are acked or expire. And the delivery count "cannot be relied upon to be precise in all situations" (KIP-932), so do not build billing logic on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Go side: a share group on one partition
&lt;/h3&gt;

&lt;p&gt;The core loop, using franz-go v1.22.1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeedBrokers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;addrs&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConsumeTopics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"jobs"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShareGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"email-workers"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShareMaxRecords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;25&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShareMaxRecordsStrict&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c"&gt;// without this one member may get a whole record batch&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pollCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fetches&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PollFetches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pollCtx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;fetches&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EachRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AckRelease&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// transient: try again, delivery count goes up&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AckAccept&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;In the full demo, every job whose number ends in 0 is released on its first delivery to simulate a transient failure. Three runs in a row: 1,000 of 1,000 jobs accepted, 100 on a second delivery, 0 accepted twice, four consumers on one partition. The headline feature works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision rule
&lt;/h2&gt;

&lt;p&gt;Put it on one line first:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tasks that belong to a transaction go in the database that owns the transaction. Facts that many systems read go in a log. Share groups let the log also hand out tasks, which is great once you already have the log.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then the checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose a Postgres queue when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The job is created in the same transaction as the business data it depends on.&lt;/li&gt;
&lt;li&gt;Your workers are in Python, or any language without a production share consumer yet.&lt;/li&gt;
&lt;li&gt;Your measured peak fits your Postgres with your payloads. Demo A did 14,000 to 17,600 tiny jobs/s with &lt;code&gt;SKIP LOCKED&lt;/code&gt; on 8 vCPUs; River claims "tens of thousands of jobs per second." Measure, do not inherit someone's number.&lt;/li&gt;
&lt;li&gt;Jobs are short enough to run inside the claim transaction, or you use leases for the long ones.&lt;/li&gt;
&lt;li&gt;You can enforce &lt;code&gt;idle_in_transaction_session_timeout&lt;/code&gt;, keep analytics off the primary, and alert on dead-but-not-removable rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Choose Kafka share groups when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kafka is already the system of record for these events, and the queue is one more reader.&lt;/li&gt;
&lt;li&gt;Several independent consumers need the same stream, or you need replay and reset-to-timestamp.&lt;/li&gt;
&lt;li&gt;Your workers are on Java or Go (franz-go), processing is idempotent, and ordering does not matter.&lt;/li&gt;
&lt;li&gt;You are fine designing around lock timeouts, retention, and &lt;code&gt;share.auto.offset.reset&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use both when&lt;/strong&gt; the job starts in your database but the work fans out to other teams: outbox table in Postgres, CDC into Kafka, share groups downstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Red flags that you picked wrong:&lt;/strong&gt; you are adding an outbox just to feed a Kafka queue that only one service reads (you wanted Postgres), or your Postgres queue needs a second database to stay fast (you are halfway to wanting a log).&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it yourself
&lt;/h2&gt;

&lt;p&gt;Hardware and versions for every number above: an 8 vCPU, 15 GB RAM Linux VM (kernel 6.12, Debian 13), PostgreSQL 17.11 with default settings except &lt;code&gt;max_connections=200&lt;/code&gt;, client and server on the same machine over a Unix socket, Python 3.13.5 with psycopg 3.3.6, Go 1.26.0 with franz-go v1.22.1 and kfake. These are toy payloads on one box. They show mechanisms and ratios, not capacity for your system.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  pg_queue_demo.py (Python, needs a throwaway Postgres)
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Postgres-as-a-queue demo: SKIP LOCKED vs blocking FOR UPDATE, crash recovery, and the MVCC failure mode.
Needs a THROWAWAY Postgres (it drops tables named jobs and done).
  pip install &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;psycopg[binary]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
  DSN=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host=/tmp port=54329 user=postgres dbname=postgres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; python pg_queue_demo.py
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;

&lt;span class="n"&gt;DSN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DSN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host=/tmp port=54329 user=postgres dbname=postgres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;CLAIM_SKIP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
WITH next AS (
    SELECT id FROM jobs
    ORDER BY id
    LIMIT %(n)s
    FOR UPDATE SKIP LOCKED
)
DELETE FROM jobs j USING next
WHERE j.id = next.id
RETURNING j.id, j.payload
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="n"&gt;CLAIM_BLOCKING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CLAIM_SKIP&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FOR UPDATE SKIP LOCKED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FOR UPDATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DROP TABLE IF EXISTS jobs, done&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CREATE TABLE jobs (id bigserial PRIMARY KEY, payload jsonb NOT NULL)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# 'done' stands in for a side effect that lives in the SAME database as the queue
&lt;/span&gt;    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CREATE TABLE done (job_id bigint NOT NULL, worker int NOT NULL)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO jobs (payload) SELECT jsonb_build_object(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, g) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FROM generate_series(1, %s) g&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;claim_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;empties&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;work_ms&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;work_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;work_ms&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# "work" done while holding the claim
&lt;/span&gt;                    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# result commits atomically with the dequeue
&lt;/span&gt;                        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executemany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO done (job_id, worker) VALUES (%s, %s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                        &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;wid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;wid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="c1"&gt;# An empty claim does not always mean an empty queue (see the blocking variant).
&lt;/span&gt;            &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT count(*) FROM jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="n"&gt;empties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;wid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_workers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;work_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;empties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;workers&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;claim_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;empties&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;work_ms&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
          &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;empties&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;experiment_throughput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repeats&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;work_ms&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;== A. &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; workers drain &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; jobs, batch=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
              &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;simulated work per batch=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;work_ms&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ms, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repeats&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; alternating runs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FOR UPDATE SKIP LOCKED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FOR UPDATE (blocking)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repeats&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FOR UPDATE SKIP LOCKED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CLAIM_SKIP&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                               &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FOR UPDATE (blocking)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CLAIM_BLOCKING&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
                &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;secs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;empties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_workers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;work_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distinct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT count(*), count(DISTINCT job_id) FROM done&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT count(*) FROM jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;distinct&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distinct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;secs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;empties&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; median &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;7.0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; jobs/s | &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                  &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;empty claims while work remained, per run: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                  &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicates 0, lost 0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;experiment_crash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;== B. a worker dies mid-transaction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;victim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CLAIM_SKIP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# claimed 10 jobs, never commits
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autocommit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT count(*) FROM (SELECT id FROM jobs FOR UPDATE SKIP LOCKED) s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;victim holds its batch: claimable by others = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&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="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT pg_terminate_backend(%s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backend_pid&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;victim session killed:  claimable by others = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&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="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;median_claim_ms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rounds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;lat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rounds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CLAIM_SKIP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;experiment_mvcc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;churn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;== C. churn &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;churn&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; jobs through the queue, then time claims, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;with and without one old snapshot held open&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;hold&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;holder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# stands in for a long report query, a stuck migration, or an idle-in-transaction session
&lt;/span&gt;            &lt;span class="n"&gt;holder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autocommit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;holder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BEGIN ISOLATION LEVEL REPEATABLE READ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;holder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# first statement takes the snapshot
&lt;/span&gt;            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autocommit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;c2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT state, backend_xmin FROM pg_stat_activity &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHERE pid = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;holder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backend_pid&lt;/span&gt;&lt;span class="p"&gt;,)).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   holder session: state=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt;, backend_xmin=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;xmin&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;churn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;run_workers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CLAIM_SKIP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# drain everything, leaving dead rows behind
&lt;/span&gt;        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="c1"&gt;# fresh work arrives behind the dead rows
&lt;/span&gt;        &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autocommit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_notice_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message_primary&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VACUUM (VERBOSE) jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# give vacuum every chance
&lt;/span&gt;        &lt;span class="n"&gt;tuples_line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tuples:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;med&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;median_claim_ms&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT pg_size_pretty(pg_total_relation_size(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;jobs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;))&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old snapshot held=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; median claim latency=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;med&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;8.3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ms  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
              &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobs table+index=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;   VACUUM VERBOSE: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tuples_line&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;holder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;holder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ROLLBACK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;holder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DSN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT version()&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&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="nf"&gt;experiment_throughput&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;experiment_crash&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;experiment_mvcc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  main.go (Go, franz-go share group against kfake)
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Share groups (KIP-932) in Go with franz-go: 4 consumers on ONE partition,&lt;/span&gt;
&lt;span class="c"&gt;// per-record ack, and a transient failure that gets redelivered.&lt;/span&gt;
&lt;span class="c"&gt;// Runs against kfake, franz-go's in-process fake broker, so no real cluster is needed.&lt;/span&gt;
&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"strconv"&lt;/span&gt;
    &lt;span class="s"&gt;"sync"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/twmb/franz-go/pkg/kfake"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/twmb/franz-go/pkg/kgo"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;topic&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"jobs"&lt;/span&gt;
    &lt;span class="n"&gt;group&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"email-workers"&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
    &lt;span class="n"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;kfake&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewCluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kfake&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NumBrokers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;kfake&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeedTopics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c"&gt;// ONE partition&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c"&gt;// Default is "latest": a brand-new share group would skip everything already in the topic.&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetGroupConfigs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"share.auto.offset.reset"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"earliest"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeedBrokers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAddrs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultProduceTopic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Itoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;mu&lt;/span&gt;          &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;
        &lt;span class="n"&gt;accepted&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="c"&gt;// job -&amp;gt; times accepted&lt;/span&gt;
        &lt;span class="n"&gt;perMember&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;redelivered&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeedBrokers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAddrs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConsumeTopics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShareGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShareMaxRecords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;25&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShareMaxRecordsStrict&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c"&gt;// without this the broker may hand one member a whole record batch&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
                &lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FlushAcks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="n"&gt;pollCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pollCancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;fetches&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;cl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PollFetches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pollCtx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;pollCancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;fetches&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EachRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Atoi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeliveryCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AckRelease&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// transient failure: give it back for another attempt&lt;/span&gt;
                        &lt;span class="k"&gt;return&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// pretend to send the email&lt;/span&gt;
                    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kgo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AckAccept&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
                    &lt;span class="n"&gt;perMember&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeliveryCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;redelivered&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&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="p"&gt;}(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;dups&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;dups&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"partitions=1 consumers=%d distinct jobs accepted=%d/%d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"accepted per consumer=%v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;perMember&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"accepted on a 2nd+ delivery (after RELEASE)=%d, accepted more than once=%d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redelivered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dups&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;&lt;/p&gt;

&lt;p&gt;For the Go demo: &lt;code&gt;go mod init sharedemo &amp;amp;&amp;amp; go get github.com/twmb/franz-go@v1.22.1 github.com/twmb/franz-go/pkg/kfake@v0.0.0-20260927204940-b5a45ccfdf7e &amp;amp;&amp;amp; go run .&lt;/code&gt; (the kfake module requires Go 1.26, which the &lt;code&gt;go&lt;/code&gt; command downloads automatically if your toolchain is older).&lt;/p&gt;

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

&lt;p&gt;Kafka 4.2 fixed the strongest technical objection to "Kafka as a queue." It did not change where your jobs are born. If they are born in a Postgres transaction, keep them there, and spend your effort on the one thing that kills Postgres queues: old snapshots. When the events already live in Kafka and many teams read them, share groups are now a legitimate queue, on Java and Go today, and on Python once the client leaves Preview.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Apache Kafka 4.2.0 release announcement (Feb 17, 2026): &lt;a href="https://kafka.apache.org/blog/2026/02/17/apache-kafka-4.2.0-release-announcement/" rel="noopener noreferrer"&gt;https://kafka.apache.org/blog/2026/02/17/apache-kafka-4.2.0-release-announcement/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apache Kafka 4.3.0 release announcement (May 22, 2026): &lt;a href="https://kafka.apache.org/blog/2026/05/22/apache-kafka-4.3.0-release-announcement/" rel="noopener noreferrer"&gt;https://kafka.apache.org/blog/2026/05/22/apache-kafka-4.3.0-release-announcement/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;KIP-932, Queues for Kafka: &lt;a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-932%3A+Queues+for+Kafka" rel="noopener noreferrer"&gt;https://cwiki.apache.org/confluence/display/KAFKA/KIP-932%3A+Queues+for+Kafka&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Kafka 4.3 broker configs: &lt;a href="https://kafka.apache.org/43/configuration/broker-configs/" rel="noopener noreferrer"&gt;https://kafka.apache.org/43/configuration/broker-configs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Kafka 4.3 group configs: &lt;a href="https://kafka.apache.org/43/configuration/group-configs/" rel="noopener noreferrer"&gt;https://kafka.apache.org/43/configuration/group-configs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;KafkaShareConsumer javadoc (4.2): &lt;a href="https://kafka.apache.org/42/javadoc/org/apache/kafka/clients/consumer/KafkaShareConsumer.html" rel="noopener noreferrer"&gt;https://kafka.apache.org/42/javadoc/org/apache/kafka/clients/consumer/KafkaShareConsumer.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Kafka 4.4.0 release plan (KIP-1191 status): &lt;a href="https://cwiki.apache.org/confluence/spaces/KAFKA/pages/429064575/Release+Plan+4.4.0" rel="noopener noreferrer"&gt;https://cwiki.apache.org/confluence/spaces/KAFKA/pages/429064575/Release+Plan+4.4.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Confluent, Queues for Kafka GA: &lt;a href="https://www.confluent.io/blog/kafka-queue-semantics-share-consumer-ga/" rel="noopener noreferrer"&gt;https://www.confluent.io/blog/kafka-queue-semantics-share-consumer-ga/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;franz-go changelog: &lt;a href="https://github.com/twmb/franz-go/blob/master/CHANGELOG.md" rel="noopener noreferrer"&gt;https://github.com/twmb/franz-go/blob/master/CHANGELOG.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;confluent-kafka-python v2.15.0: &lt;a href="https://github.com/confluentinc/confluent-kafka-python/releases/tag/v2.15.0" rel="noopener noreferrer"&gt;https://github.com/confluentinc/confluent-kafka-python/releases/tag/v2.15.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;librdkafka v2.15.0: &lt;a href="https://github.com/confluentinc/librdkafka/releases/tag/v2.15.0" rel="noopener noreferrer"&gt;https://github.com/confluentinc/librdkafka/releases/tag/v2.15.0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PostgreSQL SELECT (locking clause, SKIP LOCKED): &lt;a href="https://www.postgresql.org/docs/current/sql-select.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/sql-select.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PostgreSQL routine vacuuming: &lt;a href="https://www.postgresql.org/docs/current/routine-vacuuming.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/routine-vacuuming.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PostgreSQL NOTIFY: &lt;a href="https://www.postgresql.org/docs/current/sql-notify.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/sql-notify.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PostgreSQL client connection defaults (timeouts): &lt;a href="https://www.postgresql.org/docs/current/runtime-config-client.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/runtime-config-client.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PostgreSQL replication config (hot_standby_feedback): &lt;a href="https://www.postgresql.org/docs/current/runtime-config-replication.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/current/runtime-config-replication.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PostgreSQL 17 release notes (transaction_timeout): &lt;a href="https://www.postgresql.org/docs/17/release-17.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/17/release-17.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PostgreSQL 19 draft release notes: &lt;a href="https://www.postgresql.org/docs/19/release-19.html" rel="noopener noreferrer"&gt;https://www.postgresql.org/docs/19/release-19.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PostgreSQL 19 RC1 and GA dates: &lt;a href="https://www.postgresql.org/message-id/915517c4-d0ee-49f9-bbeb-c683ef2b9d3f%40postgresql.org" rel="noopener noreferrer"&gt;https://www.postgresql.org/message-id/915517c4-d0ee-49f9-bbeb-c683ef2b9d3f%40postgresql.org&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;River, transactional enqueueing: &lt;a href="https://riverqueue.com/docs/transactional-enqueueing" rel="noopener noreferrer"&gt;https://riverqueue.com/docs/transactional-enqueueing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Solid Queue README: &lt;a href="https://github.com/rails/solid_queue" rel="noopener noreferrer"&gt;https://github.com/rails/solid_queue&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Debezium outbox event router: &lt;a href="https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html" rel="noopener noreferrer"&gt;https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>kafka</category>
      <category>go</category>
      <category>python</category>
    </item>
    <item>
      <title>Two PySpark contracts Staff DEs learn on a bad Tuesday: foreachBatch restarts and applyInPandas skew</title>
      <dc:creator>Firat Celik</dc:creator>
      <pubDate>Thu, 01 Oct 2026 06:16:50 +0000</pubDate>
      <link>https://dev.to/firfircelik/two-pyspark-contracts-staff-des-learn-on-a-bad-tuesday-foreachbatch-restarts-and-applyinpandas-skew-39pa</link>
      <guid>https://dev.to/firfircelik/two-pyspark-contracts-staff-des-learn-on-a-bad-tuesday-foreachbatch-restarts-and-applyinpandas-skew-39pa</guid>
      <description>&lt;p&gt;Your streaming MERGE looked fine in the notebook. Your &lt;code&gt;applyInPandas&lt;/code&gt; transform looked elegant. Then a job restarted and silver row counts jumped, or a hot customer key OOMed a worker while the rest of the cluster sat idle.&lt;/p&gt;

&lt;p&gt;Neither bug is exotic. Both are documented contracts that happy-path snippets skip.&lt;/p&gt;

&lt;p&gt;This article covers two production surfaces Staff and Lead data engineers own on Databricks / PySpark:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Streaming upserts via &lt;code&gt;foreachBatch&lt;/code&gt;&lt;/strong&gt;: at-least-once writes, watermark dedupe state, Delta &lt;code&gt;MERGE&lt;/code&gt; idempotency (logical, not &lt;code&gt;txnAppId&lt;/code&gt;), empty batches, and why you must fully consume a batch DataFrame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pandas Function APIs&lt;/strong&gt;: &lt;code&gt;groupBy().applyInPandas()&lt;/code&gt; loads an entire group into memory and does &lt;strong&gt;not&lt;/strong&gt; honor &lt;code&gt;spark.sql.execution.arrow.maxRecordsPerBatch&lt;/code&gt;; &lt;code&gt;mapInPandas&lt;/code&gt; / &lt;code&gt;mapInArrow&lt;/code&gt; do.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I will mark &lt;strong&gt;FACT&lt;/strong&gt; (from primary docs) vs &lt;strong&gt;interpretation&lt;/strong&gt; (engineering judgment). No invented latency percentages. No "exactly-once foreachBatch" fairy tales.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Primary sources:&lt;/strong&gt; &lt;a href="https://docs.databricks.com/aws/en/structured-streaming/foreach" rel="noopener noreferrer"&gt;Databricks foreachBatch&lt;/a&gt;, &lt;a href="https://docs.delta.io/latest/delta-update.html" rel="noopener noreferrer"&gt;Delta MERGE / streaming upsert&lt;/a&gt;, &lt;a href="https://docs.databricks.com/aws/en/structured-streaming/watermarks" rel="noopener noreferrer"&gt;watermarks&lt;/a&gt;, &lt;a href="https://docs.databricks.com/aws/en/structured-streaming/read-state" rel="noopener noreferrer"&gt;state store reader&lt;/a&gt;, &lt;a href="https://docs.databricks.com/aws/en/structured-streaming/delta-lake" rel="noopener noreferrer"&gt;idempotent Delta writes&lt;/a&gt;, &lt;a href="https://docs.databricks.com/aws/en/pandas/pandas-function-apis" rel="noopener noreferrer"&gt;pandas function APIs&lt;/a&gt;, &lt;a href="https://spark.apache.org/docs/latest/api/python/tutorial/sql/arrow_pandas.html" rel="noopener noreferrer"&gt;Apache Arrow in PySpark&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1. Design for the restart: &lt;code&gt;foreachBatch&lt;/code&gt;, watermarks, and MERGE
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Contract 1: &lt;code&gt;foreachBatch&lt;/code&gt; is at-least-once
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;FACT (Databricks):&lt;/strong&gt; &lt;code&gt;foreachBatch()&lt;/code&gt; provides only at-least-once write guarantees. You can use the &lt;code&gt;batchId&lt;/code&gt; to deduplicate and reason toward exactly-once yourself. You still own end-to-end semantics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interpretation:&lt;/strong&gt; A green micro-batch in the UI is not a guarantee that a retry will be a no-op. Treat every handler as if Spark will call it again with the same &lt;code&gt;batch_id&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;APP_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;silver-orders-upsert-v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# change if you reset the checkpoint
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# FACT: Delta sources can deliver empty micro-batches (e.g. OPTIMIZE with
&lt;/span&gt;    &lt;span class="c1"&gt;# nothing to process, or predicate pruning). Code must tolerate them.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;batch_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_amount &amp;gt; 0 AND customer_id IS NOT NULL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;invalid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NOT (order_amount &amp;gt; 0 AND customer_id IS NOT NULL)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# FACT: txnAppId + txnVersion on DataFrameWriter make appends idempotent
&lt;/span&gt;    &lt;span class="c1"&gt;# for retries of the same batch_id. Binding txnVersion to batch_id is the
&lt;/span&gt;    &lt;span class="c1"&gt;# documented pattern.
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;append&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;txnAppId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;APP_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;txnVersion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveAsTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main.silver.orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;invalid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;invalid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;append&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;txnAppId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;APP_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;txnVersion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveAsTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main.silver.orders_dlq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main.bronze.raw_orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;foreachBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process_batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkpointLocation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/Volumes/main/ops/checkpoints/orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&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;&lt;strong&gt;FACT (Databricks Delta streaming):&lt;/strong&gt; If you delete the streaming checkpoint and restart with a new checkpoint, you must provide a &lt;strong&gt;different&lt;/strong&gt; &lt;code&gt;txnAppId&lt;/code&gt;. New checkpoints start at batch ID &lt;code&gt;0&lt;/code&gt;; reusing the old app id can make Delta skip writes you intended to redo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FACT (Databricks):&lt;/strong&gt; Prefer letting the query fail and letting Lakeflow Jobs / Airflow retry the batch, rather than building silent local retry loops that can duplicate or drop data.&lt;/p&gt;




&lt;h3&gt;
  
  
  Contract 2: watermark dedupe has a state lease (and you must finish the batch)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;FACT (Databricks watermarks):&lt;/strong&gt; &lt;code&gt;dropDuplicatesWithinWatermark&lt;/code&gt; always deduplicates events that arrive within the watermark delay. Events outside the delay &lt;strong&gt;might&lt;/strong&gt; still be deduped, but that is &lt;strong&gt;not guaranteed&lt;/strong&gt;. To guarantee dropping all duplicates, set the watermark delay greater than the maximum timestamp distance between duplicate events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FACT (Databricks foreachBatch):&lt;/strong&gt; When you use stateful operators such as &lt;code&gt;dropDuplicatesWithinWatermark&lt;/code&gt;, each &lt;code&gt;foreachBatch&lt;/code&gt; iteration must &lt;strong&gt;completely consume&lt;/strong&gt; the batch DataFrame. If you only peek (&lt;code&gt;show(2)&lt;/code&gt;, take a few rows) and leave rows unread, the streaming query can fail on the next batch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql.functions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;

&lt;span class="n"&gt;deduped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main.bronze.events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withWatermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10 hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# &amp;gt; max duplicate lag (FACT guidance)
&lt;/span&gt;    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dropDuplicatesWithinWatermark&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bad_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Anti-pattern: partial consume after a stateful operator
&lt;/span&gt;    &lt;span class="n"&gt;batch_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# remaining rows never consumed -&amp;gt; next micro-batch can fail
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;good_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;batch_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="c1"&gt;# Full action that consumes the plan (write, count, foreach, etc.)
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;batch_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;append&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;txnAppId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;events-dedupe-v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;txnVersion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveAsTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main.silver.events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;deduped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;foreachBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;good_handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkpointLocation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/Volumes/main/ops/checkpoints/events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&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;&lt;strong&gt;Debugging tip (FACT):&lt;/strong&gt; You can inspect Structured Streaming state with the &lt;code&gt;statestore&lt;/code&gt; reader. Metadata can show an operator named &lt;code&gt;dedupeWithinWatermark&lt;/code&gt;; query that &lt;code&gt;operatorId&lt;/code&gt; when you need to see what the lease still holds. See &lt;a href="https://docs.databricks.com/aws/en/structured-streaming/read-state" rel="noopener noreferrer"&gt;Read Structured Streaming state information&lt;/a&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Contract 3: MERGE inside &lt;code&gt;foreachBatch&lt;/code&gt; is a &lt;em&gt;logical&lt;/em&gt; idempotency problem
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;FACT (Delta Lake):&lt;/strong&gt; You must use &lt;code&gt;foreachBatch&lt;/code&gt; for Delta Lake merge operations from Structured Streaming. Restarts can apply the same batch multiple times, so &lt;strong&gt;your merge statement inside &lt;code&gt;foreachBatch&lt;/code&gt; must be idempotent&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FACT (Databricks / Delta writer options):&lt;/strong&gt; &lt;code&gt;txnAppId&lt;/code&gt; and &lt;code&gt;txnVersion&lt;/code&gt; are &lt;code&gt;DataFrameWriter&lt;/code&gt; options for idempotent &lt;strong&gt;writes&lt;/strong&gt; (append / save). Do &lt;strong&gt;not&lt;/strong&gt; assume they wrap &lt;code&gt;DeltaTable.merge(...)&lt;/code&gt;. Design MERGE so re-running the same micro-batch yields the same table state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FACT (Delta):&lt;/strong&gt; A merge can fail if multiple source rows match the same target row (ambiguous update). For CDC, preprocess to keep the &lt;strong&gt;latest change per key&lt;/strong&gt; before merging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FACT (Delta):&lt;/strong&gt; MERGE often reads the source multiple times. That can inflate reported streaming input rates. Cache the micro-batch DataFrame before merge and unpersist after if this becomes a bottleneck.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;delta.tables&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DeltaTable&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;

&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DeltaTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main.silver.customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upsert_cdc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;batch_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="c1"&gt;# Collapse to latest change per business key (FACT: avoid multi-match MERGE)
&lt;/span&gt;    &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;batch_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deleted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chg.*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# FACT: MERGE may scan the source more than once -&amp;gt; cache the batch
&lt;/span&gt;    &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t.customer_id = s.customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;whenMatchedDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s.deleted = true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;whenMatchedUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s.payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s.event_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;whenNotMatchedInsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s.deleted = false&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s.customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s.payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s.event_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpersist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main.bronze.customer_cdc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;foreachBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upsert_cdc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkpointLocation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/Volumes/main/ops/checkpoints/customer_cdc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&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;&lt;strong&gt;Interpretation:&lt;/strong&gt; "Idempotent MERGE" here means latest-wins on a stable key (and deletes keyed the same way), not "Spark will skip the merge via txn metadata." If the same CDC events replay, the table should converge to the same rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managed alternative (context, not a product pitch):&lt;/strong&gt; Databricks processing-guarantees docs note that managed Delta-to-Delta flows and AUTO CDC can give stronger defaults; a custom &lt;code&gt;foreachBatch&lt;/code&gt; edge is still at-least-once unless you add explicit idempotent write or dedupe logic. Use AUTO CDC when it fits; when you hand-roll MERGE, you own the restart contract above.&lt;/p&gt;




&lt;h3&gt;
  
  
  Part 1 checklist (print this next to the PR)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Checkpoint&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Empty-batch safe&lt;/td&gt;
&lt;td&gt;Delta sources can emit empty micro-batches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full consume after stateful ops&lt;/td&gt;
&lt;td&gt;Partial &lt;code&gt;show&lt;/code&gt;/&lt;code&gt;take&lt;/code&gt; can fail the next batch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Watermark &amp;gt; max duplicate lag&lt;/td&gt;
&lt;td&gt;Out-of-watermark dedupe is not guaranteed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Append path uses &lt;code&gt;txnAppId&lt;/code&gt; + &lt;code&gt;txnVersion=batch_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Writer-level idempotency on retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New checkpoint =&amp;gt; new &lt;code&gt;txnAppId&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Else batch 0 collisions skip writes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MERGE is latest-per-key / safe to re-run&lt;/td&gt;
&lt;td&gt;txn options do not wrap &lt;code&gt;DeltaTable.merge&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache around MERGE when metrics / cost hurt&lt;/td&gt;
&lt;td&gt;MERGE multi-pass over the micro-batch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fail fast to the orchestrator&lt;/td&gt;
&lt;td&gt;Local retry loops are how you invent exactly-once bugs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Part 2. Arrow is not a free pass: &lt;code&gt;applyInPandas&lt;/code&gt; vs &lt;code&gt;mapInPandas&lt;/code&gt; / &lt;code&gt;mapInArrow&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Python DE craft is not only streaming. The other Tuesday failure mode is "we used pandas because Arrow is fast" and then a skewed group killed the executor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three APIs, three memory contracts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;FACT (Databricks pandas function APIs / Apache Spark):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;What you write&lt;/th&gt;
&lt;th&gt;Memory / batching contract&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;groupBy().applyInPandas(f, schema)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One pandas DataFrame &lt;strong&gt;per group&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;All rows for the group load into memory&lt;/strong&gt; before &lt;code&gt;f&lt;/code&gt; runs. &lt;code&gt;spark.sql.execution.arrow.maxRecordsPerBatch&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; applied. Skew =&amp;gt; OOM risk.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mapInPandas(f, schema)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Iterator of pandas DataFrames (batches)&lt;/td&gt;
&lt;td&gt;Batch size controlled by &lt;code&gt;spark.sql.execution.arrow.maxRecordsPerBatch&lt;/code&gt; (default &lt;strong&gt;10,000&lt;/strong&gt; rows in Spark docs). Output length can differ from input.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mapInArrow(f, schema)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Iterator of &lt;code&gt;pyarrow.RecordBatch&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Same Arrow batch sizing idea; stay in Arrow without pandas round-trip when you can.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cogrouped &lt;code&gt;applyInPandas&lt;/code&gt; has the same whole-cogroup-in-memory warning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skew turns "elegant" into OOM
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Looks clean. Under a hot customer_id it is a bomb.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;per_customer_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# pdf = ALL rows for one customer_id
&lt;/span&gt;    &lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rolling_amt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rolling&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_periods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;


&lt;span class="c1"&gt;# FACT: entire group must fit in worker memory; maxRecordsPerBatch does NOT help
&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;applyInPandas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;per_customer_features&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id string, event_time timestamp, amount double, rolling_amt double&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Interpretation:&lt;/strong&gt; Use &lt;code&gt;applyInPandas&lt;/code&gt; when the group is a true unit of work &lt;strong&gt;and&lt;/strong&gt; you can bound group size (filter, sample hot keys, pre-aggregate, salt, or split whales). Do not use it as a generic "run pandas on Spark" hammer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prefer batch maps when you do not need whole-group semantics
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.execution.arrow.maxRecordsPerBatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_batches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# init once per Python worker task
&lt;/span&gt;    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_light_model&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# your code; keep it picklable / available on workers
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;


&lt;span class="n"&gt;scored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mapInPandas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;score_batches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id long, score double&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or stay in Arrow when pandas is unnecessary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pyarrow&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pyarrow.compute&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pc&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;filter_arrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;greater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;positive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mapInArrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter_arrow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Decision table
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Prefer&lt;/th&gt;
&lt;th&gt;Watch out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Need whole-group window / asof / custom group ML&lt;/td&gt;
&lt;td&gt;&lt;code&gt;applyInPandas&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hot keys; no Arrow batch limit; cogroup same issue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row/batch transform, arbitrary output length&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mapInPandas&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tune &lt;code&gt;maxRecordsPerBatch&lt;/code&gt; down if wide rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU vector ops, avoid pandas copies&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mapInArrow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Arrow type coverage / compute familiarity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Series-to-series column expression&lt;/td&gt;
&lt;td&gt;classic &lt;code&gt;pandas_udf&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Output length must match input for many UDF shapes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;FACT (Spark Arrow guide):&lt;/strong&gt; Lower &lt;code&gt;maxRecordsPerBatch&lt;/code&gt; when column count is large; Arrow batches can pressure JVM memory during conversion.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the two themes connect
&lt;/h2&gt;

&lt;p&gt;Both failures are the same Staff DE mistake: &lt;strong&gt;assuming the platform's happy path equals your failure domain.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Streaming: you assumed merge + checkpoint = exactly-once. Docs say at-least-once + &lt;em&gt;you&lt;/em&gt; make MERGE idempotent; writer txn options are a different tool.&lt;/li&gt;
&lt;li&gt;Pandas APIs: you assumed "Arrow optimized" = bounded memory. Docs say group maps ignore the batch limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Own the contract in the PR template, not in the postmortem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Optional diagram (for a cover or inline figure)
&lt;/h2&gt;

&lt;p&gt;Two panels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Restart timeline:&lt;/strong&gt; micro-batch N fails mid-MERGE -&amp;gt; orchestrator retries batch N -&amp;gt; arrows labeled "append: txn skip" vs "MERGE: must be latest-wins safe".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skew funnel:&lt;/strong&gt; many small &lt;code&gt;customer_id&lt;/code&gt; groups sail through &lt;code&gt;applyInPandas&lt;/code&gt;; one whale group expands to "all rows in RAM" with a red X; side path routes whales to salted / pre-aggregated / &lt;code&gt;mapInPandas&lt;/code&gt; path.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://docs.databricks.com/aws/en/structured-streaming/foreach" rel="noopener noreferrer"&gt;Use foreachBatch to write to arbitrary data sinks (Databricks)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.delta.io/latest/delta-update.html" rel="noopener noreferrer"&gt;Delta Lake table deletes, updates, and merges (streaming foreachBatch section)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.databricks.com/aws/en/structured-streaming/watermarks" rel="noopener noreferrer"&gt;Apply watermarks / dropDuplicatesWithinWatermark (Databricks)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.databricks.com/aws/en/structured-streaming/read-state" rel="noopener noreferrer"&gt;Read Structured Streaming state information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.databricks.com/aws/en/structured-streaming/delta-lake" rel="noopener noreferrer"&gt;Delta Lake streaming reads and writes (idempotent foreachBatch writes)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.databricks.com/aws/en/pandas/pandas-function-apis" rel="noopener noreferrer"&gt;pandas function APIs (Databricks)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spark.apache.org/docs/latest/api/python/tutorial/sql/arrow_pandas.html" rel="noopener noreferrer"&gt;Apache Arrow in PySpark&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/api/pyspark.sql.DataFrame.mapInArrow.html" rel="noopener noreferrer"&gt;mapInArrow API&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;I'm Firat Celik, Lead Data Engineer. I write about production data platforms, PySpark/Delta, and the boring contracts that keep pipelines honest.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>databricks</category>
      <category>pyspark</category>
      <category>python</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Fabric Runtime 2.0 is becoming the default. Here is what actually breaks.</title>
      <dc:creator>Firat Celik</dc:creator>
      <pubDate>Mon, 28 Sep 2026 06:44:13 +0000</pubDate>
      <link>https://dev.to/firfircelik/fabric-runtime-20-is-becoming-the-default-here-is-what-actually-breaks-12i9</link>
      <guid>https://dev.to/firfircelik/fabric-runtime-20-is-becoming-the-default-here-is-what-actually-breaks-12i9</guid>
      <description>&lt;h1&gt;
  
  
  Fabric Runtime 2.0 is becoming the default. Here is what actually breaks.
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Late September 2026 is when Fabric Runtime 2.0 stops being an opt-in experiment and becomes the path of least resistance for new workspaces and environment items. If you treat that as a release note, you will discover the breakages in production.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I work as a Lead Data Engineer on Azure, Fabric, Databricks, and Spark. This is the migration checklist I wish teams ran &lt;em&gt;before&lt;/em&gt; the default flipped under them.&lt;/p&gt;

&lt;p&gt;Docs to keep open while you read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/fabric/data-engineering/runtime-2-0" rel="noopener noreferrer"&gt;Fabric Runtime 2.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/fabric/data-engineering/native-execution-engine-overview" rel="noopener noreferrer"&gt;Native Execution Engine overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1) Hook: the default &lt;em&gt;is&lt;/em&gt; the migration
&lt;/h2&gt;

&lt;p&gt;Microsoft's Runtime 2.0 page is blunt: GA and ready for production, but &lt;strong&gt;not&lt;/strong&gt; the default yet. You opt in. The stated plan is to make Runtime 2.0 the default UX selection and the default for &lt;strong&gt;new workspaces and new environment items&lt;/strong&gt; in late September 2026.&lt;/p&gt;

&lt;p&gt;Today is 28 September 2026. That window is now.&lt;/p&gt;

&lt;p&gt;Existing workspaces, jobs, and Environment items stay on whatever you configured. The trap is quieter. Every new workspace for a pilot, every Environment someone creates because "defaults are fine," starts landing on Spark 4.1 / Delta 4.2 / Python 3.13 once the plan executes.&lt;/p&gt;

&lt;p&gt;Treat the flip as an incident rehearsal date. Pin what you care about. Validate 2.0 on a scoped Environment. Do not discover Python wheel breakage through a failed Monday job.&lt;/p&gt;

&lt;p&gt;Runtime 1.3 still has runway. Secondary writeups put Microsoft's LTS window for 1.3 roughly from early October 2026 through March 2027. Verify against current lifecycle docs before you commit a program date. LTS means supported, not "ignore migration."&lt;/p&gt;

&lt;h2&gt;
  
  
  2) Wrong assumption: GA means notebooks just run
&lt;/h2&gt;

&lt;p&gt;Runtime 1.3 to Runtime 2.0 is not a patch bump. It is a stack jump on nearly every component:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Runtime 1.3 (typical)&lt;/th&gt;
&lt;th&gt;Runtime 2.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Apache Spark&lt;/td&gt;
&lt;td&gt;3.5&lt;/td&gt;
&lt;td&gt;4.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delta Lake&lt;/td&gt;
&lt;td&gt;3.2&lt;/td&gt;
&lt;td&gt;4.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;3.11&lt;/td&gt;
&lt;td&gt;3.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scala&lt;/td&gt;
&lt;td&gt;2.12&lt;/td&gt;
&lt;td&gt;2.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS&lt;/td&gt;
&lt;td&gt;(prior Fabric OS)&lt;/td&gt;
&lt;td&gt;Azure Linux 3.0 (Mariner 3.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Industry writeups also call out Delta protocol defaults moving higher (reader/writer versions, Deletion Vectors in the 2.0 path). Treat protocol as a first-class migration risk, not a footnote. See Microsoft's Delta Lake table format interoperability guidance for what cross-experience readers can actually open.&lt;/p&gt;

&lt;p&gt;Practical fallout I care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.11 -&amp;gt; 3.13.&lt;/strong&gt; Environment-level libraries may migrate automatically. Validation does not. Custom wheels and pinned packages are your problem. Session-scoped &lt;code&gt;%pip install&lt;/code&gt; and notebook Resources are a different blast radius than Environment items.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scala 2.12 -&amp;gt; 2.13 / Java 11 -&amp;gt; 21.&lt;/strong&gt; JARs and connectors that "worked forever" on 1.3 deserve a dual-run, not hope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SparkR is deprecated in Spark 4.x&lt;/strong&gt; and may be removed later. If you still have SparkR notebooks, the clock is already running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GA means Microsoft ships it for production use. It does not mean your Environment's library graph survives the jump without you watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  3) Wrong assumption: NEE = free 6x
&lt;/h2&gt;

&lt;p&gt;Runtime 2.0 includes the Native Execution Engine (NEE). Microsoft's docs describe it as a vectorized path based on &lt;strong&gt;Apache Gluten&lt;/strong&gt; (offload middle layer) and &lt;strong&gt;Velox&lt;/strong&gt; (C++ engine from Meta). Supported operators leave the JVM and run columnar with SIMD-style acceleration on Parquet, Delta, and (now) CSV. Microsoft cites &lt;strong&gt;up to six times faster&lt;/strong&gt; vs open-source Spark on TPC-DS. That is a vendor benchmark on a specific workload family. It is a ceiling story, not your median job.&lt;/p&gt;

&lt;p&gt;How you enable it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Environment -&amp;gt; Spark compute -&amp;gt; &lt;strong&gt;Acceleration&lt;/strong&gt; -&amp;gt; enable native execution engine (preferred UX path), or&lt;/li&gt;
&lt;li&gt;Spark conf: &lt;code&gt;spark.native.enabled&lt;/code&gt; (also usable per notebook / SJD via &lt;code&gt;%%configure&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hard limits from the docs that matter in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structured streaming is not supported.&lt;/strong&gt; Use streaming? Plan for JVM (or keep NEE off for those jobs).&lt;/li&gt;
&lt;li&gt;Unsupported ops &lt;strong&gt;fall back&lt;/strong&gt; to classic Spark. Fallback is automatic. Your job keeps running. Your SLA may not.&lt;/li&gt;
&lt;li&gt;Every boundary between native columnar and JVM row-oriented execution pays a &lt;strong&gt;conversion tax&lt;/strong&gt;. That is the part marketing slides skip.&lt;/li&gt;
&lt;li&gt;Fabric Spark Advisor can surface fallback alerts in notebook cell output. Use them. Do not ignore green "job succeeded" when Advisor is screaming about offload misses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NEE also preserves AQE, cost-based rewrites, column pruning, and predicate pushdown when operators &lt;em&gt;do&lt;/em&gt; offload. Good. That does not help if half the plan is bouncing in and out of Velox.&lt;/p&gt;

&lt;p&gt;Blunt take: a vectorized engine that falls back on half your plan is not acceleration. It is a tax you opted into because a dashboard said "up to 6x."&lt;/p&gt;

&lt;h2&gt;
  
  
  4) How to see the truth in the plan
&lt;/h2&gt;

&lt;p&gt;Do not argue with anecdotes. Read the plan.&lt;/p&gt;

&lt;p&gt;In Spark UI / History Server, look for node names ending in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;*Transformer&lt;/code&gt; (for example &lt;code&gt;ProjectExecTransformer&lt;/code&gt;, &lt;code&gt;BroadcastHashJoinExecTransformer&lt;/code&gt;, &lt;code&gt;RollUpHashAggregateTransformer&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*NativeFileScan&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VeloxColumnarToRowExec&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those suffixes mean native offload happened for that node. Light-blue JVM nodes next to green native nodes (Microsoft's Gluten SQL / DataFrame tab uses that color language) are your fallback map. Every &lt;code&gt;VeloxColumnarToRowExec&lt;/code&gt; (and the reverse path) is a place where you paid for format conversion.&lt;/p&gt;

&lt;p&gt;Also useful: &lt;code&gt;df.explain()&lt;/code&gt;, Fabric Spark Advisor fallback alerts, and the Gluten SQL / DataFrame tab's native-vs-JVM node counts.&lt;/p&gt;

&lt;p&gt;Surgical disable for one cell (unsupported op, correctness edge, or A/B):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;spark.native.enabled&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-enable afterward if later cells should use NEE again. Spark runs cells in order. Conf sticks.&lt;/p&gt;

&lt;h2&gt;
  
  
  5) Delta 4.x trap for multi-workload tables
&lt;/h2&gt;

&lt;p&gt;This is the silent one.&lt;/p&gt;

&lt;p&gt;Microsoft is explicit: &lt;strong&gt;Delta Lake 4.2-specific features are experimental and only work on Spark experiences&lt;/strong&gt; (notebooks and Spark Job Definitions). If the same Delta tables must be used across multiple Fabric workloads, &lt;strong&gt;do not enable those features&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why it hurts: SQL analytics endpoint, Power BI Direct Lake, and other non-Spark readers do not all speak the same protocol / feature set. Protocol upgrades (higher reader/writer versions, Deletion Vectors, experimental checkpoints) are effectively one-way doors. Recreate the table is the recovery path, not "uncheck the box." Shared bronze/silver tables that feed Spark &lt;em&gt;and&lt;/em&gt; Direct Lake &lt;em&gt;and&lt;/em&gt; SQL endpoint are exactly where someone enables a shiny 4.x feature on a Friday and breaks Monday's semantic model.&lt;/p&gt;

&lt;p&gt;Rule I use: &lt;strong&gt;default to the most constrained reader in the estate.&lt;/strong&gt; If Direct Lake or the SQL endpoint must read it, keep features inside what those readers support. Save experimental Delta 4.x toys for Spark-only sandboxes.&lt;/p&gt;

&lt;h2&gt;
  
  
  6) Practical migration pattern
&lt;/h2&gt;

&lt;p&gt;This is the runbook shape I actually use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Inventory&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workspaces and Environment items: which runtime are they on?&lt;/li&gt;
&lt;li&gt;Jobs that create &lt;em&gt;new&lt;/em&gt; Environments without pinning&lt;/li&gt;
&lt;li&gt;Libraries: Environment wheels vs session &lt;code&gt;%pip&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tables read by more than one Fabric experience&lt;/li&gt;
&lt;li&gt;Streaming jobs (NEE: leave off or isolate)&lt;/li&gt;
&lt;li&gt;SparkR notebooks (debt clock)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Baseline on 1.3&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Duration, shuffle bytes, spill, failure rate, data quality checks&lt;/li&gt;
&lt;li&gt;Capture plans for critical queries &lt;em&gt;before&lt;/em&gt; you change anything&lt;/li&gt;
&lt;li&gt;Note current Delta protocol / table features on shared tables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Dual-run on 2.0&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scoped Environment item first (surgical), not whole workspace&lt;/li&gt;
&lt;li&gt;Same input snapshot, same checks, compare plans and SLA&lt;/li&gt;
&lt;li&gt;Turn NEE on only after the JVM path is green on 2.0&lt;/li&gt;
&lt;li&gt;Kill vanity actions that force row materialization (yes, that includes habit &lt;code&gt;.show()&lt;/code&gt; in hot paths during timing runs)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Rollback in the runbook before you need it&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runtime: pin Environment / workspace back to 1.3 while LTS covers you&lt;/li&gt;
&lt;li&gt;NEE only:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;spark.native.enabled&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put both in the on-call doc. A rollback you invent at 02:00 is not a rollback.&lt;/p&gt;

&lt;h2&gt;
  
  
  7) CI/CD angle: pin the runtime like a toolchain
&lt;/h2&gt;

&lt;p&gt;You would not let &lt;code&gt;python&lt;/code&gt; float to whatever the image shipped last Tuesday without a lockfile. Treat Fabric runtime the same way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pin runtime version on Environment items that production notebooks and SJDs attach to&lt;/li&gt;
&lt;li&gt;Fail the pipeline if an Environment publishes without an explicit runtime&lt;/li&gt;
&lt;li&gt;Gate promotion: 1.3 baseline metrics vs 2.0 dual-run metrics must be compared, not vibes&lt;/li&gt;
&lt;li&gt;Separate "NEE enabled" as its own promotion flag. Runtime upgrade and native offload are two changes. Ship them as two changes.&lt;/li&gt;
&lt;li&gt;Use Fabric runtime release channels (preview) to rehearse upcoming defaults on custom pools. Do not park production there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If CI deploys notebook JSON and never asserts runtime + Acceleration settings, you are continuous-delivering entropy.&lt;/p&gt;

&lt;h2&gt;
  
  
  8) Close: benchmarks sell the ceiling, production pays the median
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload type&lt;/th&gt;
&lt;th&gt;Enable NEE?&lt;/th&gt;
&lt;th&gt;Watch for...&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Large Parquet/Delta batch ETL, heavy agg/join&lt;/td&gt;
&lt;td&gt;Yes, after dual-run&lt;/td&gt;
&lt;td&gt;Fallback density, conversion boundaries, Advisor alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interactive lakehouse SQL / notebook exploration&lt;/td&gt;
&lt;td&gt;Often yes&lt;/td&gt;
&lt;td&gt;Date type mismatches on filters, wide UDF usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured streaming&lt;/td&gt;
&lt;td&gt;No (unsupported)&lt;/td&gt;
&lt;td&gt;Accidental enable via Environment inheritance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON/XML heavy paths&lt;/td&gt;
&lt;td&gt;Limited / often no&lt;/td&gt;
&lt;td&gt;Format falls back to JVM; measure before celebrating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared Delta tables (Spark + SQL endpoint / Direct Lake)&lt;/td&gt;
&lt;td&gt;NEE optional; &lt;strong&gt;Delta 4.x experimental features: no&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Protocol / feature lockout of non-Spark readers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SparkR or exotic Scala libs&lt;/td&gt;
&lt;td&gt;Maybe later&lt;/td&gt;
&lt;td&gt;Language deprecation and binary incompat first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Takeaways
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pin Runtime 1.3 on anything you cannot afford to surprise&lt;/strong&gt;, then opt into 2.0 on a scoped Environment before the default teaches your newest workspace the hard way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat NEE as a plan shape problem&lt;/strong&gt;, not a toggle. &lt;code&gt;*Transformer&lt;/code&gt; and &lt;code&gt;VeloxColumnarToRowExec&lt;/code&gt; tell the truth; "up to 6x" does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never enable experimental Delta 4.x features on multi-experience tables.&lt;/strong&gt; Shared tables inherit the weakest reader's constraints.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Benchmarks sell the ceiling. Production pays the median. Migrate like you believe that.&lt;/p&gt;

&lt;p&gt;What is your scarier unpaid bill right now: Python 3.13 libraries, NEE fallback thrash, or a Delta feature someone enabled on a table Direct Lake still needs to read?&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/fabric/data-engineering/runtime-2-0" rel="noopener noreferrer"&gt;Runtime 2.0 in Fabric (Microsoft Learn)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/fabric/data-engineering/native-execution-engine-overview" rel="noopener noreferrer"&gt;Native execution engine for Fabric Data Engineering (Microsoft Learn)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Context (not a substitute for the docs): &lt;a href="https://rajanieshkaushikk.com/2026/09/23/fabric-runtime-2-0-two-breakages/" rel="noopener noreferrer"&gt;Fabric Runtime 2.0 two breakages (Rajaniesh Kaushikk, 2026-09-23)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>fabric</category>
      <category>spark</category>
      <category>dataengineering</category>
      <category>azure</category>
    </item>
    <item>
      <title>Azure Synapse Databricks: A Migration Playbook That Survives Production</title>
      <dc:creator>Firat Celik</dc:creator>
      <pubDate>Fri, 04 Sep 2026 10:11:59 +0000</pubDate>
      <link>https://dev.to/firfircelik/azure-synapse-databricks-a-migration-playbook-that-survives-production-nge</link>
      <guid>https://dev.to/firfircelik/azure-synapse-databricks-a-migration-playbook-that-survives-production-nge</guid>
      <description>&lt;h1&gt;
  
  
  Azure Synapse → Databricks: A Migration Playbook That Survives Production
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;A field-style guide for Lead / Senior Data Engineers moving Dedicated SQL Pools, pipelines, and BI off Synapse without burning trust, budget, or the on-call team.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why this migration is harder than the slide deck&lt;/li&gt;
&lt;li&gt;What you are actually migrating&lt;/li&gt;
&lt;li&gt;Program shape: discovery → waves&lt;/li&gt;
&lt;li&gt;Four parallel workstreams&lt;/li&gt;
&lt;li&gt;Data movement: skip the JDBC tax&lt;/li&gt;
&lt;li&gt;Code conversion: syntax is easy, semantics bite&lt;/li&gt;
&lt;li&gt;Dimensional patterns on Delta (SCD2)&lt;/li&gt;
&lt;li&gt;Reconciliation &amp;amp; cutover&lt;/li&gt;
&lt;li&gt;Power BI / consumption cutover&lt;/li&gt;
&lt;li&gt;30 / 60 / 90 plan&lt;/li&gt;
&lt;li&gt;Anti-patterns I keep seeing&lt;/li&gt;
&lt;li&gt;Field rules I will not compromise on&lt;/li&gt;
&lt;li&gt;FAQ&lt;/li&gt;
&lt;li&gt;Sources&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1) Why this migration is harder than the slide deck
&lt;/h2&gt;

&lt;p&gt;If you have lived in &lt;strong&gt;Azure Synapse&lt;/strong&gt; for years, the marketing line sounds simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Move to Databricks. Unity Catalog. Lakehouse. Done.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In production programs, that is not the hard part.&lt;/p&gt;

&lt;p&gt;The hard part is that &lt;strong&gt;Synapse is not one system&lt;/strong&gt;. Under one brand you usually own years of Dedicated SQL Pool logic, ADF/Synapse Pipelines, serverless SQL over the lake, Spark pools, permission models, and Power BI datasets wired to Synapse endpoints.&lt;/p&gt;

&lt;p&gt;Treat that estate as &lt;strong&gt;one workstream with one deadline&lt;/strong&gt;, and the schedule slips. Every time.&lt;/p&gt;

&lt;p&gt;I work as a &lt;strong&gt;Lead Data Engineer&lt;/strong&gt; across Azure Synapse, Microsoft Fabric, Azure Data Lake, Databricks/Spark, SQL/T-SQL, and Power BI. This playbook is the version I want in the room &lt;em&gt;before&lt;/em&gt; the first converted stored procedure lands in production.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Thesis:&lt;/strong&gt; Migration success is not “all notebooks on Databricks.”&lt;br&gt;&lt;br&gt;
It is &lt;strong&gt;decision-ready data, trusted KPIs, and a platform your team can operate&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2) What you are actually migrating
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Synapse piece&lt;/th&gt;
&lt;th&gt;Typical complexity&lt;/th&gt;
&lt;th&gt;What “done” looks like on Databricks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dedicated SQL Pools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Highest — procs, distributions, CCI, years of tuning&lt;/td&gt;
&lt;td&gt;Databricks SQL / Spark jobs, Delta tables, Liquid Clustering / predictive optimization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Serverless SQL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Medium — views &amp;amp; external tables over ADLS&lt;/td&gt;
&lt;td&gt;Views / tables in Unity Catalog over the same lake paths (often simpler)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spark pools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lower — already Spark-shaped&lt;/td&gt;
&lt;td&gt;Databricks Runtime notebooks / jobs (often few changes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ADF / Synapse Pipelines&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High — orchestration + connectors&lt;/td&gt;
&lt;td&gt;Lakeflow Connect / Lakeflow Jobs / Workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Governance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High — SQL perms + Purview stitching&lt;/td&gt;
&lt;td&gt;Unity Catalog (and stop dual-governing forever)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Power BI / semantic models&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High politically&lt;/td&gt;
&lt;td&gt;Databricks SQL Warehouse endpoints + dataset remaps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Dedicated SQL Pools usually consume most of the calendar. Orchestration and BI consume most of the &lt;strong&gt;organizational&lt;/strong&gt; risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  3) Program shape: discovery → waves
&lt;/h2&gt;

&lt;p&gt;Do not start with “transpile everything.” Start with a &lt;strong&gt;program&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  A[1 Discovery&amp;lt;br/&amp;gt;Lakebridge Profiler] --&amp;gt; B[2 Assessment&amp;lt;br/&amp;gt;Analyzer / T-SQL]
  B --&amp;gt; C[3 Design&amp;lt;br/&amp;gt;Hybrid lift + modernize]
  C --&amp;gt; D[4 Pilot&amp;lt;br/&amp;gt;Lighthouse E2E]
  D --&amp;gt; E[5 Waves&amp;lt;br/&amp;gt;Parallel workstreams]&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  Phase A — Discovery
&lt;/h3&gt;

&lt;p&gt;Inventory what actually runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pools, databases, schemas, object counts&lt;/li&gt;
&lt;li&gt;top queries / heavy procs by CPU &amp;amp; IO&lt;/li&gt;
&lt;li&gt;pipeline inventory (ADF / Synapse)&lt;/li&gt;
&lt;li&gt;downstream BI datasets and owners&lt;/li&gt;
&lt;li&gt;unused objects (your cheapest scope cut)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lakebridge Profiler&lt;/strong&gt; (Databricks Labs) is built for Synapse estate profiling — metadata, utilisation, query patterns, baselines for a TCO conversation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase B — Assessment
&lt;/h3&gt;

&lt;p&gt;Classify the T-SQL estate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;low / medium / high complexity&lt;/li&gt;
&lt;li&gt;unsupported constructs&lt;/li&gt;
&lt;li&gt;dependency graphs (what breaks if this view moves)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lakebridge Analyzer&lt;/strong&gt; helps here. Start waves with low/medium complexity and clear business owners — not the ugliest proc on day one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase C — Design (usually hybrid)
&lt;/h3&gt;

&lt;p&gt;Three strategies show up in every steering committee:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;When it works&lt;/th&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lift-and-shift&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hard deadline off Synapse&lt;/td&gt;
&lt;td&gt;You recreate Synapse-shaped pain on a new bill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Big-bang rewrite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Greenfield team, tiny estate&lt;/td&gt;
&lt;td&gt;Timeline fantasy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hybrid&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Most real enterprises&lt;/td&gt;
&lt;td&gt;Needs discipline: automate bulk convert, modernize in waves&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most Synapse migrations, &lt;strong&gt;hybrid&lt;/strong&gt; wins: automated conversion gets you off Synapse on a schedule; modernization (Delta patterns, Liquid Clustering, Lakeflow, UC) happens after workloads are stable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase D — Pilot (lighthouse)
&lt;/h3&gt;

&lt;p&gt;Pick &lt;strong&gt;one&lt;/strong&gt; end-to-end slice:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;source → ingest → transform → gold mart → Power BI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Prove:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;storage + UC layout&lt;/li&gt;
&lt;li&gt;job standards&lt;/li&gt;
&lt;li&gt;reconciliation pack&lt;/li&gt;
&lt;li&gt;BI remap&lt;/li&gt;
&lt;li&gt;on-call runbook&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pilot’s job is not heroics. It is producing &lt;strong&gt;reusable assets&lt;/strong&gt; for later waves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase E — Waves
&lt;/h3&gt;

&lt;p&gt;Each wave should ship a &lt;strong&gt;visible business win&lt;/strong&gt;, not just “another schema moved.”&lt;/p&gt;




&lt;h2&gt;
  
  
  4) Four parallel workstreams
&lt;/h2&gt;

&lt;p&gt;After the lighthouse, scale on &lt;strong&gt;four tracks together&lt;/strong&gt;. Sequential “ETL first, BI last” is how stakeholders only feel pain.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
  P[Lighthouse Pilot]
  P --&amp;gt; I[Ingestion&amp;lt;br/&amp;gt;ADF/Synapse Pipelines → Lakeflow Connect]
  P --&amp;gt; T[Transformation&amp;lt;br/&amp;gt;T-SQL / procs → Databricks SQL / Spark]
  P --&amp;gt; O[Orchestration&amp;lt;br/&amp;gt;Schedules → Lakeflow Jobs]
  P --&amp;gt; C[Consumption&amp;lt;br/&amp;gt;Power BI → Databricks SQL Warehouse]&lt;/code&gt;&lt;/pre&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Track&lt;/th&gt;
&lt;th&gt;From&lt;/th&gt;
&lt;th&gt;To&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ingestion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ADF / Synapse Pipelines&lt;/td&gt;
&lt;td&gt;Lakeflow Connect / Jobs (or open ingest into Delta)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transformation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;T-SQL / procs / views&lt;/td&gt;
&lt;td&gt;Databricks SQL / Spark with clear contracts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Orchestration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Synapse schedules &amp;amp; deps&lt;/td&gt;
&lt;td&gt;Lakeflow Jobs / Workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Consumption&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Power BI / semantic models&lt;/td&gt;
&lt;td&gt;Databricks SQL Warehouses&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Sequencing tip that builds trust
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;BI-aware&lt;/strong&gt; sequence often works better than pure ETL-first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Land or expose business-facing marts early (even interim / federation patterns where appropriate).&lt;/li&gt;
&lt;li&gt;Put a trusted KPI dataset in front of users.&lt;/li&gt;
&lt;li&gt;Modernize the uglier ETL underneath while the business already sees progress.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Enablement is a fifth unofficial track. Synapse-native teams underinvest here and then call the platform “not ready.”&lt;/p&gt;




&lt;h2&gt;
  
  
  5) Data movement: skip the JDBC tax
&lt;/h2&gt;

&lt;p&gt;For large Dedicated SQL Pool tables, &lt;strong&gt;JDBC Synapse → Databricks&lt;/strong&gt; is how you burn cluster hours and patience.&lt;/p&gt;

&lt;p&gt;Prefer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Synapse CETAS → Parquet on ADLS Gen2 → Databricks Spark read → Delta in Unity Catalog&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  S[Synapse Dedicated SQL Pool] --&amp;gt;|CETAS| L[ADLS Gen2 Parquet]
  L --&amp;gt;|Spark read / Delta write| D[Unity Catalog Delta]
  S -.-&amp;gt;|JDBC slow path ❌| D&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  Why this path wins
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Synapse writes columnar Parquet in parallel (CETAS).&lt;/li&gt;
&lt;li&gt;ADLS is the shared landing zone both engines already understand.&lt;/li&gt;
&lt;li&gt;Databricks reads Parquet with strong scan performance, then writes &lt;strong&gt;Delta&lt;/strong&gt; (ACID, time travel, UC governance).&lt;/li&gt;
&lt;li&gt;You avoid row-by-row JDBC serialization.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example — CETAS from Synapse Dedicated SQL Pool
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- File format&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;EXTERNAL&lt;/span&gt; &lt;span class="n"&gt;FILE&lt;/span&gt; &lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;ParquetFF&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;FORMAT_TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PARQUET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DATA_COMPRESSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'org.apache.hadoop.io.compress.SnappyCodec'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Landing zone on ADLS Gen2&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;EXTERNAL&lt;/span&gt; &lt;span class="k"&gt;DATA&lt;/span&gt; &lt;span class="k"&gt;SOURCE&lt;/span&gt; &lt;span class="n"&gt;MigLanding&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;LOCATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://&amp;lt;storage&amp;gt;.dfs.core.windows.net/&amp;lt;container&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HADOOP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Export a bounded slice (always partition/filter on first runs)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;EXTERNAL&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fact_sales_export&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;LOCATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/migration/fact_sales/ds=2026-09-03/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DATA_SOURCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MigLanding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;FILE_FORMAT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ParquetFF&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;sales_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_nk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;product_nk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;business_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;net_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;quantity&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fact_sales&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;business_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example — land as Delta on Databricks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;

&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abfss://&amp;lt;container&amp;gt;@&amp;lt;storage&amp;gt;.dfs.core.windows.net/migration/fact_sales/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingested_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_timestamp&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="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overwrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overwriteSchema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveAsTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sales_uc.gold.fact_sales&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Physical design: do not cargo-cult Synapse
&lt;/h3&gt;

&lt;p&gt;In Synapse you spent years on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DISTRIBUTION = HASH(...)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ROUND_ROBIN&lt;/code&gt; / &lt;code&gt;REPLICATE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;clustered columnstore choices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On Databricks / Delta, those directives &lt;strong&gt;do not travel&lt;/strong&gt;. Mapping &lt;code&gt;HASH(customer_id)&lt;/code&gt; to a Delta &lt;strong&gt;partition&lt;/strong&gt; is a classic self-own: high-cardinality keys create tiny-file nightmares.&lt;/p&gt;

&lt;p&gt;Prefer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Liquid Clustering&lt;/strong&gt; / &lt;code&gt;CLUSTER BY AUTO&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;predictive optimization / compaction as an operating model&lt;/li&gt;
&lt;li&gt;partitions only for coarse, stable predicates (e.g. &lt;code&gt;business_date&lt;/code&gt;) when they truly help&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; Drop physical Synapse directives. Re-tune with lakehouse primitives after you have real query telemetry — not before.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  6) Code conversion: syntax is easy, semantics bite
&lt;/h2&gt;

&lt;p&gt;Automated conversion (Lakebridge transpile and friends) often handles a large share of syntax — commonly cited in the &lt;strong&gt;~80–90%&lt;/strong&gt; range for bulk translation. Your engineers live in the remaining &lt;strong&gt;10–20%&lt;/strong&gt;: cursors, dynamic SQL, weird error handling, and semantic mismatches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common remaps
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;T-SQL&lt;/th&gt;
&lt;th&gt;Databricks SQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GETDATE()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CURRENT_TIMESTAMP()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ISNULL(a, b)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;COALESCE(a, b)&lt;/code&gt; / &lt;code&gt;IFNULL(a, b)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LEN(s)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LENGTH(s)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CHARINDEX(sub, str)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LOCATE(sub, str)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT TOP 10 ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT ... LIMIT 10&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CONVERT(INT, col)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CAST(col AS INT)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Semantic trap #1 — case sensitivity
&lt;/h3&gt;

&lt;p&gt;Dedicated SQL Pools are often &lt;strong&gt;case-insensitive&lt;/strong&gt; by collation. Databricks SQL is &lt;strong&gt;case-sensitive&lt;/strong&gt; by default.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- May silently change results after migration&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;

&lt;span class="c1"&gt;-- Make intent explicit&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build this into reconciliation: pick string dimensions and compare distinct counts &lt;strong&gt;before&lt;/strong&gt; you argue about revenue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Semantic trap #2 — stored procedures
&lt;/h3&gt;

&lt;p&gt;Migrate &lt;strong&gt;structure first&lt;/strong&gt; (parameters, control flow, set-based DML). Optimize second.&lt;/p&gt;

&lt;p&gt;Rewrite candidates (do not pretend transpile “finished” them):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cursors / row-by-row loops&lt;/li&gt;
&lt;li&gt;heavy dynamic SQL&lt;/li&gt;
&lt;li&gt;Synapse-only performance hints&lt;/li&gt;
&lt;li&gt;temp-table choreography that exists only to fight distribution skew&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Semantic trap #3 — TRY/CATCH vs workflow design
&lt;/h3&gt;

&lt;p&gt;Simple audit logging often stays SQL-shaped. Multi-step compensation and downstream signaling usually belong in &lt;strong&gt;Lakeflow Jobs&lt;/strong&gt; (retries, quarantine tasks, alerting) rather than a 2,000-line proc that tries to be an orchestrator.&lt;/p&gt;




&lt;h2&gt;
  
  
  7) Dimensional patterns on Delta (SCD2)
&lt;/h2&gt;

&lt;p&gt;Slowly Changing Dimensions are where Synapse estates accumulate snowflake uniqueness. Your goal is not to clone the old proc line-for-line. Preserve the &lt;strong&gt;business rule&lt;/strong&gt;: history + current row queryability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example pattern — expire then insert (illustrative)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1) Expire changed current rows&lt;/span&gt;
&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;staging_customer&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_nk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_nk&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;segment&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;segment&lt;/span&gt;
  &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;valid_to&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;current_timestamp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;-- 2) Insert new current versions&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;customer_nk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_current&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_nk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;current_timestamp&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;staging_customer&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="n"&gt;ANTI&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_nk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_nk&lt;/span&gt;
 &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delta’s ACID behavior makes multi-step SCD flows safer than “hope the warehouse lock ordering holds.” Still: wrap critical loads in jobs with clear quarantine paths.&lt;/p&gt;




&lt;h2&gt;
  
  
  8) Reconciliation &amp;amp; cutover
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Budget reality:&lt;/strong&gt; validation often costs more calendar time than conversion.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Minimum reconcile pack (every wave)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Row counts&lt;/strong&gt; by business date / partition
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregates&lt;/strong&gt; — revenue, quantity, distinct business keys
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash / checksum&lt;/strong&gt; compares on ordered business-key sets
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tolerance checks&lt;/strong&gt; for floats / FX / rounded measures
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;String-dimension sanity&lt;/strong&gt; (case folding surprises)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Lakebridge &lt;strong&gt;Reconcile&lt;/strong&gt; supports structured comparison patterns across these dimensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cutover pattern that keeps executives calm
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Parallel run (Synapse + Databricks) for the lighthouse mart
&lt;/li&gt;
&lt;li&gt;Side-by-side Power BI validation with the business owner
&lt;/li&gt;
&lt;li&gt;Flip the dataset connection
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pause&lt;/strong&gt; Synapse compute (do not delete yet)
&lt;/li&gt;
&lt;li&gt;Keep rollback for a defined soak window
&lt;/li&gt;
&lt;li&gt;Only then decommission&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Early decommissioning is how “successful migrations” become weekend rollbacks.&lt;/p&gt;




&lt;h2&gt;
  
  
  9) Power BI / consumption cutover
&lt;/h2&gt;

&lt;p&gt;Technical remap is usually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Synapse endpoint → &lt;strong&gt;Databricks SQL Warehouse&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;refresh credentials / gateway assumptions&lt;/li&gt;
&lt;li&gt;validate DAX measures against reconcile packs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Political remap is harder:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who signs off KPI parity?&lt;/li&gt;
&lt;li&gt;which report is the source of truth during dual-run?&lt;/li&gt;
&lt;li&gt;what is the communication plan when numbers differ by 0.3% due to case folding or timezone?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat consumption as a &lt;strong&gt;first-class workstream&lt;/strong&gt; with named business owners, not a Friday afternoon connection string change.&lt;/p&gt;




&lt;h2&gt;
  
  
  10) 30 / 60 / 90 plan
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Days 1–30 — foundations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Run Lakebridge Profiler + Analyzer on Dedicated pools&lt;/li&gt;
&lt;li&gt;[ ] Cut dead objects from scope (document why)&lt;/li&gt;
&lt;li&gt;[ ] Choose one lighthouse: pipeline → gold mart → one Power BI dataset&lt;/li&gt;
&lt;li&gt;[ ] Stand up Unity Catalog layout + storage conventions&lt;/li&gt;
&lt;li&gt;[ ] Define job naming, environments, and CI basics&lt;/li&gt;
&lt;li&gt;[ ] Write the reconcile notebook/job v0&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 31–60 — prove the path
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] CETAS → ADLS → Delta for large facts (no JDBC heroics)&lt;/li&gt;
&lt;li&gt;[ ] Transpile + hand-fix the top procs for the lighthouse&lt;/li&gt;
&lt;li&gt;[ ] Repoint one BI workspace to Databricks SQL Warehouse&lt;/li&gt;
&lt;li&gt;[ ] Parallel run + signed business acceptance&lt;/li&gt;
&lt;li&gt;[ ] Capture runbooks: failure modes, quarantine, reruns&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Days 61–90 — scale without drama
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Wave 2/3 domains with the same pack&lt;/li&gt;
&lt;li&gt;[ ] Orchestration parity on Lakeflow Jobs&lt;/li&gt;
&lt;li&gt;[ ] Enablement plan for Synapse-native engineers&lt;/li&gt;
&lt;li&gt;[ ] Cost review (warehouse sizing vs old pool bills)&lt;/li&gt;
&lt;li&gt;[ ] Only then: formal Synapse decommission proposal&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  11) Anti-patterns I keep seeing
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Transpile-first, assess-never&lt;/strong&gt; — migrating abandoned procs with confidence.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JDBC for multi-billion-row facts&lt;/strong&gt; — slow, expensive, fragile.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HASH key → Delta partition&lt;/strong&gt; — tiny files, sad Spark UI.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Big-bang BI cutover&lt;/strong&gt; — no dual-run, no owner, no soak.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recreating every Synapse knob&lt;/strong&gt; — preserving constraints without preserving reasons.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping enablement&lt;/strong&gt; — platform is fine; team still thinks in distributions.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decommission on day one of “green”&lt;/strong&gt; — optimism is not a rollback strategy.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  12) Field rules I will not compromise on
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid &amp;gt; pure lift-and-shift &amp;gt; big-bang rewrite&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assessment before conversion&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ADLS + Parquet/Delta over JDBC for big moves&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not map Synapse HASH keys to partitions&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconcile every wave&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not decommission Synapse early&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enablement is a workstream&lt;/strong&gt;, not a lunch-and-learn
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migrate outcomes (trusted KPIs), not just objects&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  13) FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Should we move to Fabric instead of Databricks?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A: Different target, overlapping Azure story. This article assumes a &lt;strong&gt;Databricks Lakehouse&lt;/strong&gt; landing zone. If Fabric is in play, treat that as an explicit architecture decision — do not accidentally run two migrations under one slogan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can we keep Synapse Serverless for “just a few views”?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A: Sometimes as a bridge. Long-term dual query planes usually recreate the governance tax you are trying to escape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How perfect must automated conversion be?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A: Good enough to move bulk syntax. Your senior engineers should own semantic diffs, SCD logic, and orchestration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What is the first metric of a healthy pilot?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A: Not “job duration.” It is &lt;strong&gt;signed KPI parity&lt;/strong&gt; with a business owner plus a rerun runbook a junior can execute.&lt;/p&gt;




&lt;h2&gt;
  
  
  14) Sources
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Databricks — &lt;em&gt;Navigating a Synapse Migration to Databricks&lt;/em&gt;
&lt;a href="https://www.databricks.com/blog/navigating-synapse-migration-databricks" rel="noopener noreferrer"&gt;https://www.databricks.com/blog/navigating-synapse-migration-databricks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Databricks — &lt;em&gt;Introducing Lakebridge&lt;/em&gt;
&lt;a href="https://www.databricks.com/blog/introducing-lakebridge-free-open-data-migration-databricks-sql" rel="noopener noreferrer"&gt;https://www.databricks.com/blog/introducing-lakebridge-free-open-data-migration-databricks-sql&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lakebridge docs — Synapse Profiler
&lt;a href="https://databrickslabs.github.io/lakebridge/docs/assessment/profiler/synapse/" rel="noopener noreferrer"&gt;https://databrickslabs.github.io/lakebridge/docs/assessment/profiler/synapse/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lakebridge docs — Transpile
&lt;a href="https://databrickslabs.github.io/lakebridge/docs/transpile/" rel="noopener noreferrer"&gt;https://databrickslabs.github.io/lakebridge/docs/transpile/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Practical large-table movement discussion (CETAS → ADLS → Delta)
&lt;a href="https://medium.com/@prabhakarankanniappan/copy-3-5-billion-rows-from-synapse-to-databricks-in-30-minutes-fda15e0ffca0" rel="noopener noreferrer"&gt;https://medium.com/@prabhakarankanniappan/copy-3-5-billion-rows-from-synapse-to-databricks-in-30-minutes-fda15e0ffca0&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Suggested LinkedIn teaser (after you publish on DEV.to)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Synapse → Databricks is not a notebook move.&lt;br&gt;&lt;br&gt;
It’s Dedicated SQL Pools + pipelines + T-SQL semantics + Power BI trust.&lt;br&gt;&lt;br&gt;
I wrote a practical playbook with diagrams, CETAS→Delta examples, reconcile guidance, and a 30/60/90.&lt;br&gt;&lt;br&gt;
Link: &amp;lt;your DEV.to URL&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Firat Celik — Lead Data Engineer · Azure Synapse · Microsoft Fabric · Databricks · ETL/ELT · Power BI&lt;/em&gt;&lt;/p&gt;

</description>
      <category>databricks</category>
      <category>azure</category>
      <category>dataengineering</category>
      <category>etl</category>
    </item>
    <item>
      <title># go-intake: Go-Native Streaming Data Ingestion Toolkit</title>
      <dc:creator>Firat Celik</dc:creator>
      <pubDate>Tue, 09 Jun 2026 10:01:42 +0000</pubDate>
      <link>https://dev.to/firfircelik/go-intake-v010-a-small-go-library-for-messy-data-intake-3hko</link>
      <guid>https://dev.to/firfircelik/go-intake-v010-a-small-go-library-for-messy-data-intake-3hko</guid>
      <description>&lt;h2&gt;
  
  
  Executive Summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;go-intake&lt;/strong&gt; is a minimalist, streaming-first ETL toolkit for Go developers that transforms messy data into validated, record-oriented output. With zero third-party dependencies and a focus on composition over configuration, it provides enterprise-grade data pipeline capabilities in a single, embeddable library.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core Abstraction Layers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Interface&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3-method interface (&lt;code&gt;Open&lt;/code&gt;, &lt;code&gt;Read&lt;/code&gt;, &lt;code&gt;Close&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Data ingestion from CSV, JSONL&lt;/td&gt;
&lt;td&gt;Stream until EOF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transformer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Apply(ctx, Record) (Record, error)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Field normalization, type parsing&lt;/td&gt;
&lt;td&gt;Immutable, returns copy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Validator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Validate(ctx, Record) error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Schema enforcement, business rules&lt;/td&gt;
&lt;td&gt;Read-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Quarantine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Write(ctx, InvalidRecord)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rejected record capture with context&lt;/td&gt;
&lt;td&gt;Structured error logging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sink&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3-method interface (&lt;code&gt;Open&lt;/code&gt;, &lt;code&gt;Write&lt;/code&gt;, &lt;code&gt;Close&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Output to CSV, JSONL&lt;/td&gt;
&lt;td&gt;Streaming write&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Design Principles
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┐    ┌──────────────┐    ┌─────────────┐
│   Source    │───▶│ Transformers │───▶│ Validators  │
└─────────────┘    └──────────────┘    └─────────────┘
                                            │
                               ┌────────────┴──────────┐
                               ▼                       ▼
                         ┌──────────┐           ┌───────────┐
                         │   Sink   │           │Quarantine  │
                         └──────────┘           └───────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Memory Usage Comparison
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Memory (MB)
   500 │                      
       │    ●●●●●●●●●●●●●●●●●●  go-intake (constant)
       │                      
   100 │                      
       │                      
    50 │    ████ goflow grows with dataset
       │    ██████            
    25 │    ████████████      
       │    ████████████████  streamz grows
       │                       
     0 └──────────────────────
        0K    50K   100K   150K
           Records Processed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Performance Benchmarks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Throughput Analysis
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dataset Size&lt;/th&gt;
&lt;th&gt;Records Processed&lt;/th&gt;
&lt;th&gt;Time Elapsed&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small (10K)&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;~40ms&lt;/td&gt;
&lt;td&gt;~250,000/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium (100K)&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;~410ms&lt;/td&gt;
&lt;td&gt;~243,902/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large (161K)&lt;/td&gt;
&lt;td&gt;161,568&lt;/td&gt;
&lt;td&gt;643ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;251,000/sec&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Memory Efficiency:&lt;/strong&gt; The streaming model ensures constant memory usage regardless of dataset size. Only one record exists in memory at any point during processing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparative Performance
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph LR
    A[go-intake&amp;lt;br/&amp;gt;251K/s] --&amp;gt; B[Memory: O(1)]
    C[Gopherize&amp;lt;br/&amp;gt;125K/s] --&amp;gt; D[Memory: O(n)]
    E[goflow&amp;lt;br/&amp;gt;180K/s] --&amp;gt; F[Memory: O(n)]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Performance Scaling Chart
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Records/sec
   300K │
        │
   250K │    ● (go-intake: 251K)
        │   ╱
   200K │  ╱
        │ ╱
   150K │● (goflow: 180K)
        │
   100K │● (streamz)
        │
    50K │● (Gopherize: 125K)
        │
     0K └─────────────────────
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Feature Matrix vs Competition
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;go-intake&lt;/th&gt;
&lt;th&gt;Gopherize&lt;/th&gt;
&lt;th&gt;goflow&lt;/th&gt;
&lt;th&gt;streamz&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;✅ Zero Dependencies&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✅ Streaming Model&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✅ Structured Errors&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✅ Multi-validation Errors&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✅ Schema Discovery&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✅ Header Normalization&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✅ Quarantine Sinks&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✅ Context Cancellation&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines of Code&lt;/td&gt;
&lt;td&gt;~2,300&lt;/td&gt;
&lt;td&gt;~8,500&lt;/td&gt;
&lt;td&gt;~12,000&lt;/td&gt;
&lt;td&gt;~6,200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Bundled Components
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSVSource&lt;/strong&gt;: RFC 4180 compliant, configurable delimiter, empty line skipping&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSONLSource&lt;/strong&gt;: Newline-delimited JSON, 16MB line buffer support&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Sinks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSVSink&lt;/strong&gt;: Automatic header generation, deterministic column ordering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSONLSink&lt;/strong&gt;: HTML-escaping disabled, one object per line&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Transformers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NormalizeHeaders(style)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Case normalization&lt;/td&gt;
&lt;td&gt;Column standardization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TrimStrings()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whitespace removal&lt;/td&gt;
&lt;td&gt;Data cleaning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ParseFloat/Int/Bool/Date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Type conversion&lt;/td&gt;
&lt;td&gt;Schema enforcement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Rename/Drop/Keep/Copy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Schema evolution&lt;/td&gt;
&lt;td&gt;ETL pipeline stages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AddField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Constant injection&lt;/td&gt;
&lt;td&gt;Metadata enrichment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MapField&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Custom transformations&lt;/td&gt;
&lt;td&gt;Business logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Validators
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Error Aggregation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Required(field)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Presence&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Min/Max/Between&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Numeric range&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Regex(field, pattern)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pattern matching&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Enum(field, values...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allowlist&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Email/URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Format validation&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NotFuture(field)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Temporal constraint&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Real-World Validation Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Dataset 1: COVID-19 Aggregated Data (161,568 records)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Field Profiling Results:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Confidence&lt;/th&gt;
&lt;th&gt;Null %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Confirmed&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Country&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date&lt;/td&gt;
&lt;td&gt;date&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deaths&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recovered&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;ul&gt;
&lt;li&gt;Read: 161,568&lt;/li&gt;
&lt;li&gt;Written: 161,568&lt;/li&gt;
&lt;li&gt;Invalid: 0&lt;/li&gt;
&lt;li&gt;Failed: 0&lt;/li&gt;
&lt;li&gt;Runtime: 643ms&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Dataset 2: Synthetic Messy Data (5,000 records with 2-5% error injection)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Statistical Validation:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total Records&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Valid Output&lt;/td&gt;
&lt;td&gt;4,772 (95%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quarantined&lt;/td&gt;
&lt;td&gt;228 (4.56%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error Detection&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Error Distribution in Quarantine:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error Type&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Distribution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;td&gt;96&lt;/td&gt;
&lt;td&gt;Missing required fields&lt;/td&gt;
&lt;td&gt;████░░░░░░ (29%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;min&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;Negative numeric values&lt;/td&gt;
&lt;td&gt;██░░░░░░░░ (18%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;regex&lt;/td&gt;
&lt;td&gt;172&lt;/td&gt;
&lt;td&gt;Email format mismatch&lt;/td&gt;
&lt;td&gt;██████░░░░ (53%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;326&lt;/td&gt;
&lt;td&gt;Captured validation errors&lt;/td&gt;
&lt;td&gt;██████████ (100%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Data Migration Pipelines
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;intake&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CSV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"legacy_export.csv"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NormalizeHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SnakeCase&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"normalized.jsonl"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. API Data Validation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;intake&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api_incoming.jsonl"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimStrings&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"website"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"age"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;18&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;OnInvalid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quarantine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rejected.jsonl"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CSV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"clean.csv"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. ETL for Analytics
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;intake&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CSV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"raw_events.csv"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NormalizeHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SnakeCase&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RFC3339&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"event_id"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;OnInvalid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quarantine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bad_events.jsonl"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
    &lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"analytics_ready.jsonl"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Data Quality Inspection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CSV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unknown_dataset.csv"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;discover&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InspectSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;discover&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;SampleSize&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fields&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s: %s (confidence %.2f)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TypeConfidence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Issues&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[%s] %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Technical Specifications
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Specification&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Go Version&lt;/td&gt;
&lt;td&gt;1.23+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies&lt;/td&gt;
&lt;td&gt;Zero (stdlib only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory Model&lt;/td&gt;
&lt;td&gt;Streaming (O(1))&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency&lt;/td&gt;
&lt;td&gt;Context-aware cancellation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test Coverage&lt;/td&gt;
&lt;td&gt;100% on public API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Race Detector&lt;/td&gt;
&lt;td&gt;Clean&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Non-Goals (Explicit Design Decisions)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;❌ CLI/REPL interface (library-first)&lt;/li&gt;
&lt;li&gt;❌ DAG engine or scheduler&lt;/li&gt;
&lt;li&gt;❌ Distributed execution&lt;/li&gt;
&lt;li&gt;❌ DataFrame abstraction&lt;/li&gt;
&lt;li&gt;❌ Airflow/Airbyte clone&lt;/li&gt;
&lt;li&gt;❌ Connector marketplace&lt;/li&gt;
&lt;li&gt;❌ PDF/ML features&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/firfircelik/go-intake
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Quality Gate
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 ./...
go vet ./...
go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-race&lt;/span&gt; &lt;span class="nt"&gt;-count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All tests pass. Race detector clean. Zero external dependencies.&lt;/p&gt;

</description>
      <category>etl</category>
      <category>dataengineering</category>
      <category>go</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Introducing oteldoctor: a production-readiness analyzer for OpenTelemetry Collector configs</title>
      <dc:creator>Firat Celik</dc:creator>
      <pubDate>Fri, 22 May 2026 14:28:36 +0000</pubDate>
      <link>https://dev.to/firfircelik/introducing-oteldoctor-a-production-readiness-analyzer-for-opentelemetry-collector-configs-44ji</link>
      <guid>https://dev.to/firfircelik/introducing-oteldoctor-a-production-readiness-analyzer-for-opentelemetry-collector-configs-44ji</guid>
      <description>&lt;p&gt;I’ve just released &lt;strong&gt;oteldoctor v0.1.0&lt;/strong&gt;, an open-source Go CLI that analyzes OpenTelemetry Collector configurations before they reach production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;OpenTelemetry Collector configs often start small.&lt;/p&gt;

&lt;p&gt;Then they grow.&lt;/p&gt;

&lt;p&gt;A few receivers.&lt;br&gt;
A few processors.&lt;br&gt;
A couple of exporters.&lt;br&gt;
Some Kubernetes manifests.&lt;br&gt;
A little bit of batching.&lt;br&gt;
A debug exporter during testing.&lt;br&gt;
A quick HTTP endpoint.&lt;br&gt;
A few attributes added for convenience.&lt;/p&gt;

&lt;p&gt;Eventually, that YAML file becomes production-critical infrastructure.&lt;/p&gt;

&lt;p&gt;The problem is that a Collector config can be valid YAML, and even acceptable to the Collector, while still being risky for production.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;telemetry may be dropped when exporters fail&lt;/li&gt;
&lt;li&gt;the Collector may restart during memory spikes&lt;/li&gt;
&lt;li&gt;debug endpoints may be exposed&lt;/li&gt;
&lt;li&gt;hardcoded secrets may leak into version control&lt;/li&gt;
&lt;li&gt;high-cardinality attributes may increase observability cost&lt;/li&gt;
&lt;li&gt;service identity may be inconsistent across environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the gap oteldoctor tries to fill.&lt;/p&gt;
&lt;h2&gt;
  
  
  What oteldoctor checks
&lt;/h2&gt;

&lt;p&gt;oteldoctor analyzes Collector configs across six categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Structural&lt;/td&gt;
&lt;td&gt;Undefined references, unused components, empty pipelines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reliability&lt;/td&gt;
&lt;td&gt;Missing &lt;code&gt;memory_limiter&lt;/code&gt;, missing &lt;code&gt;batch&lt;/code&gt;, retry/queue gaps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Plain HTTP, hardcoded secrets, exposed debug endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost / Cardinality&lt;/td&gt;
&lt;td&gt;High-cardinality dimensions, missing sampling, debug in production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic Quality&lt;/td&gt;
&lt;td&gt;Deprecated attributes, missing service identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes Readiness&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GOMEMLIMIT&lt;/code&gt;, resource limits, probes, exposure risks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Example usage
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;oteldoctor analyze ./deploy &lt;span class="nt"&gt;--profile&lt;/span&gt; production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Generate SARIF for GitHub Code Scanning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;oteldoctor analyze ./deploy &lt;span class="nt"&gt;--profile&lt;/span&gt; production &lt;span class="nt"&gt;--format&lt;/span&gt; sarif &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; oteldoctor.sarif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Render the Collector pipeline as a graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;oteldoctor graph collector.yaml &lt;span class="nt"&gt;--format&lt;/span&gt; mermaid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Explain a rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;oteldoctor explain OTEL-SEC-202
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/firfircelik/oteldoctor/cmd/oteldoctor@v0.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it is not
&lt;/h2&gt;

&lt;p&gt;oteldoctor does not replace the OpenTelemetry Collector’s own configuration validation.&lt;/p&gt;

&lt;p&gt;The Collector can tell you whether a config is syntactically valid and operationally acceptable.&lt;/p&gt;

&lt;p&gt;oteldoctor focuses on production readiness: reliability, security, cost/cardinality, semantic convention quality, and Kubernetes deployment risks.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/firfircelik/oteldoctor" rel="noopener noreferrer"&gt;https://github.com/firfircelik/oteldoctor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the first public release. I’d love feedback from anyone using OpenTelemetry Collector, especially around real-world configs and rule calibration.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>go</category>
      <category>monitoring</category>
      <category>showdev</category>
    </item>
    <item>
      <title>LibKill: Scan Your Machine for Compromised npm, pip, and Bun Packages</title>
      <dc:creator>Firat Celik</dc:creator>
      <pubDate>Wed, 13 May 2026 10:59:45 +0000</pubDate>
      <link>https://dev.to/firfircelik/libkill-scan-your-machine-for-compromised-npm-pip-and-bun-packages-4f68</link>
      <guid>https://dev.to/firfircelik/libkill-scan-your-machine-for-compromised-npm-pip-and-bun-packages-4f68</guid>
      <description>&lt;p&gt;Over the last few weeks, supply chain security has once again become a very real problem for developers.&lt;/p&gt;

&lt;p&gt;We keep seeing the same attack pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CI/CD token stolen
→ malicious package version published
→ developers install or update dependencies
→ compromised code lands on real machines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the worst part is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You may already have one of those compromised packages on your machine without knowing it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is why I built &lt;strong&gt;LibKill&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is LibKill?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;LibKill&lt;/strong&gt; is an open-source supply chain security scanner.&lt;/p&gt;

&lt;p&gt;It checks your local development environment against a database of known compromised package artifacts.&lt;/p&gt;

&lt;p&gt;Currently, it scans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm global packages&lt;/li&gt;
&lt;li&gt;pip packages&lt;/li&gt;
&lt;li&gt;Bun cache&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then it cross-references what it finds against &lt;strong&gt;2,672+ known compromised package artifacts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If something suspicious is detected, LibKill can help you remove it interactively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;Most developers hear about compromised packages after the damage is already done.&lt;/p&gt;

&lt;p&gt;A package gets reported.&lt;br&gt;&lt;br&gt;
A malicious version is removed.&lt;br&gt;&lt;br&gt;
A GitHub advisory appears.&lt;br&gt;&lt;br&gt;
A security company publishes an analysis.&lt;/p&gt;

&lt;p&gt;But then comes the real question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Is any of this already on my machine?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question should be easy to answer.&lt;/p&gt;

&lt;p&gt;LibKill is my attempt to make that check fast, local, and simple.&lt;/p&gt;
&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;p&gt;LibKill currently supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scanning npm global packages&lt;/li&gt;
&lt;li&gt;Scanning installed pip packages&lt;/li&gt;
&lt;li&gt;Scanning Bun cache&lt;/li&gt;
&lt;li&gt;Cross-checking against known compromised artifacts&lt;/li&gt;
&lt;li&gt;Interactive removal when threats are found&lt;/li&gt;
&lt;li&gt;Background daemon mode&lt;/li&gt;
&lt;li&gt;Desktop notifications&lt;/li&gt;
&lt;li&gt;macOS, Linux, and Windows support&lt;/li&gt;
&lt;li&gt;Single binary&lt;/li&gt;
&lt;li&gt;Zero runtime dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is written in Go and distributed as a small standalone binary.&lt;/p&gt;
&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;You can install it with one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/firfircelik/libkill/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or build it manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/firfircelik/libkill.git
&lt;span class="nb"&gt;cd &lt;/span&gt;libkill
make &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example usage
&lt;/h2&gt;

&lt;p&gt;After installation, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;libkill scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To start background monitoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;libkill daemon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If compromised artifacts are found, LibKill will show the results and offer interactive cleanup options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threat coverage
&lt;/h2&gt;

&lt;p&gt;LibKill currently covers known compromised artifacts related to campaigns and threat clusters such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mini Shai-Hulud&lt;/li&gt;
&lt;li&gt;Contagious Interview&lt;/li&gt;
&lt;li&gt;StegaBin&lt;/li&gt;
&lt;li&gt;SANDWORM_MODE&lt;/li&gt;
&lt;li&gt;CanisterWorm&lt;/li&gt;
&lt;li&gt;W4SP&lt;/li&gt;
&lt;li&gt;TeamPCP&lt;/li&gt;
&lt;li&gt;and many others&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The threat database is built from public security sources including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Socket.dev&lt;/li&gt;
&lt;li&gt;GitHub Advisory Database&lt;/li&gt;
&lt;li&gt;OSV.dev&lt;/li&gt;
&lt;li&gt;Public security research&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why local scanning matters
&lt;/h2&gt;

&lt;p&gt;A lot of supply chain security tools focus on repositories, lockfiles, or CI pipelines.&lt;/p&gt;

&lt;p&gt;That is useful, but it does not fully answer what is already installed locally.&lt;/p&gt;

&lt;p&gt;Developers often install tools globally.&lt;/p&gt;

&lt;p&gt;They test packages.&lt;/p&gt;

&lt;p&gt;They use temporary scripts.&lt;/p&gt;

&lt;p&gt;They work across multiple languages and package managers.&lt;/p&gt;

&lt;p&gt;They forget what was installed weeks ago.&lt;/p&gt;

&lt;p&gt;A compromised package does not need to be in your current production repository to be a problem.&lt;/p&gt;

&lt;p&gt;It may already exist somewhere in your local environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Language: Go&lt;/li&gt;
&lt;li&gt;License: MIT&lt;/li&gt;
&lt;li&gt;Binary size: around 14 MB&lt;/li&gt;
&lt;li&gt;Dependencies: zero runtime dependencies&lt;/li&gt;
&lt;li&gt;Platforms: macOS, Linux, Windows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/firfircelik/libkill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final note
&lt;/h2&gt;

&lt;p&gt;LibKill is still evolving.&lt;/p&gt;

&lt;p&gt;My goal is to make it a practical local security tool for developers who work with open-source packages every day.&lt;/p&gt;

&lt;p&gt;If you work with npm, Python, Bun, or modern JavaScript tooling, you should not have to guess whether a known compromised artifact exists on your machine.&lt;/p&gt;

&lt;p&gt;You should be able to check it quickly.&lt;/p&gt;

&lt;p&gt;That is what LibKill is for.&lt;/p&gt;

&lt;p&gt;Feedback, issues, and contributions are welcome.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/firfircelik/libkill/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>security</category>
      <category>npm</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>How do you monitor third-party service outages?</title>
      <dc:creator>Firat Celik</dc:creator>
      <pubDate>Tue, 12 May 2026 09:35:59 +0000</pubDate>
      <link>https://dev.to/firfircelik/how-do-you-monitor-third-party-service-outages-2cp</link>
      <guid>https://dev.to/firfircelik/how-do-you-monitor-third-party-service-outages-2cp</guid>
      <description>&lt;p&gt;I’ve been thinking about a simple but painful problem:&lt;/p&gt;

&lt;p&gt;When something breaks in production, how do you know if the issue is your own app or a third-party service?&lt;/p&gt;

&lt;p&gt;Many apps today depend on services like AWS, GitHub, Vercel, Stripe, OpenAI, Anthropic, Cloudflare, Supabase, Firebase, Resend, SendGrid and many others.&lt;/p&gt;

&lt;p&gt;If one of them has an outage or degraded performance, users usually do not care whose fault it is.&lt;/p&gt;

&lt;p&gt;They just think your app is broken.&lt;/p&gt;

&lt;p&gt;I built a small iOS app called &lt;strong&gt;StatusWatch&lt;/strong&gt; to help with this.&lt;/p&gt;

&lt;p&gt;It monitors 80+ public status pages and sends push alerts when services go down or degrade.&lt;/p&gt;

&lt;p&gt;It is not meant to replace full observability tools.&lt;/p&gt;

&lt;p&gt;It is more like a simple dependency health dashboard for developers, indie hackers and small SaaS teams.&lt;/p&gt;

&lt;p&gt;The main question it tries to answer is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Is one of the services my app depends on having an incident right now?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I would love feedback from other developers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you currently monitor third-party status pages?&lt;/li&gt;
&lt;li&gt;Would push alerts be useful?&lt;/li&gt;
&lt;li&gt;Which services should be added?&lt;/li&gt;
&lt;li&gt;Should this stay simple, or also include uptime monitoring?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Website: &lt;a href="https://statuswatch.tech" rel="noopener noreferrer"&gt;https://statuswatch.tech&lt;/a&gt;&lt;br&gt;
App Store: &lt;a href="https://apps.apple.com/us/app/statuswatch-service-status/id6761889971" rel="noopener noreferrer"&gt;https://apps.apple.com/us/app/statuswatch-service-status/id6761889971&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>saas</category>
      <category>mobile</category>
      <category>wecoded</category>
    </item>
    <item>
      <title>How I Monitor 80+ Cloud Services in Real-Time (And Get Notified Before My Users Do)</title>
      <dc:creator>Firat Celik</dc:creator>
      <pubDate>Sun, 26 Apr 2026 11:30:20 +0000</pubDate>
      <link>https://dev.to/firfircelik/-how-i-monitor-80-cloud-services-in-real-time-and-get-notified-before-my-users-do-224d</link>
      <guid>https://dev.to/firfircelik/-how-i-monitor-80-cloud-services-in-real-time-and-get-notified-before-my-users-do-224d</guid>
      <description>&lt;p&gt;&lt;em&gt;A deep dive into building StatusWatch; push notifications for service outages, built with Flutter and a real time status aggregation backend.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The moment that started it all
&lt;/h2&gt;

&lt;p&gt;It was a Friday evening. A client messaged me: &lt;em&gt;"The checkout isn't working."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Stripe had been down for 38 minutes. I had no idea.&lt;/p&gt;

&lt;p&gt;I did what most developers do opened five browser tabs, checked Stripe's status page, scrolled Twitter looking for complaints. By the time I confirmed the issue and messaged the client back, 45 minutes had passed.&lt;/p&gt;

&lt;p&gt;That was embarrassing. More importantly, it was preventable.&lt;/p&gt;

&lt;p&gt;I started looking for a mobile app that would just... tell me when my stack breaks. Something that monitors the services I actually use and sends a push notification the moment something goes wrong. I couldn't find exactly what I wanted, so I built it.&lt;/p&gt;

&lt;p&gt;This is the story of how StatusWatch works under the hood.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core problem: status pages are not built for developers
&lt;/h2&gt;

&lt;p&gt;Every major cloud provider publishes a status page. AWS has one. GitHub has one. Stripe has one. They're public, they're (usually) accurate, and almost nobody checks them proactively.&lt;/p&gt;

&lt;p&gt;Why? Because checking 10 different status pages manually every time something feels off is not a workflow. It's a punishment.&lt;/p&gt;

&lt;p&gt;The real need is simple: &lt;strong&gt;when something in my stack breaks, I want to know immediately not when a user tells me.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;StatusWatch solves this by polling the official status APIs of 80+ services, normalizing the data into a unified format, and pushing a notification to your phone the moment an incident is detected.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture overview
&lt;/h2&gt;

&lt;p&gt;At a high level, the system has three layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Status APIs] → [Aggregation Backend] → [Flutter Mobile App]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let me walk through each one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 1: Polling the status APIs
&lt;/h2&gt;

&lt;p&gt;Most major services expose their status data in one of two formats:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Atlassian Statuspage&lt;/strong&gt; (the most common) used by GitHub, Stripe, Vercel, Cloudflare, Supabase, Linear, and dozens more. The API returns a structured JSON response with components, incidents, and scheduled maintenances.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://www.githubstatus.com/api/v2/summary.json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simplified response looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"indicator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"minor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Minor Service Outage"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"components"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Git Operations"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"degraded_performance"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"API Requests"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"operational"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"incidents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Increased error rates on Git push"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"investigating"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-04-10T14:22:00Z"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Custom status APIs&lt;/strong&gt; — services like AWS, Google Cloud, and Azure have their own formats. AWS Health Dashboard, for example, returns RSS/Atom feeds per region and service. These require individual parsers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The normalization challenge&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each service returns data in a slightly different shape. To display everything uniformly in the app, I normalize every response into a shared schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"service_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GitHub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"overall_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"degraded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"components"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"active_incidents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_checked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-04-26T09:15:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This normalization layer is what makes it possible to show a consistent green/yellow/red dashboard regardless of which service you're looking at.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 2: Detecting state changes and triggering notifications
&lt;/h2&gt;

&lt;p&gt;Polling alone isn't enough. The key logic is &lt;strong&gt;detecting when a service's status changes&lt;/strong&gt; — from operational to degraded, from degraded to outage, and back to operational.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Previous state: operational
Current state:  degraded
→ Trigger notification: "GitHub is experiencing degraded performance"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Previous state: degraded  
Current state:  operational
→ Trigger notification: "GitHub is back to normal ✓"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each service's last known state is stored server-side. On every polling cycle, the new state is compared against the stored state. If they differ, a notification is queued.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polling intervals&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not all services are polled at the same frequency. The free tier uses a 5-minute polling interval. Pro users get near-instant detection — polling runs every 60 seconds, and for major services like AWS, GitHub, and Stripe, we watch for webhook-based updates where available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push notifications&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Notifications are delivered via FCM (Firebase Cloud Messaging) for Android and APNs for iOS. When a state change is detected, the backend sends a targeted push to all users who have that service in their watchlist.&lt;/p&gt;

&lt;p&gt;The notification payload is minimal and actionable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;⚠️ Stripe — Incident Detected&lt;/span&gt;
&lt;span class="na"&gt;Body&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Payment API is experiencing elevated error rates&lt;/span&gt;
&lt;span class="na"&gt;Tap&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Opens incident timeline in StatusWatch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Layer 3: The Flutter app
&lt;/h2&gt;

&lt;p&gt;The mobile app is built with Flutter, which gives us a single codebase for iOS and Android with native performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dashboard&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The main screen is a flat list of your followed services, each showing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Service name and logo&lt;/li&gt;
&lt;li&gt;Current status (green dot, yellow dot, red dot)&lt;/li&gt;
&lt;li&gt;Active incident summary if one exists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The status colors follow a simple convention: green = operational, yellow = degraded performance, red = major outage or partial outage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incident timeline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tapping any service opens its incident timeline a chronological feed of status updates from "Investigating" through "Identified", "Monitoring", and finally "Resolved". This is pulled directly from the status API and gives you the full picture without opening a browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uptime heatmap (Pro)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For Pro users, each service shows a 90-day uptime heatmap — a grid where each cell represents one day, colored by uptime percentage. This gives a quick visual sense of a service's reliability history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State management&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The app uses Riverpod for state management. Each service's status is a stream that updates when the backend pushes new data. The UI rebuilds reactively — no manual refresh needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The services we monitor
&lt;/h2&gt;

&lt;p&gt;One of the most time-consuming parts of building StatusWatch was the initial service integration work. Not every service has a clean, documented status API. Here's a breakdown of the current coverage:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Services&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;AWS, GCP, Azure, DigitalOcean, Hetzner, Fly.io, Railway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DevOps&lt;/td&gt;
&lt;td&gt;GitHub, GitLab, CircleCI, Render, Linear, Jira&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Vercel, Netlify, Heroku, Cloudflare, Firebase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Supabase, MongoDB Atlas, Neon, Redis Cloud, Upstash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Stripe, Paddle, Twilio, SendGrid, Resend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring&lt;/td&gt;
&lt;td&gt;Datadog, Sentry, Grafana Cloud, PagerDuty, New Relic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;OpenAI, Anthropic, Hugging Face, Together AI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;80+ services total, and the list grows with every release.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned building this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Status APIs are inconsistently documented.&lt;/strong&gt; Some services have beautifully documented status APIs. Others you have to reverse engineer from their status page's network requests. A few don't expose machine-readable status data at all, which means falling back to HTML scraping fragile, but sometimes the only option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push notification timing is everything.&lt;/strong&gt; A notification that arrives 20 minutes late is almost useless. The entire value proposition is knowing &lt;em&gt;before&lt;/em&gt; your users do. This made the polling architecture a first class concern from day one, not an afterthought.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Normalization is harder than it looks.&lt;/strong&gt; "Degraded performance" on Stripe is not the same operational severity as "degraded performance" on GitHub. Building a useful unified model required understanding each service's incident taxonomy not just their API shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free tier design matters for conversion.&lt;/strong&gt; The 5-service limit on the free tier is intentional. Most solo developers track 5–8 services. Hitting that limit naturally creates the moment where a user thinks "I need one more" which is where the Pro upgrade conversation starts.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;A few things on the roadmap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Android widgets&lt;/strong&gt; glanceable status on your home screen without opening the app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More services&lt;/strong&gt; always expanding the list based on user requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom webhook endpoints&lt;/strong&gt; for services not on the official list&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team accounts&lt;/strong&gt; shared watchlists and notification routing for small DevOps teams&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;StatusWatch is free to download. The free tier covers 5 services with 5-minute notification delays enough to see if it fits your workflow before committing to Pro.&lt;/p&gt;

&lt;p&gt;If your stack touches AWS, Stripe, Supabase, OpenAI, or Vercel, you'll probably want to know about outages before your users do.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apps.apple.com/us/app/statuswatch-service-monitor/id6761889971" rel="noopener noreferrer"&gt;&lt;strong&gt;[Download StatusWatch on the App Store]&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by a Firat Celik. Feedback welcome  drop a comment or find me on X [@Bi9x6].&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: flutter, devops, sre, devtools, buildinpublic, mobile, backend, cloudinfrastructure&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>api</category>
      <category>fullstack</category>
    </item>
  </channel>
</rss>
