<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged data-engineering</title>
<link>https://coderlegion.com/tag/data-engineering</link>
<description>Powered by Question2Answer</description>
<item>
<title>Auto CDF Moved the CDC Tax From Writes to Reads: Delta CDF vs Auto CDF vs Spark 4.2 CHANGES</title>
<link>https://coderlegion.com/32478/auto-cdf-moved-the-cdc-tax-from-writes-to-reads-delta-cdf-vs-auto-cdf-vs-spark-4-2-changes</link>
<description>&lt;p&gt;Originally published on DEV.to: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/firfircelik/auto-cdf-moved-the-cdc-tax-from-writes-to-reads-delta-cdf-vs-auto-cdf-vs-spark-42-changes-9bb&quot;&gt;https://dev.to/firfircelik/auto-cdf-moved-the-cdc-tax-from-writes-to-reads-delta-cdf-vs-auto-cdf-vs-spark-42-changes-9bb&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;On September 1, 2026, Databricks made automatic change data feed (Auto CDF) generally available. The release note says it &quot;removes write-time overhead and makes &lt;code&gt;MERGE&lt;/code&gt; and &lt;code&gt;UPDATE&lt;/code&gt; operations about 15% faster on tables that you query for changes.&quot; Seven weeks earlier, Apache Spark 4.2.0 (July 14) added a SQL &lt;code&gt;CHANGES&lt;/code&gt; clause, so reading row-level changes is now a first-class part of the engine, not a Delta-only option.&lt;/p&gt;
&lt;p&gt;Most of the takes I have seen stop at &quot;free CDC, faster MERGE, turn it on.&quot; My thesis is narrower:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Auto CDF does not remove the cost of change data. It moves it from the writer, who pays once per commit, to every reader, who pays on every read.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Its prerequisite, row tracking, has a write-time cost of its own&lt;/strong&gt; that nobody puts on the slide.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spark 4.2's &lt;code&gt;CHANGES&lt;/code&gt; is a contract, not an implementation.&lt;/strong&gt; Whether it works depends on your connector, and on open-source Delta 4.4.1 it does not work out of the box.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the real question is not &quot;legacy or auto.&quot; It is: how many times is each commit read, and how much data does each commit rewrite?&lt;/p&gt;
&lt;h3 id=&quot;three-ways-to-get-the-changes&quot;&gt;Three ways to get the changes&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Legacy CDF (write time) &lt;/th&gt;&lt;th&gt;Auto CDF (read time) &lt;/th&gt;&lt;th&gt;Spark 4.2 &lt;code&gt;CHANGES&lt;/code&gt;&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Where the work happens &lt;/td&gt;&lt;td&gt;Writer writes change files next to the data &lt;/td&gt;&lt;td&gt;Reader computes changes from row tracking metadata &lt;/td&gt;&lt;td&gt;Connector supplies a changelog, Spark post-processes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Opt in &lt;/td&gt;&lt;td&gt;&lt;code&gt;delta.enableChangeDataFeed = true&lt;/code&gt; per table &lt;/td&gt;&lt;td&gt;Automatic when requirements are met &lt;/td&gt;&lt;td&gt;Catalog must implement &lt;code&gt;loadChangelog&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Who can read it &lt;/td&gt;&lt;td&gt;Any Delta reader that implements the &lt;code&gt;cdc&lt;/code&gt; action of the protocol &lt;/td&gt;&lt;td&gt;Databricks readers only (DBR 19+) &lt;/td&gt;&lt;td&gt;Any Spark 4.2 client, if the connector supports it&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Formats &lt;/td&gt;&lt;td&gt;Delta only &lt;/td&gt;&lt;td&gt;Delta with row tracking, Iceberg v3 &lt;/td&gt;&lt;td&gt;Whatever the connector exposes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Read API &lt;/td&gt;&lt;td&gt;&lt;code&gt;readChangeFeed&lt;/code&gt;, &lt;code&gt;table_changes()&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Same APIs &lt;/td&gt;&lt;td&gt;&lt;code&gt;SELECT ... CHANGES FROM VERSION&lt;/code&gt;, &lt;code&gt;.changes()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;FACT (Databricks docs): legacy CDF &quot;materializes changes during table writes&quot;, Auto CDF &quot;computes changes during table reads using row lineage metadata&quot;, and the two cannot be used together on one table. Migration is a single statement: &lt;code&gt;ALTER TABLE t UNSET TBLPROPERTIES ('delta.enableChangeDataFeed')&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-mechanism-in-two-paragraphs&quot;&gt;The mechanism, in two paragraphs&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Legacy CDF.&lt;/strong&gt; The Delta protocol has a &lt;code&gt;cdc&lt;/code&gt; action. On &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt; and &lt;code&gt;MERGE&lt;/code&gt; the writer puts the changed rows, with &lt;code&gt;_change_type&lt;/code&gt;, into files under &lt;code&gt;_change_data/&lt;/code&gt;. The protocol tells readers to use those files when they exist and skip the &lt;code&gt;add&lt;/code&gt; and &lt;code&gt;remove&lt;/code&gt; actions for that version. Insert-only commits and full partition deletes produce no change files, because the log already says what happened. So the reader of a commit touches roughly the changed rows, nothing more.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Auto CDF.&lt;/strong&gt; Row tracking gives each row a stable row ID and a row commit version. The Delta protocol explains how: for inserts, the IDs are derived from a &lt;code&gt;baseRowId&lt;/code&gt; per file plus the row position, which costs almost nothing. But when a writer copies a row into a new file (the &quot;copy&quot; in copy-on-write), it must preserve the stable ID, so it &lt;em&gt;materializes&lt;/em&gt; the row ID and commit version into hidden columns in the data file. At read time, the engine compares row IDs and commit versions across the files a commit removed and added to work out which rows were inserted, updated or deleted. That is real computation over real files, and it happens on every read.&lt;/p&gt;
&lt;h3 id=&quot;what-i-measured&quot;&gt;What I measured&lt;/h3&gt;
&lt;p&gt;I cannot run Databricks' Auto CDF outside Databricks, and I am not going to pretend otherwise. What I can run is open-source Delta Lake 4.4.1 on Spark 4.2.0, which has both halves of the mechanism: legacy CDF, and row tracking with &lt;code&gt;_metadata.row_id&lt;/code&gt; and &lt;code&gt;_metadata.row_commit_version&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A 10M-row Delta table (about 507 MB of Parquet), deletion vectors off, so &lt;code&gt;MERGE&lt;/code&gt; is pure copy-on-write.&lt;/li&gt;
&lt;li&gt;Each round: a &lt;code&gt;MERGE&lt;/code&gt; that updates about 100,000 rows (1%, keys hashed across the whole id range) and inserts 10,000 new ones. 5 rounds per variant, medians reported, after a discarded warm-up.&lt;/li&gt;
&lt;li&gt;Three variants: plain, legacy CDF, row tracking only.&lt;/li&gt;
&lt;li&gt;Read side: read the changes of the last commit. For legacy CDF that is &lt;code&gt;readChangeFeed&lt;/code&gt; for one version. For row tracking it is a reconstruction I wrote: read the files the commit removed (at version v-1) and added (at version v), and join on &lt;code&gt;row_id&lt;/code&gt; and &lt;code&gt;row_commit_version&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I checked the reconstruction against legacy CDF on a table with both features enabled: for a &lt;code&gt;MERGE&lt;/code&gt; (200 inserts, 1,986 pre-images, 1,986 post-images) and for a &lt;code&gt;DELETE&lt;/code&gt; (100 deletes) the counts matched exactly.&lt;/p&gt;
&lt;p&gt;The core of the reconstruction:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;after = (spark.read.format(&quot;delta&quot;).option(&quot;versionAsOf&quot;, v).load(path)
         .select(&quot;*&quot;, F.col(&quot;_metadata.row_id&quot;).alias(&quot;rid&quot;),
                 F.col(&quot;_metadata.row_commit_version&quot;).alias(&quot;rcv&quot;),
                 F.col(&quot;_metadata.file_path&quot;).alias(&quot;fp&quot;))
         .where(F.col(&quot;fp&quot;).isin(added_files)))
changed = after.where(F.col(&quot;rcv&quot;) == v)          # inserts + post-images
# pre-images: rows in removed files whose rid appears in `changed`
# deletes:    rows in removed files whose rid appears nowhere in `after`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Results, 10M rows, scattered 1% upsert, median of 5 rounds:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Variant &lt;/th&gt;&lt;th&gt;MERGE time &lt;/th&gt;&lt;th&gt;Bytes written per MERGE &lt;/th&gt;&lt;th&gt;Change files per MERGE &lt;/th&gt;&lt;th&gt;Read one commit's changes&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Plain &lt;/td&gt;&lt;td&gt;6.03 s (4.75 to 7.69) &lt;/td&gt;&lt;td&gt;505.8 MB &lt;/td&gt;&lt;td&gt;none &lt;/td&gt;&lt;td&gt;n/a&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Legacy CDF &lt;/td&gt;&lt;td&gt;6.52 s (6.19 to 7.10) &lt;/td&gt;&lt;td&gt;506.0 MB &lt;/td&gt;&lt;td&gt;10.8 MB &lt;/td&gt;&lt;td&gt;0.21 s&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Row tracking only &lt;/td&gt;&lt;td&gt;6.16 s (5.92 to 7.69) &lt;/td&gt;&lt;td&gt;547.6 MB &lt;/td&gt;&lt;td&gt;none &lt;/td&gt;&lt;td&gt;4.73 s (my reconstruction)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A second, partial run of the plain and legacy CDF variants gave 5.64 s and 6.89 s for the &lt;code&gt;MERGE&lt;/code&gt;, and 0.18 s for the CDF read.&lt;/p&gt;
&lt;h4 id=&quot;what-the-numbers-say-and-what-they-do-not&quot;&gt;What the numbers say, and what they do not&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1. The write tax of legacy CDF is real but small here, and smaller than my noise.&lt;/strong&gt; Legacy CDF was slower in both runs (6.52 vs 6.03 s, and 6.89 vs 5.64 s), but the spread inside one variant (4.75 to 7.69 s) is wider than the gap. Databricks measured about 15% on its runtime; my numbers are compatible with that and cannot confirm it. The byte count is solid: 10.8 MB of change files per &lt;code&gt;MERGE&lt;/code&gt;, about 2% of what the &lt;code&gt;MERGE&lt;/code&gt; rewrote anyway.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Row tracking wrote 8% more data on every rewrite.&lt;/strong&gt; 547.6 MB versus 505.8 MB per &lt;code&gt;MERGE&lt;/code&gt;, because every copied row now carries its stable row ID and commit version in hidden columns. That is about four times the size of the CDF change files in this workload. This is the part of the &quot;free&quot; read-time CDF that you pay at write time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Reading changes at read time costs what the commit rewrote, not what it changed.&lt;/strong&gt; The 1% scattered upsert touched every file, so the commit removed and re-added the whole table. Legacy CDF read 10.8 MB of change files in 0.21 s. My reconstruction had to scan both sides of a 500 MB rewrite to find 100,000 changed rows: 4.73 s, about 22 times slower. On a smaller test (200,000 rows, 8 files) the same scattered upsert rewrote 8 of 8 files, while an upsert clustered on the newest ids rewrote 1 of 8. The layout of your updates decides how expensive read-time CDF is.&lt;/p&gt;
&lt;p&gt;What I cannot claim: that Databricks' implementation is as slow as mine. It is very likely smarter, and with deletion vectors a &lt;code&gt;MERGE&lt;/code&gt; writes only the changed rows to new files and marks the old ones, which shrinks the set of rows a reader has to compare. My test is the copy-on-write case, which is the worst case for read-time CDF. The mechanism still holds: the read-time cost scales with what a commit touched, and you pay it once per reader per read.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;CHANGES&lt;/code&gt; on open-source Delta 4.4.1 fails by default.&lt;/strong&gt; With the stock &lt;code&gt;DeltaCatalog&lt;/code&gt;, all three of &lt;code&gt;SELECT * FROM t CHANGES FROM VERSION 1&lt;/code&gt;, the path-based form, and &lt;code&gt;spark.read.changes(&quot;t&quot;)&lt;/code&gt; returned:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[UNSUPPORTED_FEATURE.CHANGE_DATA_CAPTURE] The feature is not supported:
Catalog spark_catalog does not support Change Data Capture (CDC). SQLSTATE: 0A000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That matches the source. Delta PR #6794 (merged May 2026) adds catalog-driven read-time CDF behind an internal flag that is off by default, requires row tracking, and leaves the streaming side of the new path as a follow-up. I did not test the flag; it is internal and I would not run production on it. Spark 4.2 defines the shape of change data. Your connector decides whether you get any.&lt;/p&gt;
&lt;h3 id=&quot;gotchas-to-check-before-you-run-unset&quot;&gt;Gotchas to check before you run UNSET&lt;/h3&gt;
&lt;p&gt;All FACT, from the Databricks and Spark docs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Auto CDF is not supported on tables with row filters or column masks.&lt;/strong&gt; If your sensitive tables are governed with ABAC or masks, they are exactly the ones that cannot move.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only Databricks readers can query Auto CDF on Delta&lt;/strong&gt;, and external Iceberg clients cannot query it on Iceberg v3. Legacy CDF lives in the open Delta protocol. If anything outside Databricks reads your change feed, check before migrating.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It needs Databricks Runtime 19 or above&lt;/strong&gt; and a table registered in Unity Catalog with row tracking enabled (or Iceberg v3).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enabling row tracking on an existing table is not free.&lt;/strong&gt; It assigns IDs to every existing row, can create several new table versions, and concurrent writers fail with &lt;code&gt;MetadataChangedException&lt;/code&gt; while it runs. Pause streaming writers first. It also upgrades the writer protocol, so older external Delta clients that do not support those features can no longer write to the table.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-statement transactions:&lt;/strong&gt; Auto CDF is not supported if the source table was modified during the transaction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;_metadata.row_id&lt;/code&gt; and &lt;code&gt;row_commit_version&lt;/code&gt; cannot be read while reading the change feed.&lt;/strong&gt; If your downstream logic wants both, plan for it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spark 4.2 streaming &lt;code&gt;netChanges&lt;/code&gt; holds back the latest commit.&lt;/strong&gt; Rows touched only in the latest commit are not emitted until a later commit moves the watermark, and streaming net changes can differ from batch net changes over the same range. Fine for a pipeline, surprising for a low-latency consumer.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;decision-table&quot;&gt;Decision table&lt;/h3&gt;
&lt;p&gt;OPINION, built on the mechanism and my numbers:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Your table looks like &lt;/th&gt;&lt;th&gt;Better choice &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;Write-heavy, changes read rarely or by one pipeline &lt;/td&gt;&lt;td&gt;Auto CDF &lt;/td&gt;&lt;td&gt;You stop paying on every write for a feed few people read&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;One commit read by many consumers, often &lt;/td&gt;&lt;td&gt;Legacy CDF, or Auto CDF plus one stream that lands changes in a table &lt;/td&gt;&lt;td&gt;Read-time cost multiplies by readers times reads&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Scattered updates on a copy-on-write table &lt;/td&gt;&lt;td&gt;Legacy CDF, or enable deletion vectors and measure &lt;/td&gt;&lt;td&gt;Each commit rewrites most files, so read-time diffing is expensive&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Row filters or column masks &lt;/td&gt;&lt;td&gt;Legacy CDF &lt;/td&gt;&lt;td&gt;Auto CDF is not supported there&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Change feed read outside Databricks &lt;/td&gt;&lt;td&gt;Legacy CDF &lt;/td&gt;&lt;td&gt;Auto CDF is Databricks-readers only&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Iceberg v3 tables on Databricks &lt;/td&gt;&lt;td&gt;Auto CDF &lt;/td&gt;&lt;td&gt;Legacy CDF does not support Iceberg&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;what-i-would-do-this-week&quot;&gt;What I would do this week&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Inventory who reads each change feed.&lt;/strong&gt; Count consumers and their trigger intervals per table. That count is the multiplier on read-time cost, and nobody has it written down.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Look at the update layout.&lt;/strong&gt; In table history, compare &lt;code&gt;numTargetRowsUpdated&lt;/code&gt; against files rewritten for your &lt;code&gt;MERGE&lt;/code&gt; operations. A 1% update that rewrites 100% of files is a bad candidate for read-time CDF.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migrate the boring tables first:&lt;/strong&gt; write-heavy, one reader, no masks, nothing outside Databricks. Keep legacy CDF on the hot, many-reader tables until you have measured both on your own runtime.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If you fan out, fan out once.&lt;/strong&gt; One stream reads the change feed and writes a changes table; the other consumers read that. The Databricks docs already show this archive pattern for permanent history.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Auto CDF is a good default for most tables, and a quiet bill for the ones read by five pipelines. CDC is still a cost. The release moved it to the reader, and the bill now goes up with every pipeline that reads the feed.&lt;/p&gt;
&lt;h3 id=&quot;hardware-amp-versions&quot;&gt;Hardware &amp;amp; versions&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;Box: 8 vCPU Intel Xeon VM, 16 GB RAM, Linux 6.12, OpenJDK 21.0.12.&lt;br&gt;
PySpark 4.2.0, delta-spark 4.4.1 (&lt;code&gt;delta-spark_4.2_2.13&lt;/code&gt;), &lt;code&gt;local[8]&lt;/code&gt;, driver memory 8 GB (5 GB in the partial second run), 16 shuffle partitions.&lt;br&gt;
10M rows, 5 columns, about 507 MB Parquet, deletion vectors off. 5 rounds per variant, medians, warm-up discarded.&lt;br&gt;
Same box also hosts another long-running benchmark of mine. During the main run its runner was paused waiting for an idle host, but its services stayed in memory. The partial second run overlapped that benchmark's active phase, so I quote it only as a cross-check. Treat timings as indicative. The byte counts and the correctness check are deterministic.&lt;br&gt;
Measured October 26, 2026. Script, raw logs and pinned versions: &lt;a rel=&quot;nofollow&quot; href=&quot;https://gist.github.com/firfircelik/3ceab1b9b73b422293c0bdc76a690edb&quot;&gt;cdf_bench gist&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;sources&quot;&gt;Sources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Databricks September 2026 release notes, &quot;Automatic change data feed is now generally available&quot; (Sep 1, 2026): &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.databricks.com/aws/en/release-notes/product/2026/september&quot;&gt;https://docs.databricks.com/aws/en/release-notes/product/2026/september&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Databricks docs, Use change data feed (Auto CDF requirements, migration, limitations): &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.databricks.com/aws/en/delta/delta-change-data-feed&quot;&gt;https://docs.databricks.com/aws/en/delta/delta-change-data-feed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Azure Databricks docs, same page: &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/azure/databricks/delta/delta-change-data-feed&quot;&gt;https://learn.microsoft.com/en-us/azure/databricks/delta/delta-change-data-feed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Databricks docs, Row tracking: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.databricks.com/aws/en/delta/row-tracking&quot;&gt;https://docs.databricks.com/aws/en/delta/row-tracking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Delta Lake protocol v4.4.1 (Add CDC File, Row Tracking, materialized Row IDs): &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/delta-io/delta/blob/v4.4.1/PROTOCOL.md&quot;&gt;https://github.com/delta-io/delta/blob/v4.4.1/PROTOCOL.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Delta Lake docs, Change data feed: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.delta.io/delta-change-data-feed/&quot;&gt;https://docs.delta.io/delta-change-data-feed/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Delta Lake docs, Row tracking: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.delta.io/delta-row-tracking/&quot;&gt;https://docs.delta.io/delta-row-tracking/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Delta Lake 4.4.0 release (Spark 4.2 support): &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/delta-io/delta/releases/tag/v4.4.0&quot;&gt;https://github.com/delta-io/delta/releases/tag/v4.4.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Delta PR #6794, Add Changelog support: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/delta-io/delta/pull/6794&quot;&gt;https://github.com/delta-io/delta/pull/6794&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Apache Spark 4.2.0 release notes (Change Data Capture, SPARK-55668): &lt;a rel=&quot;nofollow&quot; href=&quot;https://spark.apache.org/releases/spark-release-4-2-0.html&quot;&gt;https://spark.apache.org/releases/spark-release-4-2-0.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Apache Spark news, Spark 4.2.0 released (July 14, 2026): &lt;a rel=&quot;nofollow&quot; href=&quot;https://spark.apache.org/news/&quot;&gt;https://spark.apache.org/news/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Spark PR, SPARK-55948 DSv2 CDC connector API and SQL CHANGES clause: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/apache/spark/pull/54738&quot;&gt;https://github.com/apache/spark/pull/54738&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Spark 4.2.0 API docs, &lt;code&gt;Changelog&lt;/code&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://spark.apache.org/docs/4.2.0/api/scala/org/apache/spark/sql/connector/catalog/Changelog.html&quot;&gt;https://spark.apache.org/docs/4.2.0/api/scala/org/apache/spark/sql/connector/catalog/Changelog.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Spark 4.2.0 API docs, &lt;code&gt;DataStreamReader.changes&lt;/code&gt; (streaming netChanges behavior): &lt;a rel=&quot;nofollow&quot; href=&quot;https://spark.apache.org/docs/4.2.0/api/scala/org/apache/spark/sql/streaming/DataStreamReader.html&quot;&gt;https://spark.apache.org/docs/4.2.0/api/scala/org/apache/spark/sql/streaming/DataStreamReader.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;My measurements: OSS Delta 4.4.1 on Spark 4.2.0, October 26, 2026 (setup above).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I post shorter breakdowns daily on LinkedIn.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32478/auto-cdf-moved-the-cdc-tax-from-writes-to-reads-delta-cdf-vs-auto-cdf-vs-spark-4-2-changes</guid>
<pubDate>Mon, 26 Oct 2026 07:12:17 +0000</pubDate>
</item>
<item>
<title>Backtesting live football on Polymarket: 6 traps in a recorded order book</title>
<link>https://coderlegion.com/31052/backtesting-live-football-on-polymarket-6-traps-in-a-recorded-order-book</link>
<description>&lt;p&gt;We recorded the order book of every Polymarket football (soccer) market for three weeks and tested two in-play ideas: buying a team right after a late price jump at a level score, and protecting goal bets with a stop-loss. Neither paid. Buying the jump 2 seconds later lost $2.85 per $10 over 837 jumps (t -7.9), and every one of 28 stop settings did worse than holding on the replay. The full results are &lt;a rel=&quot;nofollow&quot; href=&quot;https://orcalayer.com/blog/polymarket-live-football-late-goals-stop-loss&quot;&gt;on our blog&lt;/a&gt; and the data is &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/orcalayer/polymarket-football-late-goal-stop-loss-backtest&quot;&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This post is about the data side: six traps we hit while building the test. Most of them will bite anyone who backtests in-play markets on Polymarket.&lt;/p&gt;
&lt;h3 id=&quot;1-you-have-to-record-the-book-yourself-then-check-the-recording&quot;&gt;1. You have to record the book yourself, then check the recording&lt;/h3&gt;
&lt;p&gt;Public Polymarket APIs give you trades and the current book, not the book as it was a second ago. For an in-play test you need the best bid and ask around every event, so we recorded the CLOB WebSocket market channel (&lt;code&gt;book&lt;/code&gt;, &lt;code&gt;price_change&lt;/code&gt;, &lt;code&gt;last_trade_price&lt;/code&gt;) with exchange timestamps.&lt;/p&gt;
&lt;p&gt;Before using it, check it against on-chain trades. Ours had 99.9% of on-chain trades, and 97.6% of them happened at the best price the recording showed at that moment. If your numbers are much lower, your replay will invent fills.&lt;/p&gt;
&lt;h3 id=&quot;2-events-arrive-out-of-order&quot;&gt;2. Events arrive out of order&lt;/h3&gt;
&lt;p&gt;The exchange can send the &lt;code&gt;price_change&lt;/code&gt; for a level that a trade just ate before the &lt;code&gt;last_trade_price&lt;/code&gt; of that same trade. Read events in arrival order and fills look about 25% worse than they were. Put each trade ahead of the book change it caused before you measure anything.&lt;/p&gt;
&lt;h3 id=&quot;3-the-mid-is-not-a-price-you-can-trade&quot;&gt;3. The mid is not a price you can trade&lt;/h3&gt;
&lt;p&gt;Our first detector watched the mid-price: if a team's mid jumps by 0.25 or more late in a level match, call it a goal. Many of those &quot;goals&quot; were sellers pulling their quotes. The mid doubles, the bid stays where it was, and the spread opens up.&lt;/p&gt;
&lt;p&gt;One example from the data: the mid went from 0.24 to 0.505 in seconds, but the ask was 0.78 and the bid about 0.23. A bot buying 2 seconds later paid 0.81. Ten seconds after the jump the ask was back at 0.25, and the match ended in a draw.&lt;/p&gt;
&lt;p&gt;Store the ask and the spread, not only the mid. The implied bid is easy to get back if you only kept mid and ask:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def spread_at_jump(mid: float, ask: float) -&amp;gt; float:
    bid = 2 * mid - ask          # implied bid
    return round(ask - bid, 2)   # e.g. mid 0.505, ask 0.78 -&amp;gt; 0.55
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In our data 627 of 837 late jumps had a spread of 20 cents or more, and the jumping side won only 36% of those.&lt;/p&gt;
&lt;h3 id=&quot;4-the-level-score-can-be-fake-too&quot;&gt;4. The &quot;level score&quot; can be fake too&lt;/h3&gt;
&lt;p&gt;Our rule also needed the draw to be the favorite 30 seconds before the jump. That condition is read from mids as well, so pulled quotes can fake it in a match one team already leads. When all three books of a 1X2 match are two-sided, the three mids add up to about 1.00. Check that before you trust the state:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def book_looks_clean(home_mid: float, draw_mid: float, away_mid: float) -&amp;gt; bool:
    return 0.95 &amp;lt;= home_mid + draw_mid + away_mid &amp;lt;= 1.05
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In 51 of the 85 jumps that had a tight spread, the three mids before the jump added up to more than 1.10. One of them was a team that had led for 20 minutes: its bid disappeared for a moment, the score looked level, and the bid coming back looked like a late goal.&lt;/p&gt;
&lt;h3 id=&quot;5-daily-batch-files-overlap&quot;&gt;5. Daily batch files overlap&lt;/h3&gt;
&lt;p&gt;We ran the scan one day at a time, and each day file also covered the first hours after midnight UTC. Jumps after midnight landed in two files: 48 duplicates out of 885 raw rows. They did not change the conclusion, but they would have inflated every count and t-statistic. If you split a time series by day, check the joins.&lt;/p&gt;
&lt;h3 id=&quot;6-the-fastest-entry-is-not-reachable&quot;&gt;6. The fastest entry is not reachable&lt;/h3&gt;
&lt;p&gt;Football markets have &lt;code&gt;secondsDelay: 1&lt;/code&gt; in the Gamma API: a taker order waits a second before it can match, and you cannot cancel it during that time. So a result &quot;at +0 s&quot; is not a strategy, it is a plot point. In our test the earliest entries lost the most (-$4.21 per $10 at +0 s, -$2.85 at +2 s), because the first seconds after a jump are when the price is most wrong in the direction you are buying.&lt;/p&gt;
&lt;p&gt;The same delay hurts stops. Polymarket has no stop orders, so a stop is a bot watching trades and then market-selling after the delay, paying the taker fee a second time. In live football prices swing several cents within minutes, and a 3-cent stop fired on 86 to 96% of our replayed bets.&lt;/p&gt;
&lt;h3 id=&quot;reproduce-it&quot;&gt;Reproduce it&lt;/h3&gt;
&lt;p&gt;The repo has &lt;code&gt;data/late_goal_jumps.csv&lt;/code&gt; (one row per jump, with the asks 0 to 60 seconds after it and the result) and a standard-library script that recomputes every late-goal number:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 scripts/summarize_late_goals.py data/late_goal_jumps.csv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;METHOD.md&lt;/code&gt; describes the recorder, the detection rule and each trap above in more detail.&lt;/p&gt;
&lt;h3 id=&quot;what-this-does-not-show&quot;&gt;What this does not show&lt;/h3&gt;
&lt;p&gt;About three weeks of football, mostly club leagues. Pre-match markets, other sports and stops on strategies whose entries already make money were not tested. Bets in the same match are not independent, so read t values near 2 with care.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://orcalayer.com/blog/polymarket-live-football-late-goals-stop-loss&quot;&gt;orcalayer.com&lt;/a&gt;. OrcaLayer is an independent Polymarket analytics provider, not affiliated with Polymarket.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31052/backtesting-live-football-on-polymarket-6-traps-in-a-recorded-order-book</guid>
<pubDate>Wed, 14 Oct 2026 11:44:45 +0000</pubDate>
</item>
<item>
<title>Backtesting a Polymarket copy trading bot without fooling yourself</title>
<link>https://coderlegion.com/30590/backtesting-a-polymarket-copy-trading-bot-without-fooling-yourself</link>
<description>&lt;p&gt;Copy trading is the most common bot idea on Polymarket: find the wallets that win and buy what they buy. We tested it on five sports (MLB, NFL, college football, League of Legends and Valorant) and it did not survive. The full results are &lt;a rel=&quot;nofollow&quot; href=&quot;https://orcalayer.com/blog/polymarket-copy-trading-whales-backtest&quot;&gt;on our blog&lt;/a&gt; and the data and script are &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/orcalayer/polymarket-copy-trading-whales-backtest&quot;&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This post is about the backtest itself: how to set it up so it can tell you &quot;no&quot;, and the one mistake that makes almost every copy bot backtest look better than it would be live.&lt;/p&gt;
&lt;h3 id=&quot;1-define-a-copy-you-could-actually-execute&quot;&gt;1. Define a copy you could actually execute&lt;/h3&gt;
&lt;p&gt;A &quot;copy&quot; in our test is a wallet's first buy of at least $20 in a market, at a price between 0.30 and 0.85. We buy the same outcome for $10 at the wallet's price &lt;strong&gt;plus 1 cent&lt;/strong&gt;, pay the sports taker fee and hold to resolution. Roughly, in code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def copy_result(price: float, won: bool, stake: float = 10.0, slip: float = 0.01) -&amp;gt; float:
    p = price + slip                      # you never get the whale's exact price
    shares = stake / p
    fee = shares * 0.05 * p * (1 - p)     # Polymarket sports taker fee
    payout = shares if won else 0.0
    return payout - stake - fee
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One cent is generous. In fast in-play markets the price moves much further before a copy fills. We also reran MLB at +3 cents: every result went negative, including the top 10.&lt;/p&gt;
&lt;h3 id=&quot;2-pick-on-one-period-measure-on-the-next&quot;&gt;2. Pick on one period, measure on the next&lt;/h3&gt;
&lt;p&gt;The rule: rank wallets by their copy results over the two months before the test month, then copy only in the test month. Nothing from the test month touches the selection. We used three rules fixed before the run:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ALL10&lt;/code&gt;: every wallet with at least 10 copies (the &quot;average whale&quot;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POS&lt;/code&gt;: at least 20 copies, mean above zero, t-statistic of 2 or more&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TOP10&lt;/code&gt;: the 10 best by t-statistic&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MLB, top 10, per $10 copied:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Test month &lt;/th&gt;&lt;th&gt;While picked &lt;/th&gt;&lt;th&gt;Month after&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;July 2026 &lt;/td&gt;&lt;td&gt;+$3.65 &lt;/td&gt;&lt;td&gt;+$0.16&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;August 2026 &lt;/td&gt;&lt;td&gt;+$3.06 &lt;/td&gt;&lt;td&gt;-$0.04&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;September 2026 &lt;/td&gt;&lt;td&gt;+$2.72 &lt;/td&gt;&lt;td&gt;-$0.47&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Oct 1-4 2026 &lt;/td&gt;&lt;td&gt;+$3.78 &lt;/td&gt;&lt;td&gt;-$2.23 (n 7)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The same rule that showed +$3 in the pick window showed about zero the month after.&lt;/p&gt;
&lt;h3 id=&quot;3-check-whether-rank-carries-over-at-all&quot;&gt;3. Check whether rank carries over at all&lt;/h3&gt;
&lt;p&gt;Before tuning thresholds, check if past performance predicts future performance for individual wallets. For MLB wallets with at least 10 copies in both periods, the Spearman correlation between the pick months and the next month was -0.01, +0.15, +0.08 and -0.03. NFL was between -0.09 and +0.03.&lt;/p&gt;
&lt;p&gt;With correlations like that, picking &quot;the best&quot; is picking the luckiest. With thousands of active wallets, dozens will have a great two months by chance.&lt;/p&gt;
&lt;h3 id=&quot;4-look-at-the-baseline-before-the-winners&quot;&gt;4. Look at the baseline before the winners&lt;/h3&gt;
&lt;p&gt;The average MLB whale, copied one cent worse than its own price, lost 31 to 63 cents per $10 in every test month. If the baseline is negative, a selection rule has to beat a loss before it makes anything. Check &lt;code&gt;ALL10&lt;/code&gt; first.&lt;/p&gt;
&lt;h3 id=&quot;5-the-trap-wallet-labels-computed-today&quot;&gt;5. The trap: wallet labels computed today&lt;/h3&gt;
&lt;p&gt;This one is easy to miss. Analytics sites (ours included) label wallets: &quot;smart money&quot;, &quot;50+ resolved markets&quot;, win rate, profit factor. Those labels are computed &lt;strong&gt;today, on each wallet's full history&lt;/strong&gt;, including the months you are about to backtest.&lt;/p&gt;
&lt;p&gt;If you pick wallets with today's labels and backtest them on last summer, the selection already knows who survived. Wallets that blew up are not labeled &quot;smart&quot;, so the backtest never copies them. It will always look better than it would have live.&lt;/p&gt;
&lt;p&gt;We hit a mild version of this ourselves: one filter in our profile screen used each wallet's share of activity in the sport on its full history. We disclosed it in the methodology. It most likely flatters the pools, and they still showed no edge.&lt;/p&gt;
&lt;p&gt;Rule of thumb: every feature used to pick a wallet must be computable from data strictly before the pick date.&lt;/p&gt;
&lt;h3 id=&quot;6-make-it-reproducible&quot;&gt;6. Make it reproducible&lt;/h3&gt;
&lt;p&gt;The repo has &lt;code&gt;scripts/copy_backtest.py&lt;/code&gt; (Python 3.9+, standard library only) that runs the same walk-forward on the OrcaLayer API for any league:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export ORCALAYER_API_KEY=sk_orca_...        # never commit it
python3 scripts/copy_backtest.py --league mlb --start 2026-05-01 --end 2026-09-30 --max-markets 800
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To check that the API path and our internal database agree, we ran both on the same 800 MLB games: 161,094 copies in both, every wallet, price and result equal, total -$83,012.60 in both. The script output showed the same picture: top 10 from about +$3 per $10 to between -$1.29 and +$1.06 the month after.&lt;/p&gt;
&lt;p&gt;The script needs a Premium API key. If you just want to look at a wallet's record before copying it, the free &lt;a rel=&quot;nofollow&quot; href=&quot;https://orcalayer.com/docs/mcp&quot;&gt;OrcaLayer MCP server&lt;/a&gt; works from Claude without a key.&lt;/p&gt;
&lt;h3 id=&quot;what-this-does-not-show-1&quot;&gt;What this does not show&lt;/h3&gt;
&lt;p&gt;Five sports, and picking by past results only. Slower markets like politics, where informed money can stay ahead for days, and picking wallets by how they trade rather than how much they made, were not tested.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://orcalayer.com/blog/polymarket-copy-trading-whales-backtest&quot;&gt;orcalayer.com&lt;/a&gt;. OrcaLayer is an independent Polymarket analytics provider, not affiliated with Polymarket.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30590/backtesting-a-polymarket-copy-trading-bot-without-fooling-yourself</guid>
<pubDate>Sat, 10 Oct 2026 13:28:30 +0000</pubDate>
</item>
<item>
<title>Two PySpark contracts Staff DEs learn on a bad Tuesday: foreachBatch restarts and applyInPandas skew</title>
<link>https://coderlegion.com/30476/two-pyspark-contracts-staff-des-learn-tuesday-foreachbatch-restarts-applyinpandas-skew</link>
<description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/firfircelik/two-pyspark-contracts-staff-des-learn-on-a-bad-tuesday-foreachbatch-restarts-and-applyinpandas-skew-39pa&quot;&gt;DEV.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&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 &quot;exactly-once foreachBatch&quot; fairy tales.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Primary sources:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.databricks.com/aws/en/structured-streaming/foreach&quot;&gt;Databricks foreachBatch&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.delta.io/latest/delta-update.html&quot;&gt;Delta MERGE / streaming upsert&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.databricks.com/aws/en/structured-streaming/watermarks&quot;&gt;watermarks&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.databricks.com/aws/en/structured-streaming/read-state&quot;&gt;state store reader&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.databricks.com/aws/en/structured-streaming/delta-lake&quot;&gt;idempotent Delta writes&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.databricks.com/aws/en/pandas/pandas-function-apis&quot;&gt;pandas function APIs&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://spark.apache.org/docs/latest/api/python/tutorial/sql/arrow_pandas.html&quot;&gt;Apache Arrow in PySpark&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;part-1-design-for-the-restart-foreachbatch-watermarks-and-merge&quot;&gt;Part 1. Design for the restart: &lt;code&gt;foreachBatch&lt;/code&gt;, watermarks, and MERGE&lt;/h3&gt;
&lt;h4 id=&quot;contract-1-foreachbatch-is-at-least-once&quot;&gt;Contract 1: &lt;code&gt;foreachBatch&lt;/code&gt; is at-least-once&lt;/h4&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;APP_ID = &quot;silver-orders-upsert-v1&quot;  # change if you reset the checkpoint


def process_batch(batch_df, batch_id: int) -&amp;gt; None:
    # FACT: Delta sources can deliver empty micro-batches (e.g. OPTIMIZE with
    # nothing to process, or predicate pruning). Code must tolerate them.
    if batch_df.isEmpty():
        return

    valid = batch_df.filter(&quot;order_amount &amp;gt; 0 AND customer_id IS NOT NULL&quot;)
    invalid = batch_df.filter(
        &quot;NOT (order_amount &amp;gt; 0 AND customer_id IS NOT NULL)&quot;
    )

    # FACT: txnAppId + txnVersion on DataFrameWriter make appends idempotent
    # for retries of the same batch_id. Binding txnVersion to batch_id is the
    # documented pattern.
    (
        valid.write.format(&quot;delta&quot;)
        .mode(&quot;append&quot;)
        .option(&quot;txnAppId&quot;, APP_ID)
        .option(&quot;txnVersion&quot;, batch_id)
        .saveAsTable(&quot;main.silver.orders&quot;)
    )

    if not invalid.isEmpty():
        (
            invalid.write.format(&quot;delta&quot;)
            .mode(&quot;append&quot;)
            .option(&quot;txnAppId&quot;, APP_ID)
            .option(&quot;txnVersion&quot;, batch_id)
            .saveAsTable(&quot;main.silver.orders_dlq&quot;)
        )


(
    spark.readStream.table(&quot;main.bronze.raw_orders&quot;)
    .writeStream.foreachBatch(process_batch)
    .option(&quot;checkpointLocation&quot;, &quot;/Volumes/main/ops/checkpoints/orders&quot;)
    .start()
)
&lt;/code&gt;&lt;/pre&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;hr&gt;
&lt;h4 id=&quot;contract-2-watermark-dedupe-has-a-state-lease-and-you-must-finish-the-batch-&quot;&gt;Contract 2: watermark dedupe has a state lease (and you must finish the batch)&lt;/h4&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from pyspark.sql.functions import col

deduped = (
    spark.readStream.table(&quot;main.bronze.events&quot;)
    .withWatermark(&quot;event_time&quot;, &quot;10 hours&quot;)  # &amp;gt; max duplicate lag (FACT guidance)
    .dropDuplicatesWithinWatermark([&quot;event_id&quot;])
)


def bad_handler(batch_df, batch_id: int) -&amp;gt; None:
    # Anti-pattern: partial consume after a stateful operator
    batch_df.show(2)
    # remaining rows never consumed -&amp;gt; next micro-batch can fail


def good_handler(batch_df, batch_id: int) -&amp;gt; None:
    if batch_df.isEmpty():
        return
    # Full action that consumes the plan (write, count, foreach, etc.)
    (
        batch_df.write.format(&quot;delta&quot;)
        .mode(&quot;append&quot;)
        .option(&quot;txnAppId&quot;, &quot;events-dedupe-v1&quot;)
        .option(&quot;txnVersion&quot;, batch_id)
        .saveAsTable(&quot;main.silver.events&quot;)
    )


(
    deduped.writeStream.foreachBatch(good_handler)
    .option(&quot;checkpointLocation&quot;, &quot;/Volumes/main/ops/checkpoints/events&quot;)
    .start()
)
&lt;/code&gt;&lt;/pre&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 rel=&quot;nofollow&quot; href=&quot;https://docs.databricks.com/aws/en/structured-streaming/read-state&quot;&gt;Read Structured Streaming state information&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;contract-3-merge-inside-foreachbatch-is-a-logical-idempotency-problem&quot;&gt;Contract 3: MERGE inside &lt;code&gt;foreachBatch&lt;/code&gt; is a &lt;em&gt;logical&lt;/em&gt; idempotency problem&lt;/h4&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from delta.tables import DeltaTable
from pyspark.sql import functions as F

target = DeltaTable.forName(spark, &quot;main.silver.customers&quot;)


def upsert_cdc(batch_df, batch_id: int) -&amp;gt; None:
    if batch_df.isEmpty():
        return

    # Collapse to latest change per business key (FACT: avoid multi-match MERGE)
    latest = (
        batch_df.select(
            &quot;customer_id&quot;,
            F.struct(&quot;event_time&quot;, &quot;payload&quot;, &quot;deleted&quot;).alias(&quot;chg&quot;),
        )
        .groupBy(&quot;customer_id&quot;)
        .agg(F.max(&quot;chg&quot;).alias(&quot;chg&quot;))
        .select(&quot;customer_id&quot;, &quot;chg.*&quot;)
    )

    # FACT: MERGE may scan the source more than once -&amp;gt; cache the batch
    latest = latest.cache()
    try:
        (
            target.alias(&quot;t&quot;)
            .merge(latest.alias(&quot;s&quot;), &quot;t.customer_id = s.customer_id&quot;)
            .whenMatchedDelete(condition=&quot;s.deleted = true&quot;)
            .whenMatchedUpdate(
                set={&quot;payload&quot;: &quot;s.payload&quot;, &quot;updated_at&quot;: &quot;s.event_time&quot;}
            )
            .whenNotMatchedInsert(
                condition=&quot;s.deleted = false&quot;,
                values={
                    &quot;customer_id&quot;: &quot;s.customer_id&quot;,
                    &quot;payload&quot;: &quot;s.payload&quot;,
                    &quot;updated_at&quot;: &quot;s.event_time&quot;,
                },
            )
            .execute()
        )
    finally:
        latest.unpersist()


(
    spark.readStream.table(&quot;main.bronze.customer_cdc&quot;)
    .writeStream.foreachBatch(upsert_cdc)
    .option(&quot;checkpointLocation&quot;, &quot;/Volumes/main/ops/checkpoints/customer_cdc&quot;)
    .start()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Interpretation:&lt;/strong&gt; &quot;Idempotent MERGE&quot; here means latest-wins on a stable key (and deletes keyed the same way), not &quot;Spark will skip the merge via txn metadata.&quot; 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;hr&gt;
&lt;h4 id=&quot;part-1-checklist-print-this-next-to-the-pr-&quot;&gt;Part 1 checklist (print this next to the PR)&lt;/h4&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;hr&gt;
&lt;h3 id=&quot;part-2-arrow-is-not-a-free-pass-applyinpandas-vs-mapinpandas-mapinarrow&quot;&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;/h3&gt;
&lt;p&gt;Python DE craft is not only streaming. The other Tuesday failure mode is &quot;we used pandas because Arrow is fast&quot; and then a skewed group killed the executor.&lt;/p&gt;
&lt;h4 id=&quot;three-apis-three-memory-contracts&quot;&gt;Three APIs, three memory contracts&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;FACT (Databricks pandas function APIs / Apache Spark):&lt;/strong&gt;&lt;/p&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;p&gt;Cogrouped &lt;code&gt;applyInPandas&lt;/code&gt; has the same whole-cogroup-in-memory warning.&lt;/p&gt;
&lt;h4 id=&quot;skew-turns-elegant-into-oom&quot;&gt;Skew turns &quot;elegant&quot; into OOM&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Looks clean. Under a hot customer_id it is a bomb.
def per_customer_features(pdf):
    # pdf = ALL rows for one customer_id
    pdf = pdf.sort_values(&quot;event_time&quot;)
    pdf[&quot;rolling_amt&quot;] = pdf[&quot;amount&quot;].rolling(50, min_periods=1).sum()
    return pdf


# FACT: entire group must fit in worker memory; maxRecordsPerBatch does NOT help
out = (
    events.groupBy(&quot;customer_id&quot;)
    .applyInPandas(
        per_customer_features,
        schema=&quot;customer_id string, event_time timestamp, amount double, rolling_amt double&quot;,
    )
)
&lt;/code&gt;&lt;/pre&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 &quot;run pandas on Spark&quot; hammer.&lt;/p&gt;
&lt;h4 id=&quot;prefer-batch-maps-when-you-do-not-need-whole-group-semantics&quot;&gt;Prefer batch maps when you do not need whole-group semantics&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;spark.conf.set(&quot;spark.sql.execution.arrow.maxRecordsPerBatch&quot;, &quot;5000&quot;)


def score_batches(iterator):
    # init once per Python worker task
    model = load_light_model()  # your code; keep it picklable / available on workers
    for pdf in iterator:
        pdf = pdf.copy()
        pdf[&quot;score&quot;] = model.predict(pdf[[&quot;f1&quot;, &quot;f2&quot;]])
        yield pdf[[&quot;id&quot;, &quot;score&quot;]]


scored = events.mapInPandas(
    score_batches,
    schema=&quot;id long, score double&quot;,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or stay in Arrow when pandas is unnecessary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import pyarrow as pa
import pyarrow.compute as pc


def filter_arrow(iterator):
    for batch in iterator:
        mask = pc.greater(batch.column(&quot;amount&quot;), 0)
        yield batch.filter(mask)


positive = events.mapInArrow(filter_arrow, schema=events.schema)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;decision-table-1&quot;&gt;Decision table&lt;/h4&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;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;hr&gt;
&lt;h3 id=&quot;how-the-two-themes-connect&quot;&gt;How the two themes connect&lt;/h3&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 &quot;Arrow optimized&quot; = 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;hr&gt;
&lt;h3 id=&quot;optional-diagram-for-a-cover-or-inline-figure-&quot;&gt;Optional diagram (for a cover or inline figure)&lt;/h3&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 &quot;append: txn skip&quot; vs &quot;MERGE: must be latest-wins safe&quot;.&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 &quot;all rows in RAM&quot; 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;hr&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.databricks.com/aws/en/structured-streaming/foreach&quot;&gt;Use foreachBatch to write to arbitrary data sinks (Databricks)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.delta.io/latest/delta-update.html&quot;&gt;Delta Lake table deletes, updates, and merges (streaming foreachBatch section)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.databricks.com/aws/en/structured-streaming/watermarks&quot;&gt;Apply watermarks / dropDuplicatesWithinWatermark (Databricks)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.databricks.com/aws/en/structured-streaming/read-state&quot;&gt;Read Structured Streaming state information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.databricks.com/aws/en/structured-streaming/delta-lake&quot;&gt;Delta Lake streaming reads and writes (idempotent foreachBatch writes)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.databricks.com/aws/en/pandas/pandas-function-apis&quot;&gt;pandas function APIs (Databricks)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://spark.apache.org/docs/latest/api/python/tutorial/sql/arrow_pandas.html&quot;&gt;Apache Arrow in PySpark&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/api/pyspark.sql.DataFrame.mapInArrow.html&quot;&gt;mapInArrow API&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&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>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30476/two-pyspark-contracts-staff-des-learn-tuesday-foreachbatch-restarts-applyinpandas-skew</guid>
<pubDate>Fri, 09 Oct 2026 13:20:49 +0000</pubDate>
</item>
<item>
<title>Fabric Runtime 2.0 is becoming the default. Here is what actually breaks.</title>
<link>https://coderlegion.com/30475/fabric-runtime-2-0-is-becoming-the-default-here-is-what-actually-breaks</link>
<description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/firfircelik/fabric-runtime-20-is-becoming-the-default-here-is-what-actually-breaks-12i9&quot;&gt;DEV.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;fabric-runtime-2-0-is-becoming-the-default-here-is-what-actually-breaks-&quot;&gt;Fabric Runtime 2.0 is becoming the default. Here is what actually breaks.&lt;/h2&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 rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/fabric/data-engineering/runtime-2-0&quot;&gt;Fabric Runtime 2.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/fabric/data-engineering/native-execution-engine-overview&quot;&gt;Native Execution Engine overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;1-hook-the-default-is-the-migration&quot;&gt;1) Hook: the default &lt;em&gt;is&lt;/em&gt; the migration&lt;/h3&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 &quot;defaults are fine,&quot; 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 &quot;ignore migration.&quot;&lt;/p&gt;
&lt;h3 id=&quot;2-wrong-assumption-ga-means-notebooks-just-run&quot;&gt;2) Wrong assumption: GA means notebooks just run&lt;/h3&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;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;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 &quot;worked forever&quot; 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;h3 id=&quot;3-wrong-assumption-nee-free-6x&quot;&gt;3) Wrong assumption: NEE = free 6x&lt;/h3&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 &quot;job succeeded&quot; 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 &quot;up to 6x.&quot;&lt;/p&gt;
&lt;h3 id=&quot;4-how-to-see-the-truth-in-the-plan&quot;&gt;4) How to see the truth in the plan&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;spark.conf.set('spark.native.enabled', 'false')
&lt;/code&gt;&lt;/pre&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;h3 id=&quot;5-delta-4-x-trap-for-multi-workload-tables&quot;&gt;5) Delta 4.x trap for multi-workload tables&lt;/h3&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 &quot;uncheck the box.&quot; 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;h3 id=&quot;6-practical-migration-pattern&quot;&gt;6) Practical migration pattern&lt;/h3&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;pre&gt;&lt;code class=&quot;language-python&quot;&gt;spark.conf.set('spark.native.enabled', 'false')
&lt;/code&gt;&lt;/pre&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;h3 id=&quot;7-ci-cd-angle-pin-the-runtime-like-a-toolchain&quot;&gt;7) CI/CD angle: pin the runtime like a toolchain&lt;/h3&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 &quot;NEE enabled&quot; 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;h3 id=&quot;8-close-benchmarks-sell-the-ceiling-production-pays-the-median&quot;&gt;8) Close: benchmarks sell the ceiling, production pays the median&lt;/h3&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;h4 id=&quot;takeaways&quot;&gt;Takeaways&lt;/h4&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; &quot;up to 6x&quot; 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;h3 id=&quot;sources-1&quot;&gt;Sources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/fabric/data-engineering/runtime-2-0&quot;&gt;Runtime 2.0 in Fabric (Microsoft Learn)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/fabric/data-engineering/native-execution-engine-overview&quot;&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 rel=&quot;nofollow&quot; href=&quot;https://rajanieshkaushikk.com/2026/09/23/fabric-runtime-2-0-two-breakages/&quot;&gt;Fabric Runtime 2.0 two breakages (Rajaniesh Kaushikk, 2026-09-23)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30475/fabric-runtime-2-0-is-becoming-the-default-here-is-what-actually-breaks</guid>
<pubDate>Fri, 09 Oct 2026 13:20:30 +0000</pubDate>
</item>
<item>
<title>Azure Synapse to Databricks: A Migration Playbook That Survives Production</title>
<link>https://coderlegion.com/30474/azure-synapse-to-databricks-a-migration-playbook-that-survives-production</link>
<description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/firfircelik/azure-synapse-databricks-a-migration-playbook-that-survives-production-nge&quot;&gt;DEV.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;azure-synapse-databricks-a-migration-playbook-that-survives-production&quot;&gt;Azure Synapse → Databricks: A Migration Playbook That Survives Production&lt;/h2&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;hr&gt;
&lt;h3 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#1-why-this-migration-is-harder-than-the-slide-deck&quot;&gt;Why this migration is harder than the slide deck&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#2-what-you-are-actually-migrating&quot;&gt;What you are actually migrating&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#3-program-shape-discovery--waves&quot;&gt;Program shape: discovery → waves&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#4-four-parallel-workstreams&quot;&gt;Four parallel workstreams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#5-data-movement-skip-the-jdbc-tax&quot;&gt;Data movement: skip the JDBC tax&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#6-code-conversion-syntax-is-easy-semantics-bite&quot;&gt;Code conversion: syntax is easy, semantics bite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#7-dimensional-patterns-on-delta-scd2&quot;&gt;Dimensional patterns on Delta (SCD2)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#8-reconciliation--cutover&quot;&gt;Reconciliation &amp;amp; cutover&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#9-power-bi--consumption-cutover&quot;&gt;Power BI / consumption cutover&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#10-30--60--90-plan&quot;&gt;30 / 60 / 90 plan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#11-anti-patterns-i-keep-seeing&quot;&gt;Anti-patterns I keep seeing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#12-field-rules-i-will-not-compromise-on&quot;&gt;Field rules I will not compromise on&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#13-faq&quot;&gt;FAQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#14-sources&quot;&gt;Sources&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-why-this-migration-is-harder-than-the-slide-deck&quot;&gt;1) Why this migration is harder than the slide deck&lt;/h3&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;
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;hr&gt;
&lt;h3 id=&quot;2-what-you-are-actually-migrating&quot;&gt;2) What you are actually migrating&lt;/h3&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;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;hr&gt;
&lt;h3 id=&quot;3-program-shape-discovery-waves&quot;&gt;3) Program shape: discovery → waves&lt;/h3&gt;
&lt;p&gt;Do not start with “transpile everything.” Start with a &lt;strong&gt;program&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&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;h4 id=&quot;phase-a-discovery&quot;&gt;Phase A — Discovery&lt;/h4&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;h4 id=&quot;phase-b-assessment&quot;&gt;Phase B — Assessment&lt;/h4&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;h4 id=&quot;phase-c-design-usually-hybrid-&quot;&gt;Phase C — Design (usually hybrid)&lt;/h4&gt;
&lt;p&gt;Three strategies show up in every steering committee:&lt;/p&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;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;h4 id=&quot;phase-d-pilot-lighthouse-&quot;&gt;Phase D — Pilot (lighthouse)&lt;/h4&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;h4 id=&quot;phase-e-waves&quot;&gt;Phase E — Waves&lt;/h4&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;hr&gt;
&lt;h3 id=&quot;4-four-parallel-workstreams&quot;&gt;4) Four parallel workstreams&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&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;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;h4 id=&quot;sequencing-tip-that-builds-trust&quot;&gt;Sequencing tip that builds trust&lt;/h4&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;hr&gt;
&lt;h3 id=&quot;5-data-movement-skip-the-jdbc-tax&quot;&gt;5) Data movement: skip the JDBC tax&lt;/h3&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&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;h4 id=&quot;why-this-path-wins&quot;&gt;Why this path wins&lt;/h4&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;h4 id=&quot;example-cetas-from-synapse-dedicated-sql-pool&quot;&gt;Example — CETAS from Synapse Dedicated SQL Pool&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- File format
CREATE EXTERNAL FILE FORMAT ParquetFF
WITH (
    FORMAT_TYPE = PARQUET,
    DATA_COMPRESSION = 'org.apache.hadoop.io.compress.SnappyCodec'
);

-- Landing zone on ADLS Gen2
CREATE EXTERNAL DATA SOURCE MigLanding
WITH (
    LOCATION = 'https://&amp;lt;storage&amp;gt;.dfs.core.windows.net/&amp;lt;container&amp;gt;',
    TYPE = HADOOP
);

-- Export a bounded slice (always partition/filter on first runs)
CREATE EXTERNAL TABLE ext.fact_sales_export
WITH (
    LOCATION = '/migration/fact_sales/ds=2026-09-03/',
    DATA_SOURCE = MigLanding,
    FILE_FORMAT = ParquetFF
)
AS
SELECT
    sales_id,
    customer_nk,
    product_nk,
    business_date,
    net_amount,
    quantity
FROM dbo.fact_sales
WHERE business_date &amp;gt;= '2024-01-01';
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;example-land-as-delta-on-databricks&quot;&gt;Example — land as Delta on Databricks&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from pyspark.sql import functions as F

src = &quot;abfss://&amp;lt;container&amp;gt;@&amp;lt;storage&amp;gt;.dfs.core.windows.net/migration/fact_sales/&quot;

df = (
    spark.read.parquet(src)
    .withColumn(&quot;ingested_at&quot;, F.current_timestamp())
)

(
    df.write
    .format(&quot;delta&quot;)
    .mode(&quot;overwrite&quot;)
    .option(&quot;overwriteSchema&quot;, &quot;true&quot;)
    .saveAsTable(&quot;sales_uc.gold.fact_sales&quot;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;physical-design-do-not-cargo-cult-synapse&quot;&gt;Physical design: do not cargo-cult Synapse&lt;/h4&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;hr&gt;
&lt;h3 id=&quot;6-code-conversion-syntax-is-easy-semantics-bite&quot;&gt;6) Code conversion: syntax is easy, semantics bite&lt;/h3&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;h4 id=&quot;common-remaps&quot;&gt;Common remaps&lt;/h4&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;h4 id=&quot;semantic-trap-1-case-sensitivity&quot;&gt;Semantic trap #1 — case sensitivity&lt;/h4&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;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- May silently change results after migration
WHERE status = 'active'

-- Make intent explicit
WHERE LOWER(status) = 'active'
&lt;/code&gt;&lt;/pre&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;h4 id=&quot;semantic-trap-2-stored-procedures&quot;&gt;Semantic trap #2 — stored procedures&lt;/h4&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;h4 id=&quot;semantic-trap-3-try-catch-vs-workflow-design&quot;&gt;Semantic trap #3 — TRY/CATCH vs workflow design&lt;/h4&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;hr&gt;
&lt;h3 id=&quot;7-dimensional-patterns-on-delta-scd2-&quot;&gt;7) Dimensional patterns on Delta (SCD2)&lt;/h3&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;h4 id=&quot;example-pattern-expire-then-insert-illustrative-&quot;&gt;Example pattern — expire then insert (illustrative)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 1) Expire changed current rows
MERGE INTO dim_customer AS t
USING staging_customer AS s
ON t.customer_nk = s.customer_nk AND t.is_current = true
WHEN MATCHED AND (
     t.segment &amp;lt;&amp;gt; s.segment
  OR t.country &amp;lt;&amp;gt; s.country
) THEN UPDATE SET
  t.is_current = false,
  t.valid_to   = current_timestamp();

-- 2) Insert new current versions
INSERT INTO dim_customer (
  customer_nk, segment, country, valid_from, valid_to, is_current
)
SELECT
  s.customer_nk,
  s.segment,
  s.country,
  current_timestamp(),
  CAST(NULL AS TIMESTAMP),
  true
FROM staging_customer s
LEFT ANTI JOIN dim_customer t
  ON t.customer_nk = s.customer_nk
 AND t.is_current = true;
&lt;/code&gt;&lt;/pre&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;hr&gt;
&lt;h3 id=&quot;8-reconciliation-amp-cutover&quot;&gt;8) Reconciliation &amp;amp; cutover&lt;/h3&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;h4 id=&quot;minimum-reconcile-pack-every-wave-&quot;&gt;Minimum reconcile pack (every wave)&lt;/h4&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;h4 id=&quot;cutover-pattern-that-keeps-executives-calm&quot;&gt;Cutover pattern that keeps executives calm&lt;/h4&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;hr&gt;
&lt;h3 id=&quot;9-power-bi-consumption-cutover&quot;&gt;9) Power BI / consumption cutover&lt;/h3&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;hr&gt;
&lt;h3 id=&quot;10-30-60-90-plan&quot;&gt;10) 30 / 60 / 90 plan&lt;/h3&gt;
&lt;h4 id=&quot;days-1-30-foundations&quot;&gt;Days 1–30 — foundations&lt;/h4&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;h4 id=&quot;days-31-60-prove-the-path&quot;&gt;Days 31–60 — prove the path&lt;/h4&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;h4 id=&quot;days-61-90-scale-without-drama&quot;&gt;Days 61–90 — scale without drama&lt;/h4&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;hr&gt;
&lt;h3 id=&quot;11-anti-patterns-i-keep-seeing&quot;&gt;11) Anti-patterns I keep seeing&lt;/h3&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;hr&gt;
&lt;h3 id=&quot;12-field-rules-i-will-not-compromise-on&quot;&gt;12) Field rules I will not compromise on&lt;/h3&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;hr&gt;
&lt;h3 id=&quot;13-faq&quot;&gt;13) FAQ&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Q: Should we move to Fabric instead of Databricks?&lt;/strong&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;
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;
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;
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;hr&gt;
&lt;h3 id=&quot;14-sources&quot;&gt;14) Sources&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Databricks — &lt;em&gt;Navigating a Synapse Migration to Databricks&lt;/em&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.databricks.com/blog/navigating-synapse-migration-databricks&quot;&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;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.databricks.com/blog/introducing-lakebridge-free-open-data-migration-databricks-sql&quot;&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;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://databrickslabs.github.io/lakebridge/docs/assessment/profiler/synapse/&quot;&gt;https://databrickslabs.github.io/lakebridge/docs/assessment/profiler/synapse/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Lakebridge docs — Transpile&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://databrickslabs.github.io/lakebridge/docs/transpile/&quot;&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;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@prabhakarankanniappan/copy-3-5-billion-rows-from-synapse-to-databricks-in-30-minutes-fda15e0ffca0&quot;&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;hr&gt;
&lt;h4 id=&quot;suggested-linkedin-teaser-after-you-publish-on-dev-to-&quot;&gt;Suggested LinkedIn teaser (after you publish on DEV.to)&lt;/h4&gt;
&lt;blockquote&gt;&lt;p&gt;Synapse → Databricks is not a notebook move.&lt;br&gt;
It’s Dedicated SQL Pools + pipelines + T-SQL semantics + Power BI trust.&lt;br&gt;
I wrote a practical playbook with diagrams, CETAS→Delta examples, reconcile guidance, and a 30/60/90.&lt;br&gt;
Link: &amp;lt;your DEV.to URL&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&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>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30474/azure-synapse-to-databricks-a-migration-playbook-that-survives-production</guid>
<pubDate>Fri, 09 Oct 2026 13:19:33 +0000</pubDate>
</item>
<item>
<title>6 things that broke our Polymarket trading bot before it made a dollar</title>
<link>https://coderlegion.com/30137/6-things-that-broke-our-polymarket-trading-bot-before-it-made-a-dollar</link>
<description>&lt;p&gt;We spent a few weeks trying to build a profitable Polymarket bot. Arbitrage between linked markets, in-play market making, copying wallets that trade before goals, betting against miscalibrated prices. We recorded the full order book for every football market, indexed on-chain trades and replayed each idea.&lt;/p&gt;
&lt;p&gt;None of them made money. The full research with numbers is &lt;a rel=&quot;nofollow&quot; href=&quot;https://orcalayer.com/blog/polymarket-strategies-that-dont-work&quot;&gt;on our blog&lt;/a&gt;, and the CSVs are &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/orcalayer/polymarket-arbitrage-market-making-copy-trading-backtests&quot;&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This post is the engineering side: six things about the Polymarket API and our own pipeline that would have broken the bot even if a strategy had worked.&lt;/p&gt;
&lt;h3 id=&quot;1-your-taker-order-waits-a-second-and-you-can-t-cancel-it&quot;&gt;1. Your taker order waits a second, and you can't cancel it&lt;/h3&gt;
&lt;p&gt;Sports markets have a field in the Gamma API called &lt;code&gt;secondsDelay&lt;/code&gt;. Football is &lt;code&gt;1&lt;/code&gt;, NBA is &lt;code&gt;0&lt;/code&gt;. You can see it on any event:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET https://gamma-api.polymarket.com/events?slug=fif-sri-mri-2026-10-05
... &quot;secondsDelay&quot;: 1 ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Per the &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.polymarket.com/concepts/order-lifecycle&quot;&gt;Order Lifecycle docs&lt;/a&gt;, a marketable order waits out that delay before matching, and &quot;during either delay, the order is pending and cannot be canceled&quot;. Resting limit orders can still be cancelled. Some crypto and finance up/down markets have a 250 ms version of the same thing.&lt;/p&gt;
&lt;p&gt;What it did to us: our arbitrage detector found 369 mispriced sets across linked football markets in 18 hours. Of the 2c+ ones we rechecked one second later, all legs were still in the book in 3, worth $1.83 total. Makers pull their quotes inside your delay, or a faster taker takes the legs. If your bot sees a price and hits it, check &lt;code&gt;secondsDelay&lt;/code&gt; first.&lt;/p&gt;
&lt;h3 id=&quot;2-fees-are-not-a-percentage&quot;&gt;2. Fees are not a percentage&lt;/h3&gt;
&lt;p&gt;Only takers pay, and the fee depends on price:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def taker_fee(shares: float, price: float, fee_rate: float) -&amp;gt; float:
    # fee_rate from Gamma feeSchedule.rate, e.g. 0.05 for sports
    return shares * fee_rate * price * (1 - price)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It peaks at 0.50 (&lt;code&gt;p * (1 - p) = 0.25&lt;/code&gt;) and goes to zero near 0 and 1. A 1c edge on a 50c contract is mostly eaten. Rates and maker rebates are in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.polymarket.com/trading/fees.md&quot;&gt;fees docs&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;3-the-mempool-gives-you-nothing&quot;&gt;3. The mempool gives you nothing&lt;/h3&gt;
&lt;p&gt;Orders and cancels are off-chain (EIP-712 signatures), only the settlement goes to Polygon. We measured:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Source &lt;/th&gt;&lt;th&gt;Lag behind matching&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Order book WebSocket &lt;/td&gt;&lt;td&gt;~17 ms&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;On-chain settlement &lt;/td&gt;&lt;td&gt;~2 s median&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;By the time a trade shows up in the Polygon mempool, it already happened and the book already moved. Watch the WebSocket, not the chain.&lt;/p&gt;
&lt;h3 id=&quot;4-websocket-events-arrive-out-of-order&quot;&gt;4. WebSocket events arrive out of order&lt;/h3&gt;
&lt;p&gt;The exchange can send &lt;code&gt;price_change&lt;/code&gt; (a level got eaten) before the &lt;code&gt;last_trade_price&lt;/code&gt; event of the same trade. We replayed fills in arrival order at first, and every fill looked about 25% worse than it really was. Sort by the trade, not by arrival time, before you trust any replay.&lt;/p&gt;
&lt;p&gt;For what it's worth, a plain WebSocket recorder (&lt;code&gt;price_change&lt;/code&gt;, &lt;code&gt;book&lt;/code&gt;, &lt;code&gt;last_trade_price&lt;/code&gt; with tx hash) captured 99.9% of the trades that later appeared on-chain, so you don't need anything fancier to build a replay dataset.&lt;/p&gt;
&lt;h3 id=&quot;5-your-backtest-can-lie-with-perfect-data&quot;&gt;5. Your backtest can lie with perfect data&lt;/h3&gt;
&lt;p&gt;This one wasn't the API, it was us. We picked wallets that bought right before goals and simulated copying them 1-5 seconds later: +$4.23 per $10. Then we noticed we had only selected the buys that came right before goals. In real time you don't know which buys those are.&lt;/p&gt;
&lt;p&gt;Rerun on every buy by the same wallets, without looking at the outcome: -$0.41 per $10.&lt;/p&gt;
&lt;p&gt;Any filter that uses something you only learn later (a goal, a resolution, a price move) has to be removed from the selection step. If the result survives that, then you can start believing it.&lt;/p&gt;
&lt;h3 id=&quot;6-some-polymarket-bot-repos-want-your-private-key&quot;&gt;6. Some &quot;Polymarket bot&quot; repos want your private key&lt;/h3&gt;
&lt;p&gt;Before you reach for an existing open-source bot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.stepsecurity.io/blog/malicious-polymarket-bot-hides-in-hijacked-dev-protocol-github-org-and-steals-wallet-keys&quot;&gt;StepSecurity&lt;/a&gt; found 20+ fake repos with bought stars (&lt;code&gt;polymarket-trading-bot-new&lt;/code&gt;, &lt;code&gt;polymarket-copy-trading-bot-sports&lt;/code&gt;, ...) pulling typosquatted npm packages that steal keys and &lt;code&gt;.env&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://safedep.io/malicious-polymarket-npm-crypto-wallet-drainer/&quot;&gt;SafeDep&lt;/a&gt; found 9 npm packages (&lt;code&gt;polymarket-bot&lt;/code&gt;, &lt;code&gt;polymarket-claude-code&lt;/code&gt;, ...) that ask you to paste your wallet key.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Read the code and the dependency tree before anything touches a funded key.&lt;/p&gt;
&lt;h3 id=&quot;smaller-gamma-api-notes&quot;&gt;Smaller Gamma API notes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;offset&lt;/code&gt; pagination returns 422 somewhere after 2,100-2,500 records. Use the keyset endpoints for a full crawl.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/events/keyset&lt;/code&gt; ignores the &lt;code&gt;negRisk&lt;/code&gt; filter, filter on the market field instead. &lt;code&gt;/markets/keyset&lt;/code&gt; respects &lt;code&gt;end_date_min&lt;/code&gt; / &lt;code&gt;end_date_max&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Some requests fail without a &lt;code&gt;User-Agent&lt;/code&gt; header.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;The data is CC BY 4.0. If you're building something on Polymarket and want to check our numbers, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/orcalayer/polymarket-arbitrage-market-making-copy-trading-backtests&quot;&gt;repo&lt;/a&gt; has every CSV, and we'd like to hear where we got it wrong.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://orcalayer.com/blog/polymarket-strategies-that-dont-work&quot;&gt;orcalayer.com&lt;/a&gt;. OrcaLayer is an independent Polymarket analytics provider, not affiliated with Polymarket.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30137/6-things-that-broke-our-polymarket-trading-bot-before-it-made-a-dollar</guid>
<pubDate>Wed, 07 Oct 2026 12:47:20 +0000</pubDate>
</item>
<item>
<title>The IRS Handed Back 96.7% of the Money on Its AI Coding-Tools Order. The Record Says When, Not Why.</title>
<link>https://coderlegion.com/29073/the-irs-handed-back-96-7-of-the-money-on-its-ai-coding-tools-order-the-record-says-when-not-why</link>
<description>&lt;blockquote&gt;&lt;p&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/blog/the-irs-handed-back-96-7-of-the-money-on-its-ai-coding-tools/&quot;&gt;vibeagentmaking.com&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;One IRS delivery order for AI coding and chat tools was obligated at $3.31 million on the last day of fiscal 2025 and de-obligated by 96.7% nine months later. A census of all 449 fiscal-2026 federal AI contract actions shows how unusual that shape is, and why the record cannot say why.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The federal contract record is a plain document. It does not editorialise, it rarely explains, and when an agency changes its mind it writes the change down in capital letters and moves on. Here are the two lines that make up the entire recorded life of one purchase.&lt;/p&gt;
&lt;p&gt;On September 29, 2025, with one day left in the federal fiscal year, the Internal Revenue Service obligated $3,314,392.00 to a company called Sokat Consulting LLC. The award text reads, in full: &quot;THIS IS DELIVERY ORDER 1 FOR ARTIFICIAL INTELLIGENCE CODING TOOLS AND ARTIFICIAL INTELLIGENCE CHAT TOOLS AGAINST 2032L225D00002.&quot; The period of performance ran from September 30, 2025 to September 29, 2026. The product code says perpetual-license business application software. The ceiling on the order, the most it could ever have grown to, was $5,080,563.10.&lt;/p&gt;
&lt;p&gt;On July 9, 2026, nine months and ten days later, a modification numbered P00001 took $3,205,416.90 of that money back. Its text: &quot;THIS IS A REQ FOR MOD TO DE-OBLIGATE FUNDING FOR ARTIFICIAL INTELLIGENCE CODING TOOLS AND ARTIFICIAL INTELLIGENCE CHAT TOOLS AGAINST 2032L225D00002.&quot; Its action-type code is C, which the record itself defines as &quot;funding only action&quot;.&lt;/p&gt;
&lt;p&gt;That is all there is. What is left on the order is $108,975.10, which is 3.3% of what was obligated. The de-obligation landed with about twelve weeks still to run on a one-year order. The vehicle the order sits on, contract 2032L225D00002, has exactly one order on it, this one, as of September 21, 2026, both by a text search for the vehicle's number and by the vehicle's own list of orders in the award database. There is no delivery order 2.&lt;/p&gt;
&lt;p&gt;I want to be careful about what I am doing with these two lines, because the temptation is obvious and I have watched other people give in to it. A 96.7% de-obligation on an AI coding-tools order, at an agency that has been in the news for staffing cuts, in the year everyone is buying AI coding tools, is a story that writes itself. The problem is that the record does not write it. No document in the award file gives a reason beyond the modification's own title. So the rest of this piece is not about why the IRS did this. It is about the only question the record can actually answer: how unusual is it?&lt;/p&gt;
&lt;h3 id=&quot;the-population&quot;&gt;The population&lt;/h3&gt;
&lt;p&gt;To answer that I need every comparable line, not one. The government's own award database, USAspending, has an API that returns contract transactions by keyword, so on September 18, 2026 I pulled every federal contract action in fiscal 2026 (October 1, 2025 through the day of the pull) whose text matches &quot;artificial intelligence&quot;. That is the whole selection rule. It matches the award's own description, so it catches awards that say they are about AI and misses awards that do AI work without saying so. It is a census of what the text says, and every total below should be read that way.&lt;/p&gt;
&lt;p&gt;The pull returned 449 transactions: $358,899,346.16 obligated, $14,990,819.87 de-obligated, the de-obligations spread across 34 negative lines. So the naive headline is available immediately: fifteen million dollars of federal AI money went back this year. The naive headline is wrong, and the way it is wrong is the useful part.&lt;/p&gt;
&lt;p&gt;A negative line in this database means the obligated amount on an award went down. It does not say why, and there are at least four different events that produce one. The API records, on every transaction, a one-letter action-type code and the plain-English name of that code, and I took each code's meaning from the row that carried it rather than from memory. F means &quot;terminate for convenience (complete or partial)&quot;. K means &quot;close out&quot;. C means &quot;funding only action&quot;. B means &quot;supplemental agreement for work within scope&quot;.&lt;/p&gt;
&lt;p&gt;Then I did the thing the headline number skips: I opened each of the 34 awards and read every modification on it, and sorted the negative lines into four bins.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Offsets.&lt;/strong&gt; Two lines, $8,162,903.45. The same award put the same amount back within days. The largest single negative line in the whole year belongs to this bin: on May 29, 2026, ECS Federal's Army contract for AI/ML algorithms and models (W911QX25C0002) shows a &quot;supplemental agreement for work within scope&quot; of minus $7,812,903.45, and on June 3, five days later, a &quot;funding only action&quot; of plus $7,812,903.45. Read the negative line alone and 52.1% of the year's &quot;returned AI money&quot; is a single accounting pair. Read the award's history and the contract grew all year: plus $6,737,560.46 in January, plus $4,475,886.00 in February, plus $10,028,288.07 in March, plus $4,941,047.00 in May. It is obligated at $72,819,404.20 against a $97,637,874.98 ceiling. Nothing came back.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Closeouts.&lt;/strong&gt; Five lines, $230,099.96. The administrative close of a finished contract, where whatever was not spent is released. This is the sound of a contract ending normally.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Terminations.&lt;/strong&gt; Three lines, $1,926,136.26. The government formally ended remaining work. These are the only lines in the year that carry the termination code, and I will come back to all three.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Standing de-obligations.&lt;/strong&gt; Twenty-four lines, $4,671,680.20. Money went back and did not come back within thirty days on the same award, and the line carries no termination code. This is the bin the IRS order sits in, and it sits in it the way a whale sits in a pond. The IRS line is $3,205,416.90 of the bin's $4,671,680.20, which is 68.6%. Counting the closeouts and terminations too, it is nearly half of every dollar that left a federal AI contract this year and stayed gone.&lt;/p&gt;
&lt;p&gt;So the answer to &quot;how unusual&quot; is: very. Not because the amount is large in federal terms, it is not, but because the shape is. The rest of the year's pull-backs are either bookkeeping, or the ordinary end of a contract, or one of three explicit terminations. The IRS line is none of those. It is the largest thing in the database that went back, stayed back, and was never called a termination.&lt;/p&gt;
&lt;h3 id=&quot;what-the-three-real-terminations-look-like&quot;&gt;What the three real terminations look like&lt;/h3&gt;
&lt;p&gt;It helps to see what the record looks like when the government does end AI work on purpose, because the contrast with the IRS line is the point.&lt;/p&gt;
&lt;p&gt;The Securities and Exchange Commission, December 8, 2025. Award 50310225F0034 to NumInformatics Inc, described in the record as &quot;OIT AIML&quot;. The termination line is the most explicit sentence in this whole census: &quot;PARTIAL TERMINATION FOR CONVENIENCE AGREEMENT FOR OFFICE OF INFORMATION TECHNOLOGY ARTIFICIAL INTELLIGENCE MACHINE LEARNING (AIML). TERMINATED ALL REMAINING AIML WORK BEYOND THE TERMINATION OF CONVENIENCE NOTICE, TO INCLUDE ALL OPTIONAL OUTYEARS.&quot; It took back $851,509.27 of $1,333,740.53 and left $482,231.26, 36.2% of what had been obligated. When the SEC ended its AI/ML support work, the record says so in the record's own vocabulary.&lt;/p&gt;
&lt;p&gt;The Patent and Trademark Office, August 20, 2026. Award 1333BJ24C00280005 to Accenture Federal Services, described as &quot;PATENT SEARCH ARTIFICIAL INTELLIGENCE MODELS AND LABOR. THIS ACTION IS AWARDED PURSUANT TO THE USPTO EFFICIENCY ACT.&quot; The termination line takes back $624,728.99 and is titled &quot;PATENT SEARCH ARTIFICIAL INTELLIGENCE DEVELOPMENT SECURITY OPERATIONS SUPPORT.&quot; That is a partial termination of a slice: the award still carries $27,753,766.76 obligated against a $55,322,899.54 ceiling, so the terminated amount is 2.2% of what had been obligated before it. The patent-search AI work did not end; a piece of it did, and the record names the piece.&lt;/p&gt;
&lt;p&gt;The Air Force, March 26, 2026. Award FA864924P1125 to AI2 Incorporated, &quot;SECURE LARGE LANGUAGE MODELS TO ENABLE GENERAL ARTIFICIAL INTELLIGENCE FOR CRITICAL DEPARTMENT OF THE AIR FORCE APPLICATIONS.&quot; The termination took back $449,898.00 of $1,799,592.00, which is exactly 25.0%, to the dollar. The award record lists the period of performance as August 16, 2024 to December 16, 2025, so the termination line is dated three months after the recorded end of performance. I do not know what that sequence means and the record does not say; I note it because a reader who goes to the award page will see it.&lt;/p&gt;
&lt;p&gt;Three terminations, $1.93 million between them, each one labelled as a termination by the agency that made it; two of them left most of the award in place, and the SEC's left 36.2%. That is what the federal government ending AI work looks like in fiscal 2026 when it chooses to say so. It is small, it is partial, and it is named.&lt;/p&gt;
&lt;h3 id=&quot;what-a-de-obligation-is-not&quot;&gt;What a de-obligation is not&lt;/h3&gt;
&lt;p&gt;The IRS modification is coded C, a funding action, not F, a termination. That is not a technicality; it is the difference between what the record asserts and what a reader might assume. A funding-only de-obligation is the mechanism an agency uses when money on an award will not be needed. It is used when a vendor delivers under budget, when a scope is cut, when a programme is cancelled, when a licence count is reduced, when the funds are needed elsewhere before the fiscal year closes, and for reasons that never make it into a title field at all. The record does not distinguish between these. Reading &quot;de-obligate funding&quot; as &quot;the tools failed&quot; is a guess, and I am not going to make it, and I would ask you not to make it on my behalf.&lt;/p&gt;
&lt;p&gt;There is a second thing this piece does not claim, and it comes from an earlier one. In &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/blog/1-45-billion-federal-ai-contracts-ledger-accounts-for-17-percent/&quot;&gt;a previous look at the same ledger&lt;/a&gt; I found that the outlay column, the field that is supposed to say what was actually paid, is missing, zero, or partial on most federal AI awards, and cannot be used to say what a vendor received. So I am not claiming that Sokat was paid the $108,975.10 that remains on the order, or that it was not, or that any part of the $3.3 million was ever drawn before it went back. Obligation is a promise of money. This record is a record of promises, and of one promise mostly withdrawn.&lt;/p&gt;
&lt;p&gt;What the record does establish is narrower and, I think, more interesting than the guess. An agency committed $3.3 million, with one day left to spend that year's money, to a category of software that every large organisation is currently being told it needs. Nine months later it kept 3.3% of the commitment. It did not call this a termination. It did not place a second order on the vehicle it had set up to place orders on. Every one of those facts is a line in a public database, and none of them is an opinion.&lt;/p&gt;
&lt;h3 id=&quot;how-to-read-a-number-like-96-7-&quot;&gt;How to read a number like 96.7%&lt;/h3&gt;
&lt;p&gt;I want to say something about the number itself, because it is the kind of number that travels, and numbers that travel lose their denominators on the way.&lt;/p&gt;
&lt;p&gt;96.7% is the de-obligated amount over the obligated amount on one delivery order. It is not the IRS's AI budget, or its software budget, or anything about the IRS's other AI spending, which in this census consists of unrelated buys: $1,734,979.00 to MITRE on September 9, 2026, $485,864.89 to Unison for a &quot;Virtual Acquisition Office&quot; on August 29, and two zero-dollar administrative lines. It is not a statement about the vendor's product, which the record never describes beyond &quot;coding tools&quot; and &quot;chat tools&quot;. It is not a statement about whether anyone at the IRS used the tools; the record has no field for that. And it is not evidence that AI coding tools are being returned across the government: in the same year and the same database, the sum of every standing de-obligation on every AI-described contract other than this one is $1,466,263.30, spread across 23 lines, the largest of them $394,959.23.&lt;/p&gt;
&lt;p&gt;What 96.7% is, is a precise description of one event with no explanation attached. That makes it useful in exactly one way. Someone who wants to know why a large federal AI purchase went nearly all the way back now knows which purchase to ask about, which agency, which order number, which modification, and which date. A Freedom of Information Act request for the justification behind modification P00001 on order 2032L225F00049 would get an answer the API cannot give. So would the IRS's fiscal 2026 information-technology spend plan. So, for that matter, would a phone call to the vendor.&lt;/p&gt;
&lt;h3 id=&quot;the-method-so-you-can-check-it&quot;&gt;The method, so you can check it&lt;/h3&gt;
&lt;p&gt;Everything above comes from three small scripts run against the USAspending API on September 18, 2026 at 00:43 UTC, published alongside this piece together with the raw responses they saved. The first, &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/code/ai_termination_census.py&quot;&gt;ai_termination_census.py&lt;/a&gt;, pulls the census: &lt;code&gt;POST /api/v2/search/spending_by_transaction/&lt;/code&gt; with the keyword &quot;artificial intelligence&quot;, award types A through D (the contract types), and the fiscal-2026 date window. The second, &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/code/deob_classify.py&quot;&gt;deob_classify.py&lt;/a&gt;, takes every award that carried a negative line and pulls its full modification history from &lt;code&gt;POST /api/v2/transactions/&lt;/code&gt;, then classifies each negative line as an offset (the same award re-obligated the same amount within thirty days), a termination (code F), a closeout (code K), or standing (none of those). The third, &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/code/award_detail.py&quot;&gt;award_detail.py&lt;/a&gt;, pulls each named award's own record from &lt;code&gt;GET /api/v2/awards/&lt;/code&gt;, which is where the descriptions, periods and ceilings quoted here come from. A fourth script, &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/code/figures_c6153.py&quot;&gt;figures_c6153.py&lt;/a&gt;, reads the saved responses back and recomputes every figure quoted above, so a number that drifts from its source fails the run; it makes no network call.&lt;/p&gt;
&lt;p&gt;The limits are facts about the measurement rather than hedges. The keyword selects text, not a curated list of AI programmes. Fiscal 2026 is partial, ending at the pull date. Only contract actions are counted, not grants or the ceilings of the vehicles themselves. The thirty-day offset window is a parameter; a re-obligation on day forty-five, or on a sibling award, reads as standing here. Seven rows carry action codes (W, H, L) that I did not resolve to descriptions; none of them is negative, so none affects any figure above. And once more, because it is the sentence most likely to be dropped when this gets quoted: a de-obligation is money returned, not a verdict on the vendor, the programme, or the tools.&lt;/p&gt;
&lt;p&gt;The order still has $108,975.10 on it and eight days of performance left as I write this. On September 30 the fiscal year turns over, the vehicle will either get a delivery order 2 or it will not, and the database will record which, in capital letters, without saying why.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Every dollar figure, date, percentage and quoted record line in this piece was checked against the saved API responses by a script published with it, &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/code/figures_c6153.py&quot;&gt;figures_c6153.py&lt;/a&gt;, which reloads the 449-row census and the per-award modification histories, recomputes the four bins and every derived number, and asserts each quoted amount is present in the rows. A figure missing from the rows fails the run. The three collection scripts and the raw responses they saved (&lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/code/data/ai_fy26.json&quot;&gt;ai_fy26.json&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/code/data/ai_fy26_verdicts.json&quot;&gt;ai_fy26_verdicts.json&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/code/data/awards.out.txt&quot;&gt;awards.out.txt&lt;/a&gt;) are published beside it, with the two checks of the vehicle's orders (&lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/code/data/vehicle_2032L225D00002_recheck.json&quot;&gt;vehicle_2032L225D00002_recheck.json&lt;/a&gt;, the text search, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/code/data/idv_child_awards_2032L225D00002.json&quot;&gt;idv_child_awards_2032L225D00002.json&lt;/a&gt;, the vehicle's own list of orders).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;USAspending.gov award record, delivery order 2032L225F00049 (Sokat Consulting LLC, Department of the Treasury / Internal Revenue Service, parent IDV 2032L225D00002): the September 29, 2025 award and modification P00001 of July 9, 2026, quoted verbatim. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.usaspending.gov/award/CONT_AWD_2032L225F00049_2050_2032L225D00002_2050&quot;&gt;https://www.usaspending.gov/award/CONT_AWD_2032L225F00049_2050_2032L225D00002_2050&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;USAspending.gov record of the vehicle itself, IDV 2032L225D00002, whose list of orders holds one award as of September 21, 2026. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.usaspending.gov/award/CONT_IDV_2032L225D00002_2050&quot;&gt;https://www.usaspending.gov/award/CONT_IDV_2032L225D00002_2050&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;USAspending.gov award record, W911QX25C0002 (ECS Federal, LLC, Department of the Army): modifications P00010 through P00015, the May 29 and June 3, 2026 offset pair. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.usaspending.gov/award/CONT_AWD_W911QX25C0002_9700_-NONE-_-NONE-&quot;&gt;https://www.usaspending.gov/award/CONT_AWD_W911QX25C0002_9700_-NONE-_-NONE-&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;USAspending.gov award record, 50310225F0034 (NumInformatics Inc, Securities and Exchange Commission): modification P00002 of December 8, 2025, the partial termination for convenience, quoted verbatim. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.usaspending.gov/award/CONT_AWD_50310225F0034_5000_47QTCA24D00G1_4732&quot;&gt;https://www.usaspending.gov/award/CONT_AWD_50310225F0034_5000_47QTCA24D00G1_4732&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;USAspending.gov award record, 1333BJ24C00280005 (Accenture Federal Services LLC, U.S. Patent and Trademark Office): modification P26012 of August 20, 2026. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.usaspending.gov/award/CONT_AWD_1333BJ24C00280005_1344_-NONE-_-NONE-&quot;&gt;https://www.usaspending.gov/award/CONT_AWD_1333BJ24C00280005_1344_-NONE-_-NONE-&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;USAspending.gov award record, FA864924P1125 (AI2 Incorporated, Department of the Air Force): modification P00003 of March 26, 2026. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.usaspending.gov/award/CONT_AWD_FA864924P1125_9700_-NONE-_-NONE-&quot;&gt;https://www.usaspending.gov/award/CONT_AWD_FA864924P1125_9700_-NONE-_-NONE-&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;USAspending API, &lt;code&gt;POST /api/v2/search/spending_by_transaction/&lt;/code&gt;, &lt;code&gt;POST /api/v2/transactions/&lt;/code&gt; and &lt;code&gt;GET /api/v2/awards/{id}/&lt;/code&gt;, pulled September 18, 2026 00:43 UTC; action-type code meanings taken from each response's own &lt;code&gt;action_type_description&lt;/code&gt; field. &lt;a rel=&quot;nofollow&quot; href=&quot;https://api.usaspending.gov/docs/endpoints&quot;&gt;https://api.usaspending.gov/docs/endpoints&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Treasury Inspector General for Tax Administration, &quot;Snapshot Report: IRS Workforce Reductions as of May 2025&quot;, Report No. 2025-IE-R027, July 18, 2025: 25,386 employees separated, took a deferred resignation offer or used another incentive to leave, 25 percent of the IRS's workforce. The source for the staffing cuts. &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.tigta.gov/sites/default/files/reports/2025-08/2025ier027fr.pdf&quot;&gt;https://www.tigta.gov/sites/default/files/reports/2025-08/2025ier027fr.pdf&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&quot;The $1.45 Billion Federal AI Contracts Ledger Accounts for 17 Percent&quot;, this site, on the outlay column's reliability. &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/blog/1-45-billion-federal-ai-contracts-ledger-accounts-for-17-percent/&quot;&gt;https://vibeagentmaking.com/blog/1-45-billion-federal-ai-contracts-ledger-accounts-for-17-percent/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Data &amp; AI</category>
<guid isPermaLink="true">https://coderlegion.com/29073/the-irs-handed-back-96-7-of-the-money-on-its-ai-coding-tools-order-the-record-says-when-not-why</guid>
<pubDate>Tue, 29 Sep 2026 12:55:06 +0000</pubDate>
</item>
<item>
<title>Polymarket Gamma API endDate Is Not the Resolution Deadline</title>
<link>https://coderlegion.com/28532/polymarket-gamma-api-enddate-is-not-the-resolution-deadline</link>
<description>&lt;p&gt;&lt;em&gt;Checked against the Polymarket Gamma API on 24 September 2026.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Short answer.&lt;/strong&gt; OrcaLayer compared the &lt;code&gt;endDate&lt;/code&gt; field with the written rules on 23 Polymarket markets. Polymarket's documentation describes a market's end date as the point when it becomes eligible for resolution. The field did not behave that way on sports markets: on NFL and soccer matches it was the kickoff, on esports the start plus 6 hours, on tennis the original start plus 7 days. The deadline that governs a market is in its &lt;code&gt;description&lt;/code&gt;, and whether it still trades is in &lt;code&gt;closed&lt;/code&gt; and &lt;code&gt;acceptingOrders&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you build on Polymarket data, this changes what your code should read. A bot that stops trading at &lt;code&gt;endDate&lt;/code&gt;, a dashboard that counts down to it, and a model that treats it as the resolution time are all reading a field that means different things on different market types.&lt;/p&gt;
&lt;h3 id=&quot;what-is-enddate-in-the-polymarket-gamma-api-&quot;&gt;What is endDate in the Polymarket Gamma API?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;endDate&lt;/code&gt; is a UTC timestamp on every market in the Polymarket Gamma API. The API does not document what it represents, and it is not the resolution deadline. In OrcaLayer's sample it was kickoff on NFL and soccer matches, start plus 6 hours on esports, and original start plus 7 days on tennis. The binding deadline is in &lt;code&gt;description&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Every market returned by &lt;code&gt;https://gamma-api.polymarket.com/markets/{id}&lt;/code&gt; carries &lt;code&gt;endDate&lt;/code&gt; at the top level. If you are new to these endpoints, start with our &lt;a rel=&quot;nofollow&quot; href=&quot;https://orcalayer.com/blog/polymarket-rest-api-developers&quot;&gt;Polymarket REST API guide for developers&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The response usually tells you what kind of market you are looking at. Sports matches carry &lt;code&gt;gameStartTime&lt;/code&gt;, &lt;code&gt;eventStartTime&lt;/code&gt;, &lt;code&gt;gameId&lt;/code&gt; and &lt;code&gt;sportsMarketType&lt;/code&gt; (for example &lt;code&gt;&quot;moneyline&quot;&lt;/code&gt;). Markets that belong to a group carry &lt;code&gt;groupItemTitle&lt;/code&gt;, which can be a label such as &lt;code&gt;&quot;Match Winner&quot;&lt;/code&gt; or a date such as &lt;code&gt;&quot;September 30&quot;&lt;/code&gt;. Closed markets add &lt;code&gt;closedTime&lt;/code&gt; and &lt;code&gt;umaEndDate&lt;/code&gt;. The rules are in plain English in &lt;code&gt;description&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Polymarket's own &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.polymarket.com/concepts/resolution&quot;&gt;resolution documentation&lt;/a&gt; puts it plainly: &quot;Always read the resolution rules before trading.&quot; The rest of this article shows what happens when code reads &lt;code&gt;endDate&lt;/code&gt; instead.&lt;/p&gt;
&lt;h3 id=&quot;polymarket-enddate-by-market-type&quot;&gt;Polymarket endDate by market type&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Market type &lt;/th&gt;&lt;th&gt;Markets &lt;/th&gt;&lt;th&gt;What endDate was &lt;/th&gt;&lt;th&gt;What the rules say &lt;/th&gt;&lt;th&gt;Closed before or after endDate&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Esports (CS2, LoL, Dota 2) &lt;/td&gt;&lt;td&gt;4 &lt;/td&gt;&lt;td&gt;Scheduled start + exactly 6 hours, 4 of 4 &lt;/td&gt;&lt;td&gt;A postponed match must be rescheduled to start within 14 days, or the market resolves 50-50 &lt;/td&gt;&lt;td&gt;All 4 before, by 19 minutes to 3 h 18 min&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Tennis (ATP, ITF) &lt;/td&gt;&lt;td&gt;4 &lt;/td&gt;&lt;td&gt;Original scheduled start + exactly 7 days, 4 of 4 &lt;/td&gt;&lt;td&gt;A winner must be determined within 14 days, or the market resolves 50-50 &lt;/td&gt;&lt;td&gt;All 4 before, by 6.3 to 6.8 days&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;NFL, college football, soccer matches &lt;/td&gt;&lt;td&gt;4 &lt;/td&gt;&lt;td&gt;Kickoff, 4 of 4 &lt;/td&gt;&lt;td&gt;No fixed deadline: open until the game is completed &lt;/td&gt;&lt;td&gt;All 4 after, by 2 h 17 min to 4 h 57 min&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;MLB match &lt;/td&gt;&lt;td&gt;1 &lt;/td&gt;&lt;td&gt;Start + 7 days &lt;/td&gt;&lt;td&gt;No fixed deadline &lt;/td&gt;&lt;td&gt;Before, by about 7 days&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Sports futures &lt;/td&gt;&lt;td&gt;3 &lt;/td&gt;&lt;td&gt;Varies &lt;/td&gt;&lt;td&gt;A &quot;postponed after&quot; backstop &lt;/td&gt;&lt;td&gt;Still open&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Deadline and price markets &lt;/td&gt;&lt;td&gt;7 &lt;/td&gt;&lt;td&gt;Matched the rules in 4 of 7 &lt;/td&gt;&lt;td&gt;&quot;By&quot; a date, or a date in the title &lt;/td&gt;&lt;td&gt;Mixed&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The sample is 23 hand-picked markets covering each type, so read each row as an observed pattern, not a platform-wide rate.&lt;/p&gt;
&lt;h3 id=&quot;esports-enddate-is-the-start-plus-six-hours&quot;&gt;Esports: endDate is the start plus six hours&lt;/h3&gt;
&lt;p&gt;All four esports matches had an &lt;code&gt;endDate&lt;/code&gt; exactly six hours after &lt;code&gt;gameStartTime&lt;/code&gt;. The Counter-Strike match between QUAZAR and Misa Esports started at 08:00 UTC on 23 September, carried an &lt;code&gt;endDate&lt;/code&gt; of 14:00 UTC, and closed at 13:41 UTC.&lt;/p&gt;
&lt;p&gt;Six hours is a fair guess for when a best-of-three finishes, but the rules for the same market allow far longer:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;If the match is postponed, it must be rescheduled to a start time on or before October 7, 2026 at 11:59 PM ET (14 calendar days after the originally scheduled start of September 23, 2026 at 4:00 AM ET). If, by that deadline, the most recently announced rescheduled start time is on or before the deadline, this market resolves based on the result of the match as played. Otherwise, this market resolves 50-50, regardless of whether the match is subsequently played.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The 14 days apply to the rescheduled start, not to the result. A match rescheduled to start on 7 October can resolve later still, long after &lt;code&gt;endDate&lt;/code&gt; has passed. You can see current esports markets on &lt;a rel=&quot;nofollow&quot; href=&quot;https://orcalayer.com/esports&quot;&gt;OrcaLayer's esports page&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;tennis-seven-days-after-the-original-schedule-and-it-does-not-move&quot;&gt;Tennis: seven days after the original schedule, and it does not move&lt;/h3&gt;
&lt;p&gt;Every tennis match in the sample had an &lt;code&gt;endDate&lt;/code&gt; exactly seven days after its originally scheduled start. Here the 14-day rule applies to the result:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;If the match is canceled (not played at all), ends in a tie, or a winner has not been determined by October 6, 2026, 11:59 PM ET (14 days after the scheduled start), this market will resolve to 50-50.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Rescheduling is where it goes wrong. In two ATP matches, &lt;code&gt;gameStartTime&lt;/code&gt; was later than the start printed in the rules. The US Open match between Ben Shelton and Carlos Alcaraz was scheduled for 8 September at 15:30 UTC and started on 9 September at 03:00 UTC. Its &lt;code&gt;endDate&lt;/code&gt; was 15 September at 15:30 UTC: seven days after the original slot, not the new one.&lt;/p&gt;
&lt;p&gt;A countdown built on &lt;code&gt;endDate&lt;/code&gt; for a tennis market counts from a schedule that may no longer exist. Take the start from &lt;code&gt;gameStartTime&lt;/code&gt; and the deadline from the rules.&lt;/p&gt;
&lt;h3 id=&quot;nfl-and-soccer-markets-enddate-is-kickoff-not-the-end-of-the-game&quot;&gt;NFL and soccer markets: endDate is kickoff, not the end of the game&lt;/h3&gt;
&lt;p&gt;On the Broncos vs Chiefs NFL game, the SMU vs Louisville college football game, a La Liga match and a Romanian Liga I match, &lt;code&gt;endDate&lt;/code&gt; was equal to kickoff to the second. All four markets closed after it, between 2 h 17 min and 4 h 57 min later, which is how long the games took.&lt;/p&gt;
&lt;p&gt;The rules on these markets set no deadline at all:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;If the game is postponed, this market will remain open until the game has been completed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A kickoff is the one moment when a match market is certainly not ready to resolve. Code that compares the current time with &lt;code&gt;endDate&lt;/code&gt; would have marked all four of these markets as finished at kickoff, while the games were being played and the markets were still trading.&lt;/p&gt;
&lt;p&gt;Across the 16 closed markets in the sample, 6 closed after their &lt;code&gt;endDate&lt;/code&gt;. Four were these matches. The other two were a Bitcoin price market that closed 14 minutes after its reference time and a Fed rate market, covered below.&lt;/p&gt;
&lt;h3 id=&quot;sports-futures-sometimes-the-event-sometimes-the-backstop&quot;&gt;Sports futures: sometimes the event, sometimes the backstop&lt;/h3&gt;
&lt;p&gt;League and tournament winner markets followed no single rule. On Shakhtar Donetsk winning the 2026-27 UEFA Champions League, &lt;code&gt;endDate&lt;/code&gt; was 30 May 2027 at 23:59 UTC, while the rules fall back to &quot;Other&quot; only if the season is &quot;postponed after June 19, 2027, 11:59 PM ET&quot;, about twenty days later. The New York Yankees World Series market had an &lt;code&gt;endDate&lt;/code&gt; of 31 October 2026, and its rules allow postponement until 31 December 2026. The Boston Celtics NBA Finals market matched its rules to the minute.&lt;/p&gt;
&lt;p&gt;For futures, the field alone does not tell you whether you are looking at an expected event date or the formal backstop.&lt;/p&gt;
&lt;h3 id=&quot;deadline-markets-right-in-4-of-7-once-wrong-by-92-days&quot;&gt;Deadline markets: right in 4 of 7, once wrong by 92 days&lt;/h3&gt;
&lt;p&gt;Markets that ask whether something happens &quot;by&quot; a date came closest to what most people assume &lt;code&gt;endDate&lt;/code&gt; means. Two of them, &quot;Will Anthropic or OpenAI IPO first?&quot; and the Clarity Act market, matched the deadline in the rules, one with a one-minute offset (05:00 instead of 04:59 UTC). The two Bitcoin price markets also matched, though their rules point to &quot;the date specified in the title&quot; or &quot;the month specified in the title&quot;, so the deadline lives in the market name.&lt;/p&gt;
&lt;p&gt;Three did not match:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&quot;Will the US confirm that aliens exist by September 30?&quot;&lt;/strong&gt; The rules say &quot;by September 30, 2026, 11:59 PM ET&quot;, which is 1 October at 03:59 UTC. &lt;code&gt;endDate&lt;/code&gt; was 1 January 2027 at 04:59 UTC, &lt;strong&gt;92 days later&lt;/strong&gt;. That is exactly the &lt;code&gt;endDate&lt;/code&gt; of the parent event the market belongs to, which runs to the end of 2026, so the market appears to carry its event's date rather than its own. Two other fields in the same response point the right way or the wrong way: &lt;code&gt;groupItemTitle&lt;/code&gt; is &lt;code&gt;&quot;September 30&quot;&lt;/code&gt;, which matches the rules, and &lt;code&gt;hasReviewedDates&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;, which shows that a reviewed flag does not guarantee the date matches the rules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Fed rate market for the September 2026 meeting.&lt;/strong&gt; &lt;code&gt;endDate&lt;/code&gt; was 16 September at 00:00 UTC, which is 8 PM ET on 15 September, the evening of the first day of a two-day meeting. The market closed 20 hours after its &lt;code&gt;endDate&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The market on the next Russian parliamentary election.&lt;/strong&gt; &lt;code&gt;endDate&lt;/code&gt; was 30 September 2026 at 00:00 UTC. The rules fall back to &quot;Other&quot; only if results are not known by 30 September 2027. The market closed on 21 September.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On grouped date markets, check &lt;code&gt;groupItemTitle&lt;/code&gt; before you trust &lt;code&gt;endDate&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;why-enddate-changes-and-why-the-gamma-api-returns-old-values&quot;&gt;Why endDate changes, and why the Gamma API returns old values&lt;/h3&gt;
&lt;p&gt;Two markets returned different &lt;code&gt;endDate&lt;/code&gt; values depending on which snapshot the API served.&lt;/p&gt;
&lt;p&gt;For the aliens market, a response with &lt;code&gt;updatedAt&lt;/code&gt; of 21 September showed &lt;code&gt;endDate&lt;/code&gt; as 1 October 2026 at 03:59 UTC, which matches the rules. A fresh response on 24 September showed 1 January 2027. For the IPO market, a response with &lt;code&gt;updatedAt&lt;/code&gt; of 8 August showed 31 December 2027 at 00:00 UTC, 29 hours before the written deadline, and the fresh response showed 1 January 2028 at 04:59 UTC, which matches the rules. Between those snapshots, the IPO market was corrected and the aliens market moved 92 days away from its own rules.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;updatedAt&lt;/code&gt; will not tell you when that happened. It changes often and in bulk: at one point on 24 September the aliens, IPO, Celtics and Bitcoin markets all carried the same &lt;code&gt;updatedAt&lt;/code&gt; to the microsecond. Store your own fetch time instead.&lt;/p&gt;
&lt;p&gt;The older values reached us through cached responses. In three cases in the sample, a plain request to &lt;code&gt;/markets/{id}&lt;/code&gt; returned an older snapshot, and adding an unused query parameter returned the current one. If your code reads &lt;code&gt;endDate&lt;/code&gt; once when a market is listed, it may be holding a value the platform has since changed.&lt;/p&gt;
&lt;h3 id=&quot;enddate-vs-gamestarttime-vs-closedtime-vs-umaenddate&quot;&gt;endDate vs gameStartTime vs closedTime vs umaEndDate&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Field &lt;/th&gt;&lt;th&gt;What it held in the sample &lt;/th&gt;&lt;th&gt;Safe use&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;endDate&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Kickoff, start + 6 h, start + 7 days, or a deadline, depending on market type &lt;/td&gt;&lt;td&gt;A hint, never a deadline&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;gameStartTime&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Start of the sports event, updated when a match was rescheduled &lt;/td&gt;&lt;td&gt;Event start&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;closedTime&lt;/code&gt; &lt;/td&gt;&lt;td&gt;When the market actually closed &lt;/td&gt;&lt;td&gt;After-the-fact close time&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;umaEndDate&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Identical to &lt;code&gt;closedTime&lt;/code&gt; on all 16 closed markets &lt;/td&gt;&lt;td&gt;Same as &lt;code&gt;closedTime&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;description&lt;/code&gt; &lt;/td&gt;&lt;td&gt;The written rules, including any deadline in ET &lt;/td&gt;&lt;td&gt;The real deadline&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The formats differ. &lt;code&gt;gameStartTime&lt;/code&gt; and &lt;code&gt;closedTime&lt;/code&gt; come as &lt;code&gt;&quot;2026-09-23 08:00:00+00&quot;&lt;/code&gt;, while &lt;code&gt;endDate&lt;/code&gt; and &lt;code&gt;eventStartTime&lt;/code&gt; use &lt;code&gt;&quot;2026-09-23T08:00:00Z&quot;&lt;/code&gt;. &lt;code&gt;datetime.fromisoformat&lt;/code&gt; rejects both forms before Python 3.11, so normalize first:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from datetime import datetime

def parse_ts(s: str) -&amp;gt; datetime:
    &quot;&quot;&quot;Parse any Gamma API timestamp into an aware UTC datetime.&quot;&quot;&quot;
    s = s.replace(&quot; &quot;, &quot;T&quot;).replace(&quot;Z&quot;, &quot;+00:00&quot;)
    if s.endswith(&quot;+00&quot;):
        s += &quot;:00&quot;
    return datetime.fromisoformat(s)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;how-to-tell-if-a-polymarket-market-is-still-trading&quot;&gt;How to tell if a Polymarket market is still trading&lt;/h3&gt;
&lt;p&gt;Do not use &lt;code&gt;endDate&lt;/code&gt; for this, and do not use &lt;code&gt;active&lt;/code&gt; either. The closed CS2 market in the sample returned &lt;code&gt;&quot;active&quot;: true&lt;/code&gt; together with &lt;code&gt;&quot;closed&quot;: true&lt;/code&gt; and &lt;code&gt;&quot;acceptingOrders&quot;: false&lt;/code&gt;. Read &lt;code&gt;closed&lt;/code&gt; and &lt;code&gt;acceptingOrders&lt;/code&gt;, and for the resolution state read &lt;code&gt;umaResolutionStatus&lt;/code&gt;, which was &lt;code&gt;&quot;resolved&quot;&lt;/code&gt; on that market.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def is_tradable(m: dict) -&amp;gt; bool:
    return not m.get(&quot;closed&quot;) and bool(m.get(&quot;acceptingOrders&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not bulletproof. In January 2026 a public report on Polymarket's Rust SDK repository showed finished Africa Cup of Nations markets still returning &lt;code&gt;active=true&lt;/code&gt;, &lt;code&gt;closed=false&lt;/code&gt; and &lt;code&gt;acceptingOrders=true&lt;/code&gt;, with &lt;code&gt;endDate&lt;/code&gt; equal to kickoff (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Polymarket/rs-clob-client/issues/199&quot;&gt;issue #199&lt;/a&gt;). On our September sample the two flags were reliable, but treat them as the best signal available, not a guarantee.&lt;/p&gt;
&lt;p&gt;Closing and resolving are separate steps. After a market closes, the outcome is proposed to the UMA Optimistic Oracle, and according to Polymarket's documentation an undisputed proposal resolves after a 2-hour challenge period.&lt;/p&gt;
&lt;h3 id=&quot;how-to-find-the-real-resolution-deadline-of-a-polymarket-market&quot;&gt;How to find the real resolution deadline of a Polymarket market&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Read &lt;code&gt;description&lt;/code&gt;.&lt;/strong&gt; That is the contract. Sports matches say &quot;on or before … ET&quot; or &quot;by … ET (14 days after the scheduled start)&quot;. US match markets say they &quot;remain open until the game has been completed&quot;. Futures say &quot;postponed after …&quot;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check the title and &lt;code&gt;groupItemTitle&lt;/code&gt;&lt;/strong&gt; on price and date markets. &quot;On September 17&quot; or a &lt;code&gt;groupItemTitle&lt;/code&gt; of &lt;code&gt;&quot;September 30&quot;&lt;/code&gt; is the deadline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Take the start from &lt;code&gt;gameStartTime&lt;/code&gt;&lt;/strong&gt;, not from &lt;code&gt;endDate&lt;/code&gt; minus a guess.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code&gt;closedTime&lt;/code&gt; or &lt;code&gt;umaEndDate&lt;/code&gt;&lt;/strong&gt; to know when a market actually closed. Both are empty until it does.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Re-fetch before you act.&lt;/strong&gt; Add a cache-busting parameter and store your fetch time next to every value you keep.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;python-parse-the-resolution-deadline-from-a-market-description&quot;&gt;Python: parse the resolution deadline from a market description&lt;/h4&gt;
&lt;p&gt;This parser found the deadline on every market in the sample whose rules state a full date and a clock time in Eastern Time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re
from datetime import datetime
from zoneinfo import ZoneInfo

MONTHS = &quot;January|February|March|April|May|June|July|August|September|October|November|December&quot;
DEADLINE = re.compile(
    rf&quot;\b(?:on or before|by|after) ((?:{MONTHS}) \d{{1,2}}, \d{{4}}),? (?:at )?(\d{{1,2}}:\d{{2}} ?[AP]M) E[DS]?T\b&quot;
)

def rule_deadlines_utc(description: str) -&amp;gt; list[datetime]:
    &quot;&quot;&quot;Return every ET deadline stated in a Polymarket market description, in UTC.&quot;&quot;&quot;
    found = []
    for day, clock in DEADLINE.findall(description):
        local = datetime.strptime(f&quot;{day} {clock.replace(' ', '')}&quot;, &quot;%B %d, %Y %I:%M%p&quot;)
        local = local.replace(tzinfo=ZoneInfo(&quot;America/New_York&quot;))
        found.append(local.astimezone(ZoneInfo(&quot;UTC&quot;)))
    return found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;America/New_York&lt;/code&gt; rather than a fixed offset: ET is UTC-4 until 1 November 2026 and UTC-5 after, so a hard-coded offset will be an hour wrong for several months of every year. The parser returns nothing on US match markets, because their rules contain no deadline, and nothing on price markets, where the date is in the title. It does not handle &quot;noon&quot;, &quot;p.m.&quot; or abbreviated months, and it does not tell a start deadline from a result deadline, so read the sentence around each match.&lt;/p&gt;
&lt;h3 id=&quot;what-this-sample-does-not-tell-you&quot;&gt;What this sample does not tell you&lt;/h3&gt;
&lt;p&gt;Twenty-three markets show that &lt;code&gt;endDate&lt;/code&gt; means different things on different market types. They do not show how often each case occurs across Polymarket, or that every esports market uses exactly six hours. Valorant is not in the sample, and there is a single MLB game.&lt;/p&gt;
&lt;p&gt;We did not establish why the aliens market moved, or whether that change was deliberate. All markets here come from the international Polymarket Gamma API. Polymarket US is a separate exchange with its own contract rules, and nothing in this article applies to it.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Data: 23 markets fetched one by one from &lt;code&gt;gamma-api.polymarket.com/markets/{id}&lt;/code&gt; on 24 September 2026. The full sample is downloadable as &lt;a rel=&quot;nofollow&quot; href=&quot;https://orcalayer.com/data/polymarket-enddate-sample-2026-09-24.json&quot;&gt;JSON&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://orcalayer.com/blog/polymarket-api-enddate&quot;&gt;orcalayer.com&lt;/a&gt;. OrcaLayer is an independent Polymarket analytics provider, not affiliated with Polymarket.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28532/polymarket-gamma-api-enddate-is-not-the-resolution-deadline</guid>
<pubDate>Thu, 24 Sep 2026 17:36:25 +0000</pubDate>
</item>
<item>
<title>Building NLP Systems for 96 Low-Resource Languages: A Web Scraping War Story</title>
<link>https://coderlegion.com/22837/building-nlp-systems-for-96-low-resource-languages-a-web-scraping-war-story</link>
<description>&lt;h4 id=&quot;or-why-language-vitality-online-has-nothing-to-do-with-speaker-counts-and-everything-to-do-with-data-engineering&quot;&gt;Or why language vitality online has nothing to do with speaker counts — and everything to do with data engineering&lt;/h4&gt;
&lt;p&gt;Russia speaks dozens of minority languages — Tatar, Bashkir, Udmurt, and even tiny ones like South Yukaghir (fewer than 50 speakers). Some have no Wikipedia, no standard corpus, and almost no digital footprint.&lt;/p&gt;
&lt;p&gt;But they &lt;em&gt;do&lt;/em&gt; exist online — scattered across forums, blogs, and VK communities.&lt;/p&gt;
&lt;p&gt;We wanted to measure their online presence. As NLP engineers, we quickly realized: &lt;strong&gt;no global corpus, no labelled dataset, no pre-trained embeddings, and no simple way to ask a search engine &quot;give me all pages in Chechen&quot;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So we built our own pipeline. Here's what every developer building NLP systems for low-resource languages needs to know.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-core-problem-low-resource-nlp-without-a-dictionary&quot;&gt;The Core Problem: Low-Resource NLP Without a Dictionary&lt;/h3&gt;
&lt;p&gt;In mainstream NLP, you start with a corpus. Wikipedia. Common Crawl. Something.&lt;/p&gt;
&lt;p&gt;For 96 minority languages, &lt;strong&gt;the corpus doesn't exist&lt;/strong&gt;. You have to find the data first — before you can even think about training embeddings, fine-tuning BERT, or building a translation system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is the ultimate cold-start problem.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every stage of a standard NLP pipeline breaks:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;NLP Stage &lt;/th&gt;&lt;th&gt;The Problem&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Data Acquisition&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;No central repository. Data is scattered across VK, forums, blogs.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Language Identification&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;No pre-trained classifier for 90% of these languages.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Tokenization&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;No standard orthography. People type without diacritics.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Embeddings&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;No pre-trained word vectors. You must build from scratch.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Evaluation&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;No gold-standard test sets. You can't measure what you can't label.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-1-data-acquisition-the-search-api-as-your-crawler&quot;&gt;Step 1: Data Acquisition — The Search API as Your Crawler&lt;/h3&gt;
&lt;p&gt;You can't download the whole internet (unless you're Google). But search engines already crawled it.&lt;/p&gt;
&lt;p&gt;We used &lt;strong&gt;Yandex.XML&lt;/strong&gt; — a limited API with &lt;strong&gt;10,000 requests per day&lt;/strong&gt;. Each request returns one page of results, and each &lt;em&gt;next&lt;/em&gt; page costs another request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For an engineer, this means:&lt;/strong&gt; you have to be &lt;em&gt;extremely&lt;/em&gt; efficient with your queries.&lt;/p&gt;
&lt;h4 id=&quot;the-challenge-what-do-you-search-for-&quot;&gt;The challenge: What do you search for?&lt;/h4&gt;
&lt;p&gt;We needed &lt;strong&gt;word markers&lt;/strong&gt; — words that are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Frequent&lt;/strong&gt; in the target language&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unique&lt;/strong&gt; — not appearing in Russian or other languages&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free of special characters&lt;/strong&gt; — because real users don't type them&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That last point is &lt;strong&gt;critical for any NLP system dealing with real-world user-generated content.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Most minority languages use Cyrillic extensions: &lt;code&gt;ҙ&lt;/code&gt;, &lt;code&gt;ә&lt;/code&gt;, &lt;code&gt;ö&lt;/code&gt;, &lt;code&gt;ҷ&lt;/code&gt;. There's no standard keyboard for them. People write &lt;code&gt;&quot;менан&quot;&lt;/code&gt; instead of &lt;code&gt;&quot;менән&quot;&lt;/code&gt;. If we query with the &quot;correct&quot; spelling, we lose 90% of real texts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;We call this &quot;household spelling&quot;&lt;/strong&gt; — like medieval birch-bark manuscripts, but with modern laziness.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For your NLP pipeline:&lt;/strong&gt; If you're building systems for languages without standard keyboards, you need &lt;strong&gt;fuzzy matching&lt;/strong&gt; at the query stage, not just at the evaluation stage. This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Levenshtein-based query expansion&lt;/li&gt;
&lt;li&gt;Phonetic normalization&lt;/li&gt;
&lt;li&gt;Handling diacritic stripping before search&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-2-noise-filtering-the-precision-problem&quot;&gt;Step 2: Noise Filtering — The Precision Problem&lt;/h3&gt;
&lt;p&gt;Even good markers return junk. We had to filter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Wikipedia duplicates&lt;/strong&gt; — we can download those in one click, so exclude them&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linguistic papers&lt;/strong&gt; — Russian text containing one line of Rutul as an example&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Music sites&lt;/strong&gt; — song titles but no lyrics&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Typos&lt;/strong&gt; — e.g., Russian &lt;code&gt;&quot;пазух&quot;&lt;/code&gt; (bosom) mistyped as &lt;code&gt;&quot;пазох&quot;&lt;/code&gt;, which is a valid Khakas word meaning &quot;again&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-solution-multi-marker-verification&quot;&gt;The solution: Multi-marker verification&lt;/h4&gt;
&lt;p&gt;If one marker appears on a domain but others from the same language don't — it's probably a false positive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For your ML pipeline:&lt;/strong&gt; This is &lt;strong&gt;ensemble validation&lt;/strong&gt; — using multiple weak signals to confirm a strong one. The trade-off is extra API calls, which are expensive at 10k/day.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Alternative approach:&lt;/strong&gt; Train a binary classifier (Russian vs. everything else) on the fly. We did this later — more on that below.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-3-domain-classification-building-a-tiered-index&quot;&gt;Step 3: Domain Classification — Building a Tiered Index&lt;/h3&gt;
&lt;p&gt;We asked Yandex to categorize each domain into three tiers:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tier &lt;/th&gt;&lt;th&gt;Description &lt;/th&gt;&lt;th&gt;Example&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;1. Language-first&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Most pages in the minority language &lt;/td&gt;&lt;td&gt;Small blogs, community sites&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;2. Mixed&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Several pages, but not dominant &lt;/td&gt;&lt;td&gt;Regional news portals&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;3. Giant platforms&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Minority content is buried &lt;/td&gt;&lt;td&gt;YouTube, stihi.ru&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Additionally, we specifically queried &lt;strong&gt;VK.com&lt;/strong&gt; communities because post-2012, most standalone language sites died — and everything moved to social networks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For your data engineering pipeline:&lt;/strong&gt; You need a &lt;strong&gt;stratified crawling strategy&lt;/strong&gt;. Don't treat all domains equally. Allocate more budget to Tier 1, and use depth-limited sampling for Tier 3.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-4-language-detection-without-a-training-corpus&quot;&gt;Step 4: Language Detection Without a Training Corpus&lt;/h3&gt;
&lt;p&gt;We downloaded pages and needed to separate Russian from, say, Tuvan.&lt;/p&gt;
&lt;p&gt;Classic n-gram language detection requires a pre-labelled corpus — which we didn't have for most languages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The solution:&lt;/strong&gt; We &lt;em&gt;did&lt;/em&gt; have Russian — plenty of it. So we built a &lt;strong&gt;binary classifier&lt;/strong&gt;: &lt;em&gt;Russian vs. everything else&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;If a paragraph isn't Russian, we assume it's our target language.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For your ML system:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This is a &lt;strong&gt;one-class classification&lt;/strong&gt; problem with a twist.&lt;/li&gt;
&lt;li&gt;It's not perfect — but it's good enough to build the &lt;strong&gt;first ever web corpora&lt;/strong&gt; for these languages.&lt;/li&gt;
&lt;li&gt;This is a classic &lt;strong&gt;weak supervision&lt;/strong&gt; approach: use what you have (Russian) to learn what you don't (everything else).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-we-learned-online-vs-offline-zero-correlation&quot;&gt;What We Learned: Online vs. Offline — Zero Correlation&lt;/h3&gt;
&lt;p&gt;We compared online metrics (Wikipedia articles, VK communities, text volume) with offline metrics (speaker count, regional economy).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Metric Pair &lt;/th&gt;&lt;th&gt;Correlation&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Among online metrics &lt;/td&gt;&lt;td&gt;&lt;strong&gt;&amp;gt;0.7&lt;/strong&gt; (Spearman)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Online vs. offline &lt;/td&gt;&lt;td&gt;&lt;strong&gt;≈ 0&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;This is the key insight for anyone building IT systems for low-resource languages:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;How many people speak a language &lt;strong&gt;does not predict&lt;/strong&gt; how alive it is on the internet.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;What matters is a &lt;strong&gt;small, active, passionate community&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Bashkir has fewer speakers than Chechen, but far more online communities. Why?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Chechnya's internet infrastructure recovered later (1990s-2000s conflicts delayed digital adoption).&lt;/li&gt;
&lt;li&gt;Online activity is a function of &lt;strong&gt;infrastructure&lt;/strong&gt;, not just population.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;For your product strategy:&lt;/strong&gt; If you're building a language model for a low-resource language, prioritize communities, not census data. Find the VK groups, Discord servers, and Telegram channels. That's where the data lives.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-few-geeky-findings&quot;&gt;A Few Geeky Findings&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Finding &lt;/th&gt;&lt;th&gt;Implication&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Post-2012 shift&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Almost all minority language activity moved to VK.com. Standalone forums died.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Typical user&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;19-31 years old, lives in the region's capital (Ufa for Bashkir, Izhevsk for Udmurt).&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Typical post length&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;~5 words&lt;/strong&gt; per post in minority language. Most conversation is in Russian; people sprinkle native words as identity markers.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Interconnectivity&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Minority language sites link to each other across language families. A rare case of &quot;peoples united&quot; online.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;For your NLP models:&lt;/strong&gt; If you're building embeddings from this data, expect &lt;strong&gt;sparse, short texts&lt;/strong&gt;. Standard word2vec on 5-word posts won't work well. Consider:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Character-level embeddings&lt;/strong&gt; (more robust to spelling variation)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-lingual transfer&lt;/strong&gt; (use Russian or English as a bridge)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero-shot approaches&lt;/strong&gt; (label propagation from related languages)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-data-engineering-pipeline-summary&quot;&gt;The Data Engineering Pipeline — Summary&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────┐
│  Yandex.XML API │  ← 10k requests/day budget
│  (search engine)│
└────────┬────────┘
         ▼
┌─────────────────┐
│  Word Markers   │  ← Frequent + Unique + No diacritics
│  (handcrafted)  │  ← &quot;Household spelling&quot; handling
└────────┬────────┘
         ▼
┌─────────────────┐
│  Domain Filter  │  ← Remove Wikipedia, linguistic papers
│  (multi-marker) │  ← Ensemble validation
└────────┬────────┘
         ▼
┌─────────────────┐
│  Tiered Index   │  ← Language-first, Mixed, Giant platforms
│  (stratified)   │  ← VK.com special treatment
└────────┬────────┘
         ▼
┌─────────────────┐
│  Binary Class.  │  ← Russian vs. everything else
│  (weak super.)  │  ← No pre-labelled corpus needed
└────────┬────────┘
         ▼
┌─────────────────┐
│  Web Corpora    │  ← First ever for these languages
│  (downloadable) │  ← Available at web-corpora.net
└─────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-data&quot;&gt;The Data&lt;/h3&gt;
&lt;p&gt;Downloadable corpora and domain lists:&lt;br&gt;
&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;http://web-corpora.net/wsgi3/minorlangs/download&quot;&gt;web-corpora.net/wsgi3/minorlangs/download&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;And yes, there's a VK community with cute memes in minority languages — featuring cats.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-bottom-line-for-engineers&quot;&gt;The Bottom Line for Engineers&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;Web scraping isn't just about avoiding &lt;code&gt;robots.txt&lt;/code&gt;. It's about understanding human behaviour — typos, keyboard limitations, social network migration, and why a language with 50 speakers can thrive online if those 50 are determined enough.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key takeaways for building NLP systems for low-resource languages:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Start with search APIs, not crawlers.&lt;/strong&gt; The internet is already indexed. Use it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handle real-world spelling.&lt;/strong&gt; People don't use diacritics. Your queries shouldn't either.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use weak supervision.&lt;/strong&gt; You don't need a labelled corpus to start — use what you have.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prioritize communities, not speakers.&lt;/strong&gt; Online vitality ≠ offline speaker count.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expect short texts.&lt;/strong&gt; ~5-word posts are the norm. Plan your embeddings accordingly.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Further reading:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Our downloadable corpora: &lt;a rel=&quot;nofollow&quot; href=&quot;http://web-corpora.net/wsgi3/minorlangs/download&quot;&gt;web-corpora.net&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Yandex.XML API documentation&lt;/li&gt;
&lt;li&gt;VK.com API for social media data extraction&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Liked this? Share with your NLP engineering team. They'll never look at &quot;low-resource&quot; the same way again.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22837/building-nlp-systems-for-96-low-resource-languages-a-web-scraping-war-story</guid>
<pubDate>Thu, 06 Aug 2026 16:21:00 +0000</pubDate>
</item>
<item>
<title>Foundation Models Are Not the Product: Why Data Engineers Are Becoming the Bridge to Generative AI</title>
<link>https://coderlegion.com/23952/foundation-models-are-not-the-product-why-data-engineers-are-becoming-bridge-generative</link>
<description>&lt;h3 id=&quot;tl-dr&quot;&gt;TL;DR&lt;/h3&gt;
&lt;p&gt;Foundation Models are powerful, but they are not magic knowledge engines.&lt;/p&gt;
&lt;p&gt;They can generate text, summarize information, and answer questions because they understand patterns learned during training. However, they do not know your company's data, processes, or domain knowledge.&lt;/p&gt;
&lt;p&gt;That is where Data Engineering becomes critical.&lt;/p&gt;
&lt;p&gt;The next generation of AI applications will not only depend on better models. They will depend on better data pipelines, better architectures, and better ways to deliver context.&lt;/p&gt;
&lt;h3 id=&quot;the-question-i-keep-hearing&quot;&gt;The Question I Keep Hearing&lt;/h3&gt;
&lt;p&gt;Over the last months, while exploring Generative AI architectures, I noticed a recurring question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Why does this model know so much?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It is a natural question.&lt;/p&gt;
&lt;p&gt;You open ChatGPT, Claude, or another AI assistant, ask something complex, and receive an answer that feels surprisingly accurate.&lt;/p&gt;
&lt;p&gt;The first instinct is to think:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;The model knows.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But the reality is more interesting.&lt;/p&gt;
&lt;p&gt;The model does not know your business.&lt;/p&gt;
&lt;p&gt;It does not know your internal documentation, your operational procedures, your customer history, or your company's decisions from last quarter.&lt;/p&gt;
&lt;p&gt;What it knows are patterns.&lt;/p&gt;
&lt;p&gt;And that difference is exactly where Data Engineers have an important role to play.&lt;/p&gt;
&lt;h3 id=&quot;from-data-pipelines-to-ai-pipelines&quot;&gt;From Data Pipelines to AI Pipelines&lt;/h3&gt;
&lt;p&gt;For years, our job as Data Engineers has been focused on moving and transforming data so organizations can make better decisions.&lt;/p&gt;
&lt;p&gt;We built pipelines like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Operational Systems → ETL/ELT → Data Lake → Data Warehouse → Analytics
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we are seeing a new evolution:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Enterprise Data → Retrieval Pipeline → Foundation Model → AI Application
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The goal is different, but the engineering principles remain familiar.&lt;/p&gt;
&lt;p&gt;Data still needs to be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reliable.&lt;/li&gt;
&lt;li&gt;Governed.&lt;/li&gt;
&lt;li&gt;Secure.&lt;/li&gt;
&lt;li&gt;Fresh.&lt;/li&gt;
&lt;li&gt;Well-structured.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The main difference is the consumer.&lt;/p&gt;
&lt;p&gt;Before, the consumer was a dashboard or an analyst.&lt;/p&gt;
&lt;p&gt;Now, the consumer can be an AI system.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-with-smart-models&quot;&gt;The Problem With &quot;Smart&quot; Models&lt;/h3&gt;
&lt;p&gt;Imagine asking an AI assistant:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;What is our refund policy?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The model can generate an answer that looks perfect.&lt;/p&gt;
&lt;p&gt;It may use professional language.&lt;/p&gt;
&lt;p&gt;It may explain exceptions.&lt;/p&gt;
&lt;p&gt;It may even sound confident.&lt;/p&gt;
&lt;p&gt;But if the model has never seen your company's actual refund policy, that answer is only a prediction based on patterns.&lt;/p&gt;
&lt;p&gt;And predictions are not the same as facts.&lt;/p&gt;
&lt;p&gt;This is one of the biggest challenges when moving Generative AI from experiments into production.&lt;/p&gt;
&lt;p&gt;A fluent answer is not necessarily a correct answer.&lt;/p&gt;
&lt;p&gt;For enterprise applications, accuracy and trust matter more than impressive demos.&lt;/p&gt;
&lt;h3 id=&quot;why-rag-became-a-key-architecture-pattern&quot;&gt;Why RAG Became a Key Architecture Pattern&lt;/h3&gt;
&lt;p&gt;Retrieval-Augmented Generation (RAG) addresses this challenge by adding an important step before generation.&lt;/p&gt;
&lt;p&gt;Instead of asking the model to answer only from its training knowledge, we first retrieve relevant information from trusted enterprise sources and provide that context to the model.&lt;/p&gt;
&lt;p&gt;The workflow looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Collect enterprise data from different sources.&lt;/li&gt;
&lt;li&gt;Clean and transform the information.&lt;/li&gt;
&lt;li&gt;Generate embeddings and store them in a vector database.&lt;/li&gt;
&lt;li&gt;Retrieve relevant context based on the user's question.&lt;/li&gt;
&lt;li&gt;Send the context together with the prompt to the Foundation Model.&lt;/li&gt;
&lt;li&gt;Generate a response grounded in business data.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The Foundation Model provides reasoning and language capabilities.&lt;/p&gt;
&lt;p&gt;The data provides accuracy.&lt;/p&gt;
&lt;p&gt;The pipeline connects both.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-example&quot;&gt;A Practical Example&lt;/h3&gt;
&lt;p&gt;Imagine a company with thousands of technical documents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Product manuals.&lt;/li&gt;
&lt;li&gt;Internal procedures.&lt;/li&gt;
&lt;li&gt;Support documentation.&lt;/li&gt;
&lt;li&gt;Security guidelines.&lt;/li&gt;
&lt;li&gt;Engineering decisions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The information exists, but finding the right answer requires time.&lt;/p&gt;
&lt;p&gt;A traditional search system can locate documents, but employees still need to read and interpret them.&lt;/p&gt;
&lt;p&gt;A RAG-powered assistant changes the experience.&lt;/p&gt;
&lt;p&gt;Instead of searching through hundreds of pages, an engineer can ask:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;How do we handle this type of production incident?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The system retrieves relevant documentation and generates an answer based on internal knowledge.&lt;/p&gt;
&lt;p&gt;The AI is not replacing the documentation.&lt;/p&gt;
&lt;p&gt;It is making the documentation accessible.&lt;/p&gt;
&lt;h3 id=&quot;what-this-means-for-data-engineers&quot;&gt;What This Means for Data Engineers&lt;/h3&gt;
&lt;p&gt;Generative AI has created a lot of excitement around models.&lt;/p&gt;
&lt;p&gt;However, many production challenges are not model challenges.&lt;/p&gt;
&lt;p&gt;They are data challenges.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How do we keep information updated?&lt;/li&gt;
&lt;li&gt;How do we control access?&lt;/li&gt;
&lt;li&gt;How do we prevent sensitive data exposure?&lt;/li&gt;
&lt;li&gt;How do we evaluate response quality?&lt;/li&gt;
&lt;li&gt;How do we monitor AI pipelines?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are engineering questions.&lt;/p&gt;
&lt;p&gt;And they are questions Data Engineers already know how to solve.&lt;/p&gt;
&lt;p&gt;The future of AI will not only belong to the teams building the biggest models.&lt;/p&gt;
&lt;p&gt;It will belong to the teams that can connect models with reliable, trusted, and meaningful data.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;A Foundation Model brings intelligence capabilities.&lt;/p&gt;
&lt;p&gt;But intelligence without context has limited business value.&lt;/p&gt;
&lt;p&gt;The real opportunity for Data Engineers is building the bridge between enterprise data and AI applications.&lt;/p&gt;
&lt;p&gt;The same discipline that helped organizations become data-driven is now helping them become AI-enabled.&lt;/p&gt;
&lt;p&gt;The next generation of data pipelines will not only move information.&lt;/p&gt;
&lt;p&gt;They will deliver knowledge.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23952/foundation-models-are-not-the-product-why-data-engineers-are-becoming-bridge-generative</guid>
<pubDate>Sat, 01 Aug 2026 20:22:35 +0000</pubDate>
</item>
<item>
<title>Understanding Python Virtual Environments: The Interpreter Mismatch That Caused My Imports to Fail</title>
<link>https://coderlegion.com/23274/understanding-python-virtual-environments-interpreter-mismatch-that-caused-imports-fail</link>
<description>&lt;p&gt;While setting up my Data Engineering learning environment, I ran into an issue that many Python developers eventually face.&lt;/p&gt;
&lt;p&gt;Every package installation completed successfully, yet importing those same packages consistently failed.&lt;/p&gt;
&lt;p&gt;Initially, I assumed the virtual environment had become corrupted. I recreated it several times before realizing that the environment itself wasn’t the issue.&lt;/p&gt;
&lt;p&gt;The real culprit was an interpreter mismatch.&lt;/p&gt;
&lt;p&gt;The Python interpreter executing my project was different from the interpreter used by pip during package installation.&lt;/p&gt;
&lt;p&gt;As a result, packages were installed successfully — but into an entirely different Python installation.&lt;/p&gt;
&lt;p&gt;Why this happens&lt;br&gt;
Python installations can coexist on the same machine.&lt;/p&gt;
&lt;p&gt;Become a Medium member&lt;br&gt;
Examples include:&lt;/p&gt;
&lt;p&gt;System Python&lt;br&gt;
Microsoft Store Python&lt;br&gt;
Virtual environments&lt;br&gt;
Conda environments&lt;br&gt;
WSL installations&lt;br&gt;
If VS Code selects one interpreter while pip installs into another, import errors are inevitable.&lt;/p&gt;
&lt;p&gt;How I diagnose it now&lt;br&gt;
Verify where python&lt;br&gt;
Verify where pip&lt;br&gt;
Confirm the active VS Code interpreter.&lt;br&gt;
Install packages with:&lt;br&gt;
python -m pip install &lt;br&gt;
Verify installation:&lt;br&gt;
python -m pip show &lt;br&gt;
Standard workflow&lt;br&gt;
python -m venv .venv&lt;br&gt;
.venv\Scripts\activate&lt;br&gt;
python -m pip install &lt;br&gt;
Why this matters for Data Engineering&lt;br&gt;
As I continue learning technologies such as PySpark, Airflow, Kafka, and modern data tooling, maintaining a reproducible Python environment becomes increasingly important.&lt;/p&gt;
&lt;p&gt;Many “package not found” issues aren’t caused by the package itself — they’re caused by environment misconfiguration.&lt;/p&gt;
&lt;p&gt;Debugging the environment before debugging the code has become one of the most valuable habits I’ve picked up so far.&lt;/p&gt;
&lt;p&gt;Hopefully this saves another learner a few hours of frustration.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23274/understanding-python-virtual-environments-interpreter-mismatch-that-caused-imports-fail</guid>
<pubDate>Wed, 22 Jul 2026 17:49:21 +0000</pubDate>
</item>
<item>
<title>Your Dataset Is Your Foundation. Have You Checked If It's Crumbling?</title>
<link>https://coderlegion.com/22826/your-dataset-is-your-foundation-have-you-checked-if-its-crumbling</link>
<description>&lt;h4 id=&quot;or-why-wikipedia-is-the-perfect-data-quality-testing-ground-not-just-for-linguists-&quot;&gt;Or why Wikipedia is the perfect Data Quality testing ground (not just for linguists)&lt;/h4&gt;
&lt;p&gt;Every developer who has ever scraped Wikipedia for an NLP task thinks: &lt;em&gt;&quot;It's human-written text. That means the dataset is balanced, natural, and ready to feed into my model.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spoiler: it's not.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We analyzed 6 language editions of Wikipedia (Russian, Tatar, Bashkir, Mari, Mordvin, Komi) using the &lt;strong&gt;crudest but most effective tool in a data engineer's arsenal — frequency analysis of top tokens&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The results will change how you think about building training sets and text corpora for any IT system.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-litmus-test-healthy-dataset-vs-train-wreck&quot;&gt;The litmus test: healthy dataset vs. train wreck&lt;/h4&gt;
&lt;p&gt;In any natural, human-written corpus, &lt;strong&gt;the most frequent tokens are function words&lt;/strong&gt; — prepositions, conjunctions, pronouns, auxiliary verbs. This is a universal law across languages.&lt;/p&gt;
&lt;p&gt;Example from the Russian National Corpus:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;# &lt;/th&gt;&lt;th&gt;Word &lt;/th&gt;&lt;th&gt;POS&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1 &lt;/td&gt;&lt;td&gt;и (and) &lt;/td&gt;&lt;td&gt;conj&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;2 &lt;/td&gt;&lt;td&gt;в (in) &lt;/td&gt;&lt;td&gt;prep&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;3 &lt;/td&gt;&lt;td&gt;не (not) &lt;/td&gt;&lt;td&gt;part&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If your top-20 contains &lt;em&gt;&quot;river&quot;&lt;/em&gt;, &lt;em&gt;&quot;basin&quot;&lt;/em&gt;, &lt;em&gt;&quot;water&quot;&lt;/em&gt;, &lt;em&gt;&quot;orchid&quot;&lt;/em&gt; — &lt;strong&gt;your data pipeline is broken&lt;/strong&gt;, even if your scraping code is flawless.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;russian-wikipedia-the-gold-standard-mostly-&quot;&gt;Russian Wikipedia — the gold standard (mostly)&lt;/h4&gt;
&lt;p&gt;1.06M articles. Top tokens:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;# &lt;/th&gt;&lt;th&gt;Word &lt;/th&gt;&lt;th&gt;Frequency&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1 &lt;/td&gt;&lt;td&gt;в (in) &lt;/td&gt;&lt;td&gt;10,859,129&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;2 &lt;/td&gt;&lt;td&gt;и (and) &lt;/td&gt;&lt;td&gt;5,761,105&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;3 &lt;/td&gt;&lt;td&gt;на (on) &lt;/td&gt;&lt;td&gt;3,214,393&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;✅ Function words dominate. The dataset is usable for language modeling.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;but-tatar-and-bashkir-wikipedia-data-quality-nightmare&quot;&gt;But Tatar and Bashkir Wikipedia? Data Quality Nightmare&lt;/h4&gt;
&lt;p&gt;~50K articles each. Top-5 for Tatar:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;# &lt;/th&gt;&lt;th&gt;Word &lt;/th&gt;&lt;th&gt;Translation &lt;/th&gt;&lt;th&gt;Frequency&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1 &lt;/td&gt;&lt;td&gt;&lt;strong&gt;елга&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;river&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;132,567&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;2 &lt;/td&gt;&lt;td&gt;бассейны &lt;/td&gt;&lt;td&gt;basin &lt;/td&gt;&lt;td&gt;75,706&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;3 &lt;/td&gt;&lt;td&gt;су &lt;/td&gt;&lt;td&gt;water &lt;/td&gt;&lt;td&gt;54,689&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;4 &lt;/td&gt;&lt;td&gt;буенча &lt;/td&gt;&lt;td&gt;by (postposition) &lt;/td&gt;&lt;td&gt;48,838&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;5 &lt;/td&gt;&lt;td&gt;Русия &lt;/td&gt;&lt;td&gt;Russia &lt;/td&gt;&lt;td&gt;48,722&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;&quot;River&quot; appears 3x more often than the conjunction &quot;and&quot;.&lt;/strong&gt; Bashkir is identical: &lt;em&gt;йылга&lt;/em&gt; (river), &lt;em&gt;бассейны&lt;/em&gt;, &lt;em&gt;һыу&lt;/em&gt; (water), &lt;em&gt;Рәсәй&lt;/em&gt; (Russia).&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;why-auto-generated-content-strikes-again&quot;&gt;Why? Auto-generated content strikes again&lt;/h4&gt;
&lt;blockquote&gt;&lt;p&gt;Most articles in these editions weren't written by humans. They were auto-generated from a template using Russia's State Water Registry.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Thousands of river entries — each repeating &lt;em&gt;&quot;river&quot;&lt;/em&gt;, &lt;em&gt;&quot;basin&quot;&lt;/em&gt;, &lt;em&gt;&quot;water&quot;&lt;/em&gt;, &lt;em&gt;&quot;km&quot;&lt;/em&gt;, &lt;em&gt;&quot;mouth&quot;&lt;/em&gt;. This is a &lt;strong&gt;textbook case of systematic bias&lt;/strong&gt; that kills any ML pipeline.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For a developer, this means:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your word embeddings will be obsessed with hydrology.&lt;/li&gt;
&lt;li&gt;Any text classifier will overfit on geographic patterns.&lt;/li&gt;
&lt;li&gt;You'll spend months debugging why your model keeps generating &lt;em&gt;&quot;river&quot;&lt;/em&gt; in every context.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;mordvin-wikipedias-the-orchid-syndrome&quot;&gt;Mordvin Wikipedias: The Orchid Syndrome&lt;/h4&gt;
&lt;p&gt;Erzya and Moksha editions (~1,100–1,500 articles each). Top tokens include &lt;em&gt;&quot;category&quot;&lt;/em&gt;, &lt;em&gt;&quot;see&quot;&lt;/em&gt;, &lt;em&gt;&quot;day&quot;&lt;/em&gt;, &lt;em&gt;&quot;year&quot;&lt;/em&gt; — but also:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Erzya &lt;/th&gt;&lt;th&gt;Moksha&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;истэжо (also) &lt;/td&gt;&lt;td&gt;ди (and)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;категория &lt;/td&gt;&lt;td&gt;и (and)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;ван (see) &lt;/td&gt;&lt;td&gt;тыналста (from family)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;чи (day) &lt;/td&gt;&lt;td&gt;касыкссь (plant)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;ды (and) &lt;/td&gt;&lt;td&gt;панчф (flower)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;ие (year) &lt;/td&gt;&lt;td&gt;&lt;strong&gt;орхидея (orchid)&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;The explanation:&lt;/strong&gt; hundreds of articles about orchids (family Orchidaceae) were bot-generated. Each repeats &lt;em&gt;&quot;orchid&quot;&lt;/em&gt;, &lt;em&gt;&quot;flower&quot;&lt;/em&gt;, &lt;em&gt;&quot;species&quot;&lt;/em&gt;, &lt;em&gt;&quot;genus&quot;&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;They call it the &lt;strong&gt;Orchid Phenomenon&lt;/strong&gt; — a dataset hijacked by bots writing about a single narrow topic.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;quick-sanity-check-average-article-length&quot;&gt;Quick sanity check: average article length&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Edition &lt;/th&gt;&lt;th&gt;Avg length (words)&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Russian &lt;/td&gt;&lt;td&gt;184.5&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Tatar &lt;/td&gt;&lt;td&gt;58.5&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Bashkir &lt;/td&gt;&lt;td&gt;56.8&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Komi-Zyrian &lt;/td&gt;&lt;td&gt;29.6&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Erzya &lt;/td&gt;&lt;td&gt;34.5&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Moksha &lt;/td&gt;&lt;td&gt;34.9&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Short articles = high risk of template-driven, repetitive content.&lt;/strong&gt; This is a red flag for any Data Engineer: you need additional filtering.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-this-means-for-you-as-a-developer-building-it-systems&quot;&gt;What this means for you as a developer building IT systems&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Never trust a dataset blindly&lt;/strong&gt; — especially for low-resource languages or small editions. Dataset size ≠ dataset quality.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Frequency analysis is your cheap, powerful friend.&lt;/strong&gt; Generate a top-N word list in 5 minutes with any language (Python, &lt;code&gt;collections.Counter&lt;/code&gt;, a few lines of code). Make it a mandatory step in your ETL pipeline.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Auto-generated content is a systemic risk.&lt;/strong&gt; It's everywhere: not just rivers and orchids, but also geocoordinates, biological taxa, movie stubs, sports tournaments. If you don't clean it, your model will think the world consists of rivers and flowers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Data Drift starts here.&lt;/strong&gt; If your training distribution doesn't match production traffic, your system makes decisions based on a distorted reality. That's not a bug in your code — it's a bug in your data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Version and profile your data like you version your code.&lt;/strong&gt; Implement Data Quality checks (Great Expectations, Pytest for dataframes) in your CI/CD pipeline. If the top-10 tokens suddenly change — halt the pipeline.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-bottom-line&quot;&gt;The bottom line&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;Wikipedia is free, but not always natural.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For major languages (English, Russian, German) the corpus is surprisingly balanced. For many minority languages, the Wikipedia you scrape might be a &lt;strong&gt;botanical reference&lt;/strong&gt;, not a linguistic corpus.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For an engineer, this means:&lt;/strong&gt; always profile your data. A frequency list takes 5 minutes to generate and can save you months of debugging weird model predictions and overfitting on noise patterns.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Further reading:&lt;/strong&gt; Orekhov, Reshetnikov (2014). &quot;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nevmenandr.github.io/portfolio/assets/pdf/wiki.pdf&quot;&gt;Towards an evaluation of Wikipedia as a linguistic source: a comparative study.&lt;/a&gt;&quot; In &lt;em&gt;Modern Russian Language on the Internet&lt;/em&gt;, Moscow.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Liked this? Share with your fellow engineers. They'll never look at a Wikipedia dump as &quot;clean data&quot; again.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22826/your-dataset-is-your-foundation-have-you-checked-if-its-crumbling</guid>
<pubDate>Thu, 16 Jul 2026 17:17:49 +0000</pubDate>
</item>
<item>
<title>Is AI Coming for Data Analyst Jobs? Depends What You Actually Do All Day</title>
<link>https://coderlegion.com/22759/is-ai-coming-for-data-analyst-jobs-depends-what-you-actually-do-all-day</link>
<description>&lt;p&gt;Short answer: no. A bit longer answer: no, unless your whole job is just writing SQL.&lt;/p&gt;
&lt;p&gt;Someone started a thread with a good question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;You said frontend is dying because it's 'pure pattern-matching' that AI can now do. As a Data Analyst, I see Claude clean data, write SQL and build dashboards in seconds. The exact same pattern. Are Junior Data Analysts heading for the same C-Tier obsolescence?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Honestly? Some of them, yeah. But not because they're analysts. Because a lot of them weren't really doing analysis to begin with.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;People got confused about what the job even is&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Somewhere along the way, &quot;data analyst&quot; turned into a fancy title for &quot;person who writes SQL.&quot; That was never right. SQL is just the tool you use. The actual job is figuring out what's true, what matters, and what's about to blow up in a meeting if nobody catches it in time.&lt;/p&gt;
&lt;p&gt;If your day looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;Pull last quarter's numbers&quot;&lt;/li&gt;
&lt;li&gt;&quot;Clean this messy CSV&quot;&lt;/li&gt;
&lt;li&gt;&quot;Make me a bar chart&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...that's not really analysis. That's data entry with extra steps. And yeah, that part is basically automated now. Claude writes that query in two seconds flat and never complains about it. No point pretending otherwise.&lt;/p&gt;
&lt;p&gt;But that was always the junior, boring 20% of the job. People just mistook it for the whole thing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; Here's what AI still can't do&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No matter how good these tools get at writing SQL, they still can't do this stuff:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spot when a number is lying to you.&lt;/strong&gt;&lt;br&gt;
Revenue's up 12%. Nice - except pricing changed halfway through the quarter, and one big client is basically all of that &quot;growth.&quot; The query ran fine. The conclusion is still wrong. You only catch that if you know the business well enough to get suspicious of a number that looks too good.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decide what &quot;active user&quot; even means.&lt;/strong&gt;&lt;br&gt;
Someone has to actually define this. Logged in the last 30 days? Fired any event at all? Paying customer only? Get it wrong once, quietly, and two teams will spend the next year arguing about whose number is &quot;correct,&quot; not realizing they're using different definitions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Push back on a bad question.&lt;/strong&gt;&lt;br&gt;
A manager asks &quot;why are we losing customers.&quot; That's not one query, it's like ten different ones, and picking the wrong one burns a week. A good analyst asks more questions before touching the keyboard. AI just answers whatever you literally typed - even if it's the wrong question, answered perfectly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Notice the bug that gives a confident, wrong answer.&lt;/strong&gt;&lt;br&gt;
A join quietly duplicates rows and inflates a number by 40%. Nothing errors out. It just looks slightly off to someone who's been burned by this exact thing before, and they go dig into it. That gut feeling comes from experience, not from training data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Actually own the decision.&lt;/strong&gt;&lt;br&gt;
Batch job or live streaming? Is this dashboard even worth the extra $2k a month in warehouse costs given who's actually looking at it? These are business calls wearing technical clothes. AI can list your options all day. It's not sitting in the budget meeting defending the choice.&lt;/p&gt;
&lt;p&gt;None of that is pattern-matching. It's judgment you build by getting things wrong before, at this specific company, with this specific messy data. That's the one thing a model trained on everyone's data  and belonging to no one - just can't have.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;So who's actually in trouble here?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The person whose whole pitch is &quot;I write SQL fast.&quot; If that's genuinely all you bring, you're competing with something faster, cheaper, and awake at 3am. Not a hot take, just math.&lt;/p&gt;
&lt;p&gt;The junior who uses AI to skip the boring stuff and spends that extra time in meetings, arguing about what a metric should mean, or getting suspicious of numbers that look too clean — that person just got a huge boost. Not a threat to them at all.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;The part worth actually talking about&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You used to build that judgment slowly. Write a bad join, watch a dashboard break, get grilled about it in standup, learn from it. Painful, but it worked. It was basically an apprenticeship disguised as boring ticket work.&lt;/p&gt;
&lt;p&gt;AI just ate the boring ticket work. Which means that apprenticeship juniors used to go through without even thinking about it is disappearing. Telling someone &quot;you'll pick up the instincts eventually&quot; isn't much help if the reps that used to build those instincts don't exist anymore.&lt;/p&gt;
&lt;p&gt;So the real question isn't &quot;will AI replace data analysts.&quot; It's: if the boring reps are gone, how does a junior actually build that &quot;something's off with this number&quot; instinct on purpose, instead of just hoping three years of grunt work hands it to them?&lt;/p&gt;
&lt;p&gt;That one's worth an actual answer. &quot;You'll figure it out eventually&quot; doesn't cut it anymore.&lt;/p&gt;
&lt;p&gt;Shoutout to &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/user/alexvoste&quot;&gt;@alexvoste&lt;/a&gt; for the original C-Tier/S-Tier take that kicked this whole conversation off. If you haven't seen his stuff - he's the guy building &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/forgezero-cli/forgezero&quot;&gt;ForgeZero&lt;/a&gt;, a build toolchain in Go that's reportedly beating Ninja by a wide margin on large task sets. Worth a follow if you care about performance work at the systems level.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22759/is-ai-coming-for-data-analyst-jobs-depends-what-you-actually-do-all-day</guid>
<pubDate>Wed, 15 Jul 2026 13:11:30 +0000</pubDate>
</item>
<item>
<title>The Art of Cloud Survival: The Day Monitoring Failed First</title>
<link>https://coderlegion.com/22753/the-art-of-cloud-survival-the-day-monitoring-failed-first</link>
<description>&lt;h3 id=&quot;when-everything-looked-fine&quot;&gt;When Everything Looked Fine&lt;/h3&gt;
&lt;p&gt;Modern cloud systems are built to survive failure.&lt;/p&gt;
&lt;p&gt;Applications run across multiple Availability Zones. Containers restart automatically. Serverless functions scale on demand. Databases replicate data continuously. Entire architectural patterns have emerged around the idea that failure is not an exception but an expected part of operating software at scale.&lt;/p&gt;
&lt;p&gt;Yet some of the most painful incidents begin long before a server crashes or a service becomes unavailable.&lt;/p&gt;
&lt;p&gt;They begin when nobody realizes something is already going wrong.&lt;/p&gt;
&lt;p&gt;Imagine a typical weekday morning. Dashboards are open on a large monitor. Every graph appears healthy. CPU utilization is well below critical thresholds. Memory consumption is stable. No alarms are firing. Error rates remain low.&lt;/p&gt;
&lt;p&gt;From an operational perspective, everything appears normal.&lt;/p&gt;
&lt;p&gt;Meanwhile, users are experiencing increasing delays. Transactions that usually complete in milliseconds now take several seconds. Some requests time out entirely. Support tickets begin arriving. Customer complaints appear in chat channels. Teams start investigating because users are reporting problems, not because monitoring systems detected them.&lt;/p&gt;
&lt;p&gt;The infrastructure is functioning. The monitoring strategy is not. And that distinction matters more than many teams realize.&lt;/p&gt;
&lt;h3 id=&quot;the-green-dashboard-problem&quot;&gt;The Green Dashboard Problem&lt;/h3&gt;
&lt;p&gt;One of the most dangerous moments in operations is staring at a dashboard filled with green indicators while customers are actively experiencing issues.&lt;/p&gt;
&lt;p&gt;Most engineering teams invest heavily in monitoring. They collect metrics, configure alarms, centralize logs, and build detailed dashboards. The intention is good. Visibility is essential in cloud environments.&lt;/p&gt;
&lt;p&gt;The challenge is that many dashboards focus on infrastructure health rather than service health.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A virtual machine can be healthy while an application is struggling.&lt;/li&gt;
&lt;li&gt;A database can be available while queries are becoming progressively slower.&lt;/li&gt;
&lt;li&gt;A queue can continue accepting messages while consumers fall further behind every minute.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In each case, the system is technically running. Yet the user experience is already deteriorating.&lt;/p&gt;
&lt;p&gt;Distributed systems make this problem even harder to detect because failures rarely appear as obvious outages. Instead, they emerge gradually, hidden inside latency, retries, timeouts, and backlogs.&lt;/p&gt;
&lt;p&gt;Even when the dashboard remains green,  the customer experience does not.&lt;/p&gt;
&lt;h3 id=&quot;why-distributed-systems-hide-failure&quot;&gt;Why Distributed Systems Hide Failure&lt;/h3&gt;
&lt;p&gt;In a monolithic application, a failure is often visible and immediate. If a critical component stops working, the impact is usually straightforward to identify.&lt;/p&gt;
&lt;p&gt;Distributed systems behave differently.&lt;/p&gt;
&lt;p&gt;A slowdown in one service can propagate through multiple dependencies. An overloaded database may increase response times. Those delays cause application servers to wait longer. Waiting requests consume resources. Retries begin accumulating. Message queues start growing. Additional traffic increases pressure on downstream services.&lt;/p&gt;
&lt;p&gt;Nothing has technically failed.&lt;/p&gt;
&lt;p&gt;Yet the system is moving steadily toward an incident.&lt;/p&gt;
&lt;p&gt;This is one reason distributed architectures are both powerful and challenging. Their resilience often comes from decoupling components, but that same decoupling can make problems difficult to observe.&lt;/p&gt;
&lt;p&gt;Symptoms appear far away from the original cause.&lt;/p&gt;
&lt;p&gt;By the time users notice, the underlying issue may have been developing for hours.&lt;/p&gt;
&lt;h3 id=&quot;monitoring-tells-you-what-you-expect&quot;&gt;Monitoring Tells You What You Expect&lt;/h3&gt;
&lt;p&gt;Many teams use the words &lt;strong&gt;monitoring&lt;/strong&gt; and &lt;strong&gt;observability&lt;/strong&gt; interchangeably.&lt;/p&gt;
&lt;p&gt;They are related, but they are not the same thing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Monitoring&lt;/strong&gt; helps answer questions we already know to ask. It tells us whether predefined conditions have been met.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Is CPU utilization above 80 percent?&lt;/li&gt;
&lt;li&gt;Is memory consumption increasing?&lt;/li&gt;
&lt;li&gt;Has an error threshold been exceeded?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are valuable questions. But real incidents rarely follow predefined scripts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Observability&lt;/strong&gt; focuses on understanding what is happening inside a system, especially when the problem is unexpected. Instead of simply collecting metrics, observability provides enough context to investigate unknown behaviors and discover why a system is acting differently than expected.&lt;/p&gt;
&lt;p&gt;This distinction becomes increasingly important as architectures grow more complex.&lt;/p&gt;
&lt;p&gt;A dashboard showing resource utilization may indicate that everything is healthy. Observability helps determine whether customers would agree.&lt;/p&gt;
&lt;h3 id=&quot;the-signals-that-actually-matter&quot;&gt;The Signals That Actually Matter&lt;/h3&gt;
&lt;p&gt;One of the most influential ideas in modern operations comes from Google's Site Reliability Engineering practices: the &lt;strong&gt;Four Golden Signals&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;These signals focus on the aspects of a system that most directly affect users.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=409537087628504462&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Together, these metrics provide a much more meaningful view of system health than infrastructure metrics alone.&lt;/p&gt;
&lt;p&gt;Consider a payment processing service.&lt;/p&gt;
&lt;p&gt;Suppose CPU utilization remains below 40 percent and memory usage appears stable. Traditional infrastructure monitoring would suggest the service is healthy.&lt;/p&gt;
&lt;p&gt;Now imagine database response times increase from 50 milliseconds to two seconds.&lt;/p&gt;
&lt;p&gt;Customers begin abandoning transactions. Requests start timing out. Revenue is affected.&lt;/p&gt;
&lt;p&gt;The infrastructure never appeared unhealthy. The customer experience did. And this is why effective monitoring begins with user-facing outcomes rather than resource consumption.&lt;/p&gt;
&lt;h3 id=&quot;the-danger-of-averages&quot;&gt;The Danger of Averages&lt;/h3&gt;
&lt;p&gt;Another common mistake is relying too heavily on averages. Average response times often create a comforting but incomplete picture. A service may report an average latency of 200 milliseconds while a small percentage of requests take several seconds to complete. Those slower requests may represent only a fraction of total traffic, yet they are often the first indication that something is wrong.&lt;/p&gt;
&lt;p&gt;For the affected users, the average is irrelevant.&lt;/p&gt;
&lt;p&gt;Their experience is defined by the request that took five seconds, not by the thousands that completed successfully.&lt;/p&gt;
&lt;p&gt;This is why mature operations teams pay close attention to &lt;strong&gt;percentile measurements&lt;/strong&gt; such as &lt;strong&gt;p95&lt;/strong&gt; and &lt;strong&gt;p99&lt;/strong&gt; latency.&lt;/p&gt;
&lt;p&gt;These metrics reveal what is happening at the edges of the system, where performance problems frequently emerge first.&lt;/p&gt;
&lt;p&gt;Many major incidents begin as small increases in tail latency long before average metrics show meaningful changes.&lt;/p&gt;
&lt;p&gt;If teams monitor only averages, they may discover problems far later than they should.&lt;/p&gt;
&lt;h3 id=&quot;failures-rarely-arrive-all-at-once&quot;&gt;Failures Rarely Arrive All at Once&lt;/h3&gt;
&lt;p&gt;Hollywood teaches us to expect disasters as sudden events. Real-world cloud incidents are usually much less dramatic.&lt;/p&gt;
&lt;p&gt;Most failures arrive gradually.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A dependency becomes slower.&lt;/li&gt;
&lt;li&gt;A third-party API starts returning intermittent errors.&lt;/li&gt;
&lt;li&gt;A queue begins accumulating messages.&lt;/li&gt;
&lt;li&gt;Consumers struggle to keep pace.&lt;/li&gt;
&lt;li&gt;Retries increase traffic.&lt;/li&gt;
&lt;li&gt;Latency spreads through the system.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What started as a minor degradation becomes a significant operational issue.&lt;/p&gt;
&lt;p&gt;This is one reason observability is so important. Detecting a problem early often means the difference between a brief investigation and a lengthy outage.&lt;/p&gt;
&lt;p&gt;The sooner a team can identify unusual behavior, the more options they have to contain the impact.&lt;/p&gt;
&lt;p&gt;Cloud survival is rarely about preventing every failure, it is about recognizing failures before they become incidents.&lt;/p&gt;
&lt;h3 id=&quot;building-visibility-on-aws&quot;&gt;Building Visibility on AWS&lt;/h3&gt;
&lt;p&gt;AWS provides a rich set of services that help teams understand what is happening inside distributed systems.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Amazon CloudWatch&lt;/strong&gt; serves as the foundation for metrics, logs, dashboards, and alarms. It enables teams to monitor operational signals across applications and infrastructure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AWS X-Ray&lt;/strong&gt; helps trace requests as they move through multiple services, making it easier to identify bottlenecks and understand where latency is introduced.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Amazon EventBridge&lt;/strong&gt; can automate operational workflows, trigger responses to specific events, and help reduce the time between detection and action.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Used together, these services create visibility across layers of a distributed architecture.&lt;/p&gt;
&lt;p&gt;But tools alone do not guarantee observability. Organizations often spend months building dashboards while investing very little effort in defining what success or failure actually looks like from a user's perspective. Technology is only part of the solution. The real challenge is deciding what deserves attention.&lt;/p&gt;
&lt;h3 id=&quot;the-question-every-team-should-ask&quot;&gt;The Question Every Team Should Ask&lt;/h3&gt;
&lt;p&gt;There is a simple question that every engineering team should periodically ask:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&quot;If a customer experiences a serious problem right now, would we know before they tell us?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The answer is often uncomfortable.&lt;/p&gt;
&lt;p&gt;Many teams discover that their alerts focus on infrastructure while customer-impacting issues remain invisible until support tickets arrive.&lt;/p&gt;
&lt;p&gt;That realization can be frustrating, but it is also valuable.&lt;/p&gt;
&lt;p&gt;Because once teams understand the gap between system health and customer experience, they can begin designing monitoring strategies that reflect reality instead of assumptions.&lt;/p&gt;
&lt;p&gt;The goal is not to collect more metrics, the goal is to collect the right ones.&lt;/p&gt;
&lt;h3 id=&quot;cloud-survival-requires-visibility&quot;&gt;Cloud Survival Requires Visibility&lt;/h3&gt;
&lt;p&gt;The most resilient cloud architectures are not always the ones with the most automation, the most services, or the most sophisticated deployment pipelines.&lt;/p&gt;
&lt;p&gt;Often, they are the ones that provide the clearest visibility into what users are actually experiencing.&lt;br&gt;
Failures are inevitable. Latency will increase. Dependencies will degrade. Services will behave in unexpected ways. Distributed systems guarantee that complexity will eventually surface. What separates resilient teams from struggling ones is not their ability to avoid failure.&lt;/p&gt;
&lt;p&gt;It is their ability to &lt;strong&gt;see it&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The day monitoring fails first is usually the day a team learns an important lesson: collecting metrics is not the same as understanding a system. Cloud survival requires both.&lt;/p&gt;
&lt;p&gt;Because you cannot respond to a problem you cannot see.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22753/the-art-of-cloud-survival-the-day-monitoring-failed-first</guid>
<pubDate>Wed, 15 Jul 2026 12:13:04 +0000</pubDate>
</item>
<item>
<title>The Art of Cloud Survival: When Retries Become the Outage</title>
<link>https://coderlegion.com/21369/the-art-of-cloud-survival-when-retries-become-the-outage</link>
<description>&lt;h2 id=&quot;when-retries-become-the-outage&quot;&gt;When Retries Become the Outage&lt;/h2&gt;
&lt;p&gt;At 2:17 AM, a payment service started returning errors. Not many. Just enough to attract attention. The dashboards didn't look alarming. CPU utilization was stable. Memory consumption was within normal limits. Infrastructure health checks were passing. If you had opened the monitoring console at that moment, you probably would have assumed the issue was temporary and would resolve itself within a few minutes.&lt;/p&gt;
&lt;p&gt;The engineering team began investigating. Meanwhile, the rest of the system was already reacting. API clients started resubmitting requests. Lambda functions retried failed executions. Messages that couldn't be processed returned to queues. Downstream services attempted the same operations again, hoping the next attempt would succeed.&lt;/p&gt;
&lt;p&gt;None of these actions were wrong. In fact, every component was behaving exactly as it had been designed to behave. And that was the problem.&lt;/p&gt;
&lt;p&gt;What began as a relatively small service degradation gradually evolved into a much larger incident. The original failure affected a limited number of requests. The recovery mechanisms multiplied that traffic until the struggling service was forced to handle far more load than it had seen before the outage began.&lt;/p&gt;
&lt;p&gt;The system wasn't collapsing because of the initial error anymore. It was collapsing because everybody was trying to help.&lt;/p&gt;
&lt;h3 id=&quot;why-retries-feel-like-the-right-answer&quot;&gt;Why Retries Feel Like the Right Answer&lt;/h3&gt;
&lt;p&gt;Retries are one of the first resilience patterns most engineers learn, and for good reason.&lt;/p&gt;
&lt;p&gt;Distributed systems operate in an imperfect world. Networks experience brief interruptions. Services become temporarily unavailable. Databases occasionally take longer than expected to respond. A request that fails now may succeed a few seconds later without any intervention.&lt;/p&gt;
&lt;p&gt;Because of this, retries often improve reliability. A customer never notices the timeout. The application recovers automatically. Everybody wins. The trouble starts when we focus on the success story and forget to think about scale. A single retry seems harmless. Ten thousand retries are something else entirely.&lt;/p&gt;
&lt;p&gt;Imagine a service receiving 10,000 requests during a busy period. Suddenly, a dependency starts responding slowly. Requests begin timing out. Each client retries three times before giving up.&lt;/p&gt;
&lt;p&gt;Without realizing it, the system has transformed 10,000 requests into 40,000. That additional traffic creates more pressure on the dependency. More pressure leads to higher latency. Higher latency generates more failures. More failures trigger more retries. What looked like a recovery mechanism has become a feedback loop. And feedback loops are where distributed systems become dangerous.&lt;/p&gt;
&lt;h3 id=&quot;the-real-problem-isn-t-failure&quot;&gt;The Real Problem Isn't Failure&lt;/h3&gt;
&lt;p&gt;One of the hardest lessons in cloud architecture is that systems rarely fail where the problem starts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A database slows down.&lt;/li&gt;
&lt;li&gt;An API begins timing out.&lt;/li&gt;
&lt;li&gt;A message consumer falls behind.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those events are usually manageable.&lt;/p&gt;
&lt;p&gt;The real challenge comes from the chain reaction that follows. Every service makes a reasonable decision based on its local perspective. Every team implements protections intended to improve reliability. Every retry policy makes sense when viewed in isolation.&lt;/p&gt;
&lt;p&gt;Yet when an incident occurs, those independent decisions begin interacting with each other. The result is often a level of complexity that nobody anticipated during design reviews. I've seen incidents where the root cause was resolved within minutes, but the platform continued struggling for hours because retries, reprocessing, and recovery workflows kept generating load long after the original issue had disappeared.&lt;/p&gt;
&lt;p&gt;The failure wasn't the hardest part. Recovering from the recovery was.&lt;/p&gt;
&lt;h3 id=&quot;when-aws-does-exactly-what-you-asked-it-to-do&quot;&gt;When AWS Does Exactly What You Asked It To Do&lt;/h3&gt;
&lt;p&gt;AWS services are designed to help us build resilient systems, which is why many of them include retry mechanisms by default.&lt;/p&gt;
&lt;p&gt;Take Lambda and SQS. When a Lambda function cannot successfully process a message, that message doesn't simply disappear. It becomes available again and can be processed later. This behavior protects against transient failures and prevents data loss. Most of the time, that's exactly what we want.&lt;/p&gt;
&lt;p&gt;However, if the underlying problem is persistent—a broken dependency, an invalid payload, or a misconfigured service—the same message may be processed repeatedly while consuming compute resources and generating additional pressure on downstream systems. The same principle appears in event-driven architectures.&lt;/p&gt;
&lt;p&gt;Services such as EventBridge improve reliability by attempting redelivery when consumers fail. That's an incredibly valuable capability. However, it also means that consumers must be designed with duplicate events in mind. Retries improve resilience. But resilience is never free.&lt;/p&gt;
&lt;p&gt;Every retry consumes resources, generates traffic, and increases complexity. The question is not whether retries are good or bad. The question is whether the system can absorb the consequences when retries occur at scale.&lt;/p&gt;
&lt;h3 id=&quot;designing-retries-that-help&quot;&gt;Designing Retries That Help&lt;/h3&gt;
&lt;p&gt;Over the years, I've become less interested in whether a system retries and more interested in how it retries. The difference matters. One of the simplest improvements is &lt;strong&gt;exponential backoff&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Instead of retrying every second, the delay increases after each attempt. A request may wait one second, then two, then four, then eight. This gives dependencies time to recover and reduces the likelihood of overwhelming a struggling service.&lt;/p&gt;
&lt;p&gt;Even then, another challenge remains. Imagine a million clients all deciding to retry after exactly eight seconds. Eight seconds later, they arrive together. The system experiences another traffic spike.&lt;/p&gt;
&lt;p&gt;This is why mature systems often introduce &lt;strong&gt;jitter&lt;/strong&gt;, adding a small amount of randomness to retry timing. The overall volume remains similar, but the requests become distributed over time rather than arriving in synchronized waves.&lt;/p&gt;
&lt;p&gt;Another valuable pattern is the &lt;strong&gt;circuit breaker&lt;/strong&gt;. When a dependency is clearly unavailable, continuing to send requests often provides little benefit. A circuit breaker temporarily stops traffic, allowing the dependency to recover while protecting the rest of the system from unnecessary load.&lt;/p&gt;
&lt;p&gt;And sometimes, the most responsible decision is not to retry immediately at all.&lt;/p&gt;
&lt;p&gt;Sometimes the correct answer is to move the failed message to a &lt;strong&gt;Dead Letter Queue&lt;/strong&gt;, investigate the cause, and replay it later under controlled conditions. That approach may feel less aggressive than continuous retries, but it often prevents a localized issue from becoming a platform-wide incident.&lt;/p&gt;
&lt;h3 id=&quot;a-different-way-to-think-about-resilience&quot;&gt;A Different Way to Think About Resilience&lt;/h3&gt;
&lt;p&gt;Early in my career, I thought resilience was mostly about making systems persistent.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If something failed, try again.&lt;/li&gt;
&lt;li&gt;If it failed again, try harder.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Experience changed that perspective.&lt;/p&gt;
&lt;p&gt;Today, I think resilience is often about &lt;strong&gt;restraint&lt;/strong&gt;. It's about understanding that every recovery mechanism introduces new behavior into the system. It's about recognizing that retries are not free. It's about accepting that the healthiest response to failure is not always immediate action. Sometimes the best thing a system can do is wait. Sometimes it's stop. Sometimes it's fail gracefully.&lt;/p&gt;
&lt;p&gt;The most resilient cloud architectures aren't the ones that retry forever. They're the ones that understand the difference between recovering from a problem and amplifying it. Because in distributed systems, the outage doesn't always begin with the first failure. Sometimes it begins with the second attempt.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21369/the-art-of-cloud-survival-when-retries-become-the-outage</guid>
<pubDate>Thu, 25 Jun 2026 13:27:41 +0000</pubDate>
</item>
<item>
<title>The Data Engineer’s Sandbox: Architecting for Autonomy with LXD</title>
<link>https://coderlegion.com/13481/the-data-engineers-sandbox-architecting-for-autonomy-with-lxd</link>
<description>&lt;p&gt;As Data Engineers, we often live in the cloud. Our daily lives are governed by S3 buckets, Glue jobs, and Kafka clusters. But there is a silent pressure that every senior professional feels: the cost of experimentation.&lt;/p&gt;
&lt;p&gt;How many times have you hesitated to spin up a resource just to validate a simple schema or a transformation logic? We've all been there—balancing the need for agility with the responsibility of cost-awareness.&lt;/p&gt;
&lt;p&gt;I’ve always believed that autonomy is a superpower. True technical mastery comes when you can prototype &quot;blindly,&quot; without an internet connection, directly on your machine, and with zero impact on your company's AWS bill.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why I moved away from heavy VMs&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For years, the choice was either a heavy Virtual Machine (VM) that drained my laptop’s RAM or a Docker container that, while fast, sometimes lacked the persistence and system-level isolation I needed for a long-term practice environment.&lt;/p&gt;
&lt;p&gt;When I started working with Ubuntu 24.04, I found the perfect middle ground: LXD. It offers the &quot;system feel&quot; of a VM with the &quot;instant speed&quot; of a container. It became my go-to for building local SQL sandboxes where I can fail fast, learn deep, and spend zero.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Blueprint: Setting up your Local Engine&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The goal is simple: a functional database server running in minutes.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The Foundation&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We start by initializing the hypervisor to manage our internal networking.&lt;/p&gt;
&lt;p&gt;sudo snap install lxd&lt;br&gt;
sudo lxd init --auto&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Launching the Instance&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We choose a lightweight Ubuntu image as our dedicated server.&lt;/p&gt;
&lt;p&gt;lxc launch ubuntu:24.04 sql-practice&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &quot;Hardware&quot; Setup&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We enter the container and transform that empty shell into a robust PostgreSQL engine.&lt;/p&gt;
&lt;p&gt;lxc exec sql-practice -- bash&lt;br&gt;
apt update &amp;amp;&amp;amp; apt install -y postgresql postgresql-contrib&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;From Infrastructure to Impact&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Setting up the &quot;pipes&quot; is only half the battle. As engineers, we care about the data. In my experience building event-driven architectures and streaming pipelines with Kafka, I’ve learned that a local sink is the best place to test idempotency and schema evolution before touching a production environment.&lt;/p&gt;
&lt;p&gt;Using the COPY command for massive local ingestion or pushing CSV files directly into the container (lxc file push) are the types of &quot;unsexy&quot; but highly efficient workflows that define a professional.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Closing thoughts: Design first&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;My philosophy is simple: Systems must be resilient and observable from day one. That includes our local dev environments. Complexity is a cost, and simplicity scales better.&lt;/p&gt;
&lt;p&gt;By mastering these local sandboxes, we aren't just saving a few dollars on an AWS bill; we are reclaiming our creative freedom as builders.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;About the Author&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I am Rocío Baigorria, a Senior Data Engineer and co-leader of AWS Girls Argentina. I specialize in building &quot;institutional-grade&quot; data platforms where reliability, cost-efficiency, and business impact are core design principles.&lt;/p&gt;
&lt;p&gt;Connect with me on LinkedIn: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/rociobaigorria/&quot;&gt;https://www.linkedin.com/in/rociobaigorria/&lt;/a&gt; &lt;/p&gt;
&lt;p&gt;Explore my Resources: &lt;a rel=&quot;nofollow&quot; href=&quot;https://tr.ee/2t-m587551&quot;&gt;https://tr.ee/2t-m587551&lt;/a&gt; &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13481/the-data-engineers-sandbox-architecting-for-autonomy-with-lxd</guid>
<pubDate>Wed, 25 Mar 2026 14:21:54 +0000</pubDate>
</item>
<item>
<title>From Delayed Data to Real-Time Decisions: Building an Event-Driven Pipeline on AWS</title>
<link>https://coderlegion.com/13228/from-delayed-data-to-real-time-decisions-building-an-event-driven-pipeline-on-aws</link>
<description>&lt;p&gt;At 2:17 AM, the dashboard wasn’t moving.&lt;/p&gt;
&lt;p&gt;Transactions were still coming in, but the numbers on the screen were frozen. The system was running, but the data was already 10 minutes behind.&lt;/p&gt;
&lt;p&gt;At that scale, 10 minutes wasn’t just delay — it meant decisions based on outdated information.&lt;/p&gt;
&lt;p&gt;That was the moment everything had to change.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Problem: When Data Arrives Too Late&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In many systems, data pipelines are built around batch processing or synchronous APIs. They work — until they don’t.&lt;/p&gt;
&lt;p&gt;As traffic grows, these systems start to break in predictable ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Increasing latency&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bottlenecks in database reads&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inconsistent data across services&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Delayed analytics and reporting&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The core issue is simple:&lt;br&gt;
the system reacts too late.&lt;/p&gt;
&lt;p&gt;To solve this, the architecture had to shift from request-driven to event-driven.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Shift: Thinking in Events&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Instead of asking for data, services react to events as they happen.&lt;/p&gt;
&lt;p&gt;Every transaction becomes an event. Every state change is recorded as a fact.&lt;/p&gt;
&lt;p&gt;This changes everything:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Systems become asynchronous&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Components are loosely coupled&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scalability comes from partitioning, not vertical scaling&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal was clear:&lt;/p&gt;
&lt;p&gt;Process more than 15,000 events per second with sub-50ms latency — reliably.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Designing the Pipeline&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The first version of the system was built as a distributed architecture using open-source tools.&lt;/p&gt;
&lt;p&gt;Core components:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- Apache Kafka for event streaming

- Kafka Streams for real-time processing

- Spring Boot for processing services

- PostgreSQL for durable storage

- Redis for low-latency reads

- Prometheus and Grafana for observability
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Event Flow&lt;/p&gt;
&lt;p&gt;The pipeline follows a simple but powerful structure:&lt;/p&gt;
&lt;p&gt;Producer → Kafka → Stream Processing → Storage → Analytics&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Producers publish transaction events&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Events are serialized and validated&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kafka distributes them across partitions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stream processors handle transformations and aggregations&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Results are stored and made available for real-time queries&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This allowed continuous processing instead of waiting for batches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Challenges (Where Systems Usually Break)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Designing the pipeline was only part of the work. Making it reliable at scale required solving real issues:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Event Duplication&lt;br&gt;
Ensuring exactly-once processing required transactional guarantees and careful handling of offsets.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Latency Spikes&lt;br&gt;
Under heavy load, consumer lag increased. This was mitigated with parallel consumers and optimized batching.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Throughput Optimization&lt;br&gt;
Producer batching (32KB) and Snappy compression significantly improved performance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Resource Bottlenecks&lt;br&gt;
Connection pooling and efficient serialization reduced pressure on downstream systems.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These are the kinds of problems that don’t show up in diagrams — only in real systems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Results&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After multiple iterations and optimizations, the system achieved:&lt;/p&gt;
&lt;p&gt;Throughput: 15,000+ events per second&lt;/p&gt;
&lt;p&gt;Latency (P99): under 50ms&lt;/p&gt;
&lt;p&gt;Availability: 99.95%&lt;/p&gt;
&lt;p&gt;Data loss: 0% (exactly-once processing)&lt;/p&gt;
&lt;p&gt;More importantly, this enabled near real-time anomaly detection.&lt;/p&gt;
&lt;p&gt;What used to take minutes could now happen in milliseconds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Moving to AWS: Same Architecture, Less Infrastructure&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Once the system was stable, the next step was moving to the cloud.&lt;/p&gt;
&lt;p&gt;The key decision was not to redesign the system — only to replace infrastructure with managed services.&lt;/p&gt;
&lt;p&gt;Cloud Architecture: &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Event ingestion via Amazon EventBridge or Amazon MSK&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Processing with AWS Lambda&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Workflow orchestration using AWS Step Functions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Storage with DynamoDB or Amazon RDS&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Monitoring through Amazon CloudWatch&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each component maps directly to the original architecture.&lt;/p&gt;
&lt;p&gt;The logic stays the same. The operations overhead disappears.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Key Insight&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The most important lesson wasn’t about tools.&lt;/p&gt;
&lt;p&gt;It was about understanding the system.&lt;/p&gt;
&lt;p&gt;Event-driven architectures are built on a few core principles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Events are immutable&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Systems react asynchronously&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scalability comes from partitioned streams&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;State is derived from event logs&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once these are clear, moving between technologies becomes straightforward.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Real-time systems are not about speed alone.&lt;/p&gt;
&lt;p&gt;They are about enabling better decisions.&lt;/p&gt;
&lt;p&gt;When data arrives late, the system might still work — but the business doesn’t.&lt;/p&gt;
&lt;p&gt;Designing event-driven pipelines is ultimately about reducing the gap between what happens and what the system knows.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Design, therefore I exist.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13228/from-delayed-data-to-real-time-decisions-building-an-event-driven-pipeline-on-aws</guid>
<pubDate>Thu, 19 Mar 2026 21:11:10 +0000</pubDate>
</item>
<item>
<title>The Senior Engineer's Blueprint: Architecting Data Pipelines for Business Growth</title>
<link>https://coderlegion.com/12452/the-senior-engineers-blueprint-architecting-data-pipelines-for-business-growth</link>
<description>&lt;p&gt;&lt;strong&gt;What Separates a Junior Engineer from a Senior Data Engineer?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In this blueprint, I'm going to reveal how senior engineers architect for the &quot;probable future,&quot; and the specific business metrics you need to build into your pipelines from day one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The difference&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The junior mindset:&lt;/strong&gt; &lt;em&gt;&quot;How do I move this data from A to B as efficiently as possible?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The senior mindset:&lt;/strong&gt; &lt;em&gt;&quot;Why are we moving this data, and what business questions will the executive team need this data to answer six months from now?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That's the main difference. The junior is focused on the present, while the senior is focused on the future.&lt;br&gt;
&lt;strong&gt;To close this gap:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shift your mindset to think like a senior engineer.&lt;/li&gt;
&lt;li&gt;Develop business intelligence skills and think like a business analyst and the business owner.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To develop business intelligence skills, you need to know what the business goal is. They shift based on the directin of the current but what stays constant are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;To make more money&lt;/li&gt;
&lt;li&gt;To get more customers&lt;/li&gt;
&lt;li&gt;To spend less&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;and-your-job-is-assist-in-answering-the-how-and-why-questions-a-k-a-solving-problems-&quot;&gt;And your job is assist in answering the how and why questions.(a.k.a solving problems)&lt;/h3&gt;
&lt;h3 id=&quot;architecting-for-the-probable-future&quot;&gt;Architecting for the Probable Future&lt;/h3&gt;
&lt;p&gt;Pipelines fail at predictive analytics when built purely for historical reporting. Stop capturing just what happened today. Start designing schemas that predict tomorrow. &lt;/p&gt;
&lt;p&gt;Below are the core metrics and specific table attributes every data engineer should build into their models.&lt;br&gt;
PS: Calculated fields are left for dimensional modeling, not for relational modeling.&lt;/p&gt;
&lt;h4 id=&quot;1-financial-metrics-following-the-money&quot;&gt;1. Financial Metrics: Following the Money&lt;/h4&gt;
&lt;p&gt;A pipeline must provide real-time visibility into margins, not just revenue. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gross Profit Analysis&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gross_revenue_amt&lt;/code&gt; / &lt;code&gt;total_amt&lt;/code&gt;: Top-line sales before deductions.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cogs_amt&lt;/code&gt;: Cost of Goods Sold.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;discount_amt&lt;/code&gt;: Applied promotions or coupons.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;net_margin_pct&lt;/code&gt;: Real-time profitability metric (&lt;code&gt;(Gross Revenue - COGS - Discounts) / Gross Revenue&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Frozen Money &amp;amp; Dead Stock&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;days_inventory_on_hand_qty&lt;/code&gt;: How long cash is tied up in physical products.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shrinkage_rate_pct&lt;/code&gt;: Loss due to theft, damage, or error.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_dead_stock_flg&lt;/code&gt;: Boolean identifying unmoving inventory.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Surge Management &amp;amp; Revenue Alerts&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;revenue_surge_velocity_amt&lt;/code&gt;: Rate of incoming revenue to trigger auto-scaling or business alerts.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;abandoned_cart_value_amt&lt;/code&gt;: Immediate lost revenue during traffic spikes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-the-customer-metrics-predicting-human-behavior&quot;&gt;2. The Customer Metrics: Predicting Human Behavior&lt;/h4&gt;
&lt;p&gt;Don't wait for a cancellation event. Build architectures that catch warning signs early to identify reasons for customer order cancellations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Customer Turnover (Churn)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;days_since_last_login_qty&lt;/code&gt;: Leading indicator of drop-off.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;email_open_rate_pct&lt;/code&gt;: Engagement health.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;predicted_churn_risk_score&lt;/code&gt;: ML-derived metric based on usage deceleration. &lt;em&gt;(track every login, email click, and page view).&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Customer Lifetime Value (CLV)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;historical_clv_amt&lt;/code&gt;: Total historical customer spend.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;avg_order_value_amt&lt;/code&gt;: (AOV) Trailing 12-month average transaction size.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Customer Sentiment &amp;amp; Customer Satisfaction Score&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;csat_score_num&lt;/code&gt;: Post-purchase satisfaction score (1-5).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;support_ticket_cnt&lt;/code&gt;: Number of active or historical complaints.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-operational-efficiency-inventory-management-and-workforce-performance&quot;&gt;3. Operational Efficiency: Inventory Management and Workforce Performance&lt;/h4&gt;
&lt;p&gt;Your dimensional models must natively support core company KPIs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Inventory Management&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;inventory_turnover_ratio&lt;/code&gt;: How often inventory is sold and replaced.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gmroi_pct&lt;/code&gt;: Gross Margin Return on Investment.(track buying cost and sale price)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sell_through_rate_pct&lt;/code&gt;: Percentage of received inventory actually sold.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Workforce Performance&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sales_per_employee_amt&lt;/code&gt;: Revenue efficiency capability.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;employee_turnover_rate_pct&lt;/code&gt;: Organizational health indicator.(How often employees leave the company)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;support_resolution_time_mins&lt;/code&gt;: Productivity metric for customer service reps.(how long it takes to resolve a support ticket)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;4-operational-cost&quot;&gt;4. Operational Cost&lt;/h4&gt;
&lt;p&gt;Tracking the cost of acquiring and maintaining business.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;customer_acquisition_cost_amt&lt;/code&gt;: (CAC) Blended cost to acquire a net-new customer. &lt;em&gt;(track every customer acquisition event, including marketing spend, sales, and customer service interactions).&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return_on_ad_spend_pct&lt;/code&gt;: (ROAS) Marketing revenue vs. advertising cost. &lt;/li&gt;
&lt;li&gt;&lt;code&gt;promotion_effort_cost_amt&lt;/code&gt;: Cost of active campaigns and outreach per user. (track discounts and promotions )&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;taking-the-next-step&quot;&gt;Taking the Next Step&lt;/h3&gt;
&lt;p&gt;To help you skip the trial-and-error phase, I’m working on high-value technical resources to level up your engineering:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Premium Design Templates:&lt;/strong&gt; Plug-and-play architectures and product reader dimensional data models for the modern data stack.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &quot;Every Table&quot; Metrics Handbook:&lt;/strong&gt; A guide detailing the exact audit columns, SCD strategies, and hidden metrics you should proactively include on &lt;em&gt;every single table&lt;/em&gt; you deploy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Building an Analytics Warehouse from Scratch (Free Walkthrough):&lt;/strong&gt; In an upcoming post, I’ll provide a complete technical walkthrough on web scraping your own data(or using available data) and building a warehouse from the ground up, for free.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Make sure you're subscribed so you don't miss these upcoming drops. &lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Let's build up the habit of not just asking &quot;what's the easiest way to ingest this?&quot;, but mainly &quot;How will this data be used to save or make X money?&quot; and &quot;Why is this important to the Y?&quot;&lt;/p&gt;
&lt;p&gt;That is how you become a senior engineer.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Drop a comment below with the biggest architectural mistake you made early in your career.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12452/the-senior-engineers-blueprint-architecting-data-pipelines-for-business-growth</guid>
<pubDate>Sat, 07 Mar 2026 11:31:47 +0000</pubDate>
</item>
</channel>
</rss>