<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CobuildX AI</title>
    <description>The latest articles on DEV Community by CobuildX AI (cobuildx-ai).</description>
    <link>https://dev.to/cobuildx-ai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F15088%2Fd94010e3-279b-40ed-b4bc-4f6158184522.jpeg</url>
      <title>DEV Community: CobuildX AI</title>
      <link>https://dev.to/cobuildx-ai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cobuildx-ai"/>
    <language>en</language>
    <item>
      <title>Introducing PII Extractor: A Lightweight Model for PII Extraction at Warp Speed</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Thu, 29 Oct 2026 05:12:16 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/introducing-pii-extractor-a-lightweight-model-for-pii-extraction-at-warp-speed-59ga</link>
      <guid>https://dev.to/cobuildx-ai/introducing-pii-extractor-a-lightweight-model-for-pii-extraction-at-warp-speed-59ga</guid>
      <description>&lt;p&gt;Three open models, from 17M to 68M parameters, built for a single job: finding personal information in text accurately enough for production and fast enough to run on every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Highlights
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;88.3% F1 on Privacy-Bench&lt;/strong&gt;, a benchmark of human-annotated business documents, with a 68M-parameter model. PII-Tracer (596M) scores 86.5%, GLiNER2-PII (205M) 83.7% and nvidia GLiNER-PII (570M) 66.4%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The smallest model, at 17M parameters, is within half a point of PII-Tracer&lt;/strong&gt; at 1/35 of its size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;37% fewer false alarms than PII-Tracer&lt;/strong&gt; while finding almost as much personal data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12 types of PII&lt;/strong&gt;, from names and emails to government IDs, secrets and IP addresses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Openly available&lt;/strong&gt; on Hugging Face under Apache-2.0: &lt;a href="https://huggingface.co/CobuildX-AI/cbx-pii-extractor-68M" rel="noopener noreferrer"&gt;pii-extractor-68M&lt;/a&gt;, &lt;a href="https://huggingface.co/CobuildX-AI/cbx-pii-extractor-32M" rel="noopener noreferrer"&gt;pii-extractor-32M&lt;/a&gt; and &lt;a href="https://huggingface.co/CobuildX-AI/cbx-pii-extractor-17M" rel="noopener noreferrer"&gt;pii-extractor-17M&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every message sent to an LLM, every document added to a search index and every support ticket written to a log raises the same question: &lt;em&gt;does this contain personal information, and exactly where?&lt;/em&gt; The check runs on every request, so its speed and cost set the floor for everything behind it. Yet the strongest PII detectors available today carry hundreds of millions of parameters, and general-purpose LLMs carry far more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PII Extractor&lt;/strong&gt; is a family of three compact models purpose-built to extract personal data from the text you send it, and light enough to fine-tune for your own use case. At 68M parameters, it is the most accurate PII detector we tested on real business documents. At 17M, it processes 1,000 tokens in 0.17 seconds on four CPU threads, with no GPU at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PII detection belongs in a small model
&lt;/h2&gt;

&lt;p&gt;Finding PII is not open-ended generation. A model reads a text once and decides, for every word, whether it belongs to a name, an email address, an account number or nothing at all. The answer is a set of character positions that can be redacted, masked or routed.&lt;/p&gt;

&lt;p&gt;That makes it a natural fit for a small encoder. An encoder reads the whole text in both directions at once, so every word is judged in its full context: in "Will will sign the contract on Monday", the first "Will" is a name and the second is not. There is no text to generate and no output to parse, and the cost of a call depends only on the length of the input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet PII Extractor
&lt;/h2&gt;

&lt;p&gt;PII Extractor is fine-tuned from Ettin [6], an open encoder family from Johns Hopkins University with an architecture similar to ModernBERT [7]. The three models are built on Ettin-68M, Ettin-32M and Ettin-17M.&lt;/p&gt;

&lt;p&gt;Each model reads up to 8K tokens in a single pass, splits longer texts automatically, and returns every piece of personal data with its exact position and one of 12 types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it covers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PERSON&lt;/td&gt;
&lt;td&gt;Names, including first names alone, initials and nicknames&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EMAIL&lt;/td&gt;
&lt;td&gt;Email addresses, including written-out forms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHONE&lt;/td&gt;
&lt;td&gt;Phone and fax numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADDRESS&lt;/td&gt;
&lt;td&gt;Street addresses; a city or country on its own is not labelled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DATE&lt;/td&gt;
&lt;td&gt;Dates of birth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;Web addresses, with or without &lt;code&gt;https://&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACCOUNT_NUMBER&lt;/td&gt;
&lt;td&gt;Bank, card, IBAN and customer account numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GOV_ID&lt;/td&gt;
&lt;td&gt;Social security, passport, national ID, tax ID and driving licence numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USERNAME&lt;/td&gt;
&lt;td&gt;Usernames and handles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SECRET&lt;/td&gt;
&lt;td&gt;Passwords, PINs, access codes, API keys and tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP_ADDRESS&lt;/td&gt;
&lt;td&gt;IPv4 and IPv6 addresses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OTHER_ID&lt;/td&gt;
&lt;td&gt;Employee, patient, member and device IDs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Leading accuracy on business documents
&lt;/h2&gt;

&lt;p&gt;On Privacy-Bench, 656 pages of real business documents annotated by people, PII Extractor-68M scores 88.3% F1, ahead of PII-Tracer at 86.5%, GLiNER2-PII at 83.7% and nvidia GLiNER-PII at 66.4%. PII Extractor-32M also scores above all three, at 18× smaller than PII-Tracer.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Privacy-Bench&lt;/th&gt;
&lt;th&gt;PII-TRACE&lt;/th&gt;
&lt;th&gt;GPU, 1,000 tokens&lt;/th&gt;
&lt;th&gt;CPU, 1,000 tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PII Extractor-68M&lt;/td&gt;
&lt;td&gt;68M&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88.3%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;82.4%&lt;/td&gt;
&lt;td&gt;23 ms&lt;/td&gt;
&lt;td&gt;1.17 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PII Extractor-32M&lt;/td&gt;
&lt;td&gt;32M&lt;/td&gt;
&lt;td&gt;87.5%&lt;/td&gt;
&lt;td&gt;84.6%&lt;/td&gt;
&lt;td&gt;16 ms&lt;/td&gt;
&lt;td&gt;0.40 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PII Extractor-17M&lt;/td&gt;
&lt;td&gt;17M&lt;/td&gt;
&lt;td&gt;86.0%&lt;/td&gt;
&lt;td&gt;81.2%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.17 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PII-Tracer [3]&lt;/td&gt;
&lt;td&gt;596M&lt;/td&gt;
&lt;td&gt;86.5%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;92.6%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;271 ms&lt;/td&gt;
&lt;td&gt;9.15 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLiNER2-PII [4]&lt;/td&gt;
&lt;td&gt;205M&lt;/td&gt;
&lt;td&gt;83.7%&lt;/td&gt;
&lt;td&gt;62.2%&lt;/td&gt;
&lt;td&gt;199 ms&lt;/td&gt;
&lt;td&gt;3.68 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nvidia GLiNER-PII [8]&lt;/td&gt;
&lt;td&gt;570M&lt;/td&gt;
&lt;td&gt;66.4%&lt;/td&gt;
&lt;td&gt;75.1%&lt;/td&gt;
&lt;td&gt;429 ms&lt;/td&gt;
&lt;td&gt;9.45 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Emails and phone numbers are close to solved for every model except nvidia GLiNER-PII, which marks the names inside an email address such as &lt;code&gt;megan.donovan@lilly.com&lt;/code&gt; as two names and scores 34% on emails. Names sit at 86–88% for both PII Extractor and PII-Tracer. Addresses are where PII Extractor pulls furthest ahead: 63% for the 68M model, against 51% for PII-Tracer and 8% for GLiNER2-PII. Account numbers and links remain hard for every model, and appear only 167 and 21 times in the benchmark.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;n/a: PII-Tracer does not support usernames, so it is not scored on them. Dates and secrets are not shown because Privacy-Bench does not annotate them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On PII-TRACE, a benchmark of multi-turn conversations released alongside PII-Tracer, PII-Tracer leads with 92.6% against 84.6% for PII Extractor-32M, mostly because that benchmark also labels meeting dates and reference codes, which PII Extractor deliberately leaves unmarked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fewer false alarms, cleaner redaction
&lt;/h2&gt;

&lt;p&gt;An F1 score combines two different failures. A missed value leaks personal data. A false alarm hides ordinary text and makes a document harder to use, which is why over-eager detectors tend to get switched off.&lt;/p&gt;

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

&lt;p&gt;Out of 8,490 PII values in Privacy-Bench, PII Extractor-68M and PII-Tracer miss a similar number (613 and 567). The difference is in what they flag that isn't PII: 342 false alarms for PII Extractor-68M against 546 for PII-Tracer, 37% fewer. GLiNER2-PII makes the fewest false alarms but misses about one value in five. nvidia GLiNER-PII misses the fewest values (538) but raises 3,194 false alarms, more than nine times as many as PII Extractor-68M.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fast enough for every request, even without a GPU
&lt;/h2&gt;

&lt;p&gt;We timed every model on the same 50 text samples at each of three lengths, 1,000, 2,000 and 3,000 tokens, and report the median time per sample.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxqv2xcpjf0efrkiu1w47.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxqv2xcpjf0efrkiu1w47.png" alt="Median time per sample on a T4 GPU" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On a T4 GPU, all three PII Extractor models stay under 70 milliseconds per sample, even at 3,000 tokens. PII-Tracer takes 271 milliseconds at 1,000 tokens and almost 2 seconds at 3,000. nvidia GLiNER-PII is the slowest at 1,000 tokens, at 429 milliseconds, and takes 1.6 seconds at 3,000.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzh1x40fmsdz6mt8uwlei.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzh1x40fmsdz6mt8uwlei.png" alt="Median time per sample on four CPU threads" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without a GPU, PII Extractor-17M takes 0.17 seconds at 1,000 tokens and 0.48 seconds at 3,000, against 9.15 and 37.37 seconds for PII-Tracer. That makes it practical on ordinary application servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which size to choose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PII Extractor-68M&lt;/strong&gt; for the highest accuracy, on a GPU.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PII Extractor-32M&lt;/strong&gt; for the best balance of accuracy and speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PII Extractor-17M&lt;/strong&gt; for CPU-only servers and the lowest cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How we evaluated
&lt;/h2&gt;

&lt;p&gt;We ran every model ourselves, on the same texts, with the same scoring. &lt;strong&gt;Privacy-Bench&lt;/strong&gt; [1] contains 656 pages of Word and PDF business documents with 8,490 PII values annotated by people. &lt;strong&gt;PII-TRACE&lt;/strong&gt; [2] contains 500 multi-turn conversations with 2,442 annotated values.&lt;/p&gt;

&lt;p&gt;A prediction counts as correct only when its start, end and type all match the annotation. Each benchmark's types were mapped to ours as below, and every model was scored on the types that both the benchmark and the model support.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PII Extractor&lt;/th&gt;
&lt;th&gt;Privacy-Bench&lt;/th&gt;
&lt;th&gt;PII-TRACE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PERSON&lt;/td&gt;
&lt;td&gt;NAME_GIVEN, NAME_FAMILY&lt;/td&gt;
&lt;td&gt;private_person&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EMAIL&lt;/td&gt;
&lt;td&gt;EMAIL_ADDRESS&lt;/td&gt;
&lt;td&gt;private_email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHONE&lt;/td&gt;
&lt;td&gt;PHONE_NUMBER&lt;/td&gt;
&lt;td&gt;private_phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADDRESS&lt;/td&gt;
&lt;td&gt;LOCATION_ADDRESS&lt;/td&gt;
&lt;td&gt;private_address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;private_url&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACCOUNT_NUMBER&lt;/td&gt;
&lt;td&gt;ACCOUNT_NUMBER&lt;/td&gt;
&lt;td&gt;account_number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USERNAME&lt;/td&gt;
&lt;td&gt;USERNAME&lt;/td&gt;
&lt;td&gt;Not annotated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DATE&lt;/td&gt;
&lt;td&gt;Not annotated&lt;/td&gt;
&lt;td&gt;private_date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SECRET&lt;/td&gt;
&lt;td&gt;Not annotated&lt;/td&gt;
&lt;td&gt;secret&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;GOV_ID, IP_ADDRESS and OTHER_ID are not annotated in either benchmark, so this evaluation does not cover them.&lt;/p&gt;

&lt;p&gt;Latency is the median of 50 text samples per length, processed one at a time on a Tesla T4 GPU and on four CPU threads. PII-TRACE scores use its public subset, so they are not comparable with the official test results in the PII-Tracer paper [2].&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;English only.&lt;/strong&gt; The models were trained and tested on English text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Our definitions of PII.&lt;/strong&gt; Sensitive attributes such as health, religion or ethnicity are not detected in this version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rare types.&lt;/strong&gt; Usernames, account numbers and links are found less reliably than names, emails and phones.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;We're bringing &lt;strong&gt;dynamic labels at run time&lt;/strong&gt; to our super-efficient, lightweight models: describe the PII types you need when you call the model, in the style of GLiNER [5], and get them back with no retraining, at a size that still runs on every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try PII Extractor
&lt;/h2&gt;

&lt;p&gt;All three models are on Hugging Face under the Apache-2.0 licence: &lt;a href="https://huggingface.co/CobuildX-AI/cbx-pii-extractor-68M" rel="noopener noreferrer"&gt;pii-extractor-68M&lt;/a&gt;, &lt;a href="https://huggingface.co/CobuildX-AI/cbx-pii-extractor-32M" rel="noopener noreferrer"&gt;pii-extractor-32M&lt;/a&gt; and &lt;a href="https://huggingface.co/CobuildX-AI/cbx-pii-extractor-17M" rel="noopener noreferrer"&gt;pii-extractor-17M&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Need PII detection tuned to your own definitions, documents or languages? Get in touch with our team at &lt;strong&gt;&lt;a href="https://cobuildx.ai/contact" rel="noopener noreferrer"&gt;cobuildx.ai/contact&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Benchmarks
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;TonicAI. &lt;em&gt;Privacy-Bench&lt;/em&gt; (human-annotated documents). &lt;a href="https://huggingface.co/datasets/TonicAI/Privacy-Bench" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/TonicAI/Privacy-Bench&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;K. Zhang et al. &lt;em&gt;PII-TRACE: A Benchmark for Context-Aware PII Detection in Multi-Turn LLM Conversations.&lt;/em&gt; arXiv:2609.22200, 2026. &lt;a href="https://huggingface.co/datasets/perplexity-ai/PII-TRACE" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/perplexity-ai/PII-TRACE&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Models and methods
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Perplexity AI. &lt;em&gt;PII-Tracer.&lt;/em&gt; &lt;a href="https://huggingface.co/perplexity-ai/PII-Tracer" rel="noopener noreferrer"&gt;https://huggingface.co/perplexity-ai/PII-Tracer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fastino. &lt;em&gt;GLiNER2-PII.&lt;/em&gt; &lt;a href="https://huggingface.co/fastino/gliner2-privacy-filter-PII-multi" rel="noopener noreferrer"&gt;https://huggingface.co/fastino/gliner2-privacy-filter-PII-multi&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;U. Zaratiana et al. &lt;em&gt;GLiNER: Generalist Model for Named Entity Recognition using Bidirectional Transformer.&lt;/em&gt; NAACL 2024. arXiv:2311.08526.&lt;/li&gt;
&lt;li&gt;O. Weller et al. &lt;em&gt;Seq vs Seq: An Open Suite of Paired Encoders and Decoders&lt;/em&gt; (Ettin). arXiv:2507.11412, 2025.&lt;/li&gt;
&lt;li&gt;B. Warner et al. &lt;em&gt;Smarter, Better, Faster, Longer: A Modern Bidirectional Encoder for Fast, Memory Efficient, and Long Context Finetuning and Inference&lt;/em&gt; (ModernBERT). arXiv:2412.13663, 2024.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Other baselines
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;NVIDIA. &lt;em&gt;gliner-PII.&lt;/em&gt; &lt;a href="https://huggingface.co/nvidia/gliner-PII" rel="noopener noreferrer"&gt;https://huggingface.co/nvidia/gliner-PII&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://cobuildx.ai/blog/pii-extractor" rel="noopener noreferrer"&gt;cobuildx.ai&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>privacy</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Moving Storage from GCP to AWS: Cloud Storage, Disks and File Shares</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Wed, 28 Oct 2026 10:29:34 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/moving-storage-from-gcp-to-aws-cloud-storage-disks-and-file-shares-17ko</link>
      <guid>https://dev.to/cobuildx-ai/moving-storage-from-gcp-to-aws-cloud-storage-disks-and-file-shares-17ko</guid>
      <description>&lt;p&gt;&lt;em&gt;A plain-English guide to moving storage from Google Cloud to AWS: Cloud Storage, Persistent Disk, Filestore, backups and transfer tools. What replaces what, how storage classes and access change, how to copy the data safely, what changes on the bill, and a step-by-step plan.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every app keeps data somewhere. Photos that users upload. Disks that servers run on. Folders that many machines share. Backups. Records that must be kept for years. All of this is "storage". When you move from Google Cloud (GCP) to AWS, storage is often the biggest thing to carry across.&lt;/p&gt;

&lt;p&gt;On GCP, storage means three main things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Storage&lt;/strong&gt; keeps files in buckets. A bucket is like a big folder in the cloud. Each file in it is called an object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent Disk and Hyperdisk&lt;/strong&gt; are the hard drives attached to virtual machines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filestore&lt;/strong&gt; is a shared folder that many machines can use at the same time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Around these sit backups and tools for copying data.&lt;/p&gt;

&lt;p&gt;Storage looks easy to move. A bucket is a bucket. But the details are different. The cheap "cold" storage works in a different way. Access rules are written in a different way. And copying many terabytes takes time and money. On top of that, every file must arrive whole. Not one file can be lost or changed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"S3 is effectively a living, breathing organism."&lt;/p&gt;

&lt;p&gt;— &lt;strong&gt;Andy Warfield&lt;/strong&gt;, VP and Distinguished Engineer at Amazon S3, in &lt;a href="https://www.allthingsdistributed.com/2023/07/building-and-operating-a-pretty-big-storage-system.html" rel="noopener noreferrer"&gt;&lt;em&gt;Building and operating a pretty big storage system called S3&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a good way to think about the move. You are not just dropping files into a box. You are moving into a service with its own habits. This guide covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which AWS service replaces each GCP storage service&lt;/li&gt;
&lt;li&gt;What works differently on AWS&lt;/li&gt;
&lt;li&gt;How to copy the data safely&lt;/li&gt;
&lt;li&gt;What changes on the bill&lt;/li&gt;
&lt;li&gt;A simple step-by-step plan&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Quick Map
&lt;/h2&gt;

&lt;p&gt;Here is each GCP storage service and its closest match on AWS. The rest of the post explains each row.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjn5k9l1fp6dduupjswgx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjn5k9l1fp6dduupjswgx.png" alt="Storage service map from Google Cloud to AWS: Cloud Storage to Amazon S3, Persistent Disk and Hyperdisk to Amazon EBS, Local SSD to EC2 instance store, Filestore to Amazon EFS or FSx, Backup and DR to AWS Backup, and Storage Transfer Service to AWS DataSync" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GCP&lt;/th&gt;
&lt;th&gt;Closest AWS match&lt;/th&gt;
&lt;th&gt;Watch out for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Storage&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html" rel="noopener noreferrer"&gt;Amazon S3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Cold storage works differently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autoclass&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/intelligent-tiering.html" rel="noopener noreferrer"&gt;S3 Intelligent-Tiering&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Very small files are not moved to cheaper tiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistent Disk / Hyperdisk&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/ebs/latest/userguide/what-is-ebs.html" rel="noopener noreferrer"&gt;Amazon EBS&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;You set disk speed yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regional Persistent Disk&lt;/td&gt;
&lt;td&gt;No direct match&lt;/td&gt;
&lt;td&gt;An EBS disk lives in one data center&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local SSD&lt;/td&gt;
&lt;td&gt;EC2 instance store&lt;/td&gt;
&lt;td&gt;Data is lost when the machine stops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filestore&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://docs.aws.amazon.com/efs/latest/ug/whatisefs.html" rel="noopener noreferrer"&gt;Amazon EFS&lt;/a&gt;, or Amazon FSx&lt;/td&gt;
&lt;td&gt;Pick by how the folder is shared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Cloud NetApp Volumes&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/what-is-fsx-ontap.html" rel="noopener noreferrer"&gt;Amazon FSx for NetApp ONTAP&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Same NetApp tools on both sides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backup and DR Service&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/aws-backup/latest/devguide/whatisbackup.html" rel="noopener noreferrer"&gt;AWS Backup&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Backup plans are set up again, not copied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage Transfer Service&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/datasync/latest/userguide/tutorial_transfer-google-cloud-storage.html" rel="noopener noreferrer"&gt;AWS DataSync&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;DataSync can read Cloud Storage directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transfer Appliance&lt;/td&gt;
&lt;td&gt;Copy over the network with DataSync&lt;/td&gt;
&lt;td&gt;Snowball is closed to new customers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What about Snowball?&lt;/strong&gt; Snowball was AWS's way to ship large amounts of data on a physical device. But AWS &lt;a href="https://docs.aws.amazon.com/snowball/latest/developer-guide/snowball-edge-availability-change.html" rel="noopener noreferrer"&gt;stopped offering Snowball Edge to new customers on November 7, 2025&lt;/a&gt;. For a move from one cloud to another, copying over the network is the normal path anyway.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Cloud Storage to Amazon S3
&lt;/h2&gt;

&lt;p&gt;Cloud Storage and &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html" rel="noopener noreferrer"&gt;Amazon S3&lt;/a&gt; work the same basic way. You make buckets. You put files in them. Each file has a name, its data and a few labels (called metadata). Almost everything you do in one, you can do in the other.&lt;/p&gt;

&lt;p&gt;Google even offers an &lt;a href="https://cloud.google.com/storage/docs/interoperability" rel="noopener noreferrer"&gt;S3-style way to talk to Cloud Storage&lt;/a&gt;. That is why many AWS tools can read your GCP buckets directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storage classes
&lt;/h3&gt;

&lt;p&gt;A storage class is a price plan for your files. Files you read often go in a normal class. Files you rarely read go in a cheaper "cold" class. You pay less to keep them, but more to read them.&lt;/p&gt;

&lt;p&gt;Both clouds have these classes, and the names roughly line up. But there is one big difference. On GCP, every class gives your file back in milliseconds, even Archive. On AWS, the two coldest classes do not. You must first &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/restoring-objects.html" rel="noopener noreferrer"&gt;ask AWS to restore the file&lt;/a&gt;, and that can take minutes or hours.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cloud Storage class&lt;/th&gt;
&lt;th&gt;S3 class to use&lt;/th&gt;
&lt;th&gt;Shortest time you pay for&lt;/th&gt;
&lt;th&gt;How fast you can read it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;S3 Standard&lt;/td&gt;
&lt;td&gt;No minimum&lt;/td&gt;
&lt;td&gt;Right away&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nearline&lt;/td&gt;
&lt;td&gt;S3 Standard-IA&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;Right away&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coldline&lt;/td&gt;
&lt;td&gt;S3 Glacier Instant Retrieval&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;Right away&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Archive&lt;/td&gt;
&lt;td&gt;S3 Glacier Instant Retrieval&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;Right away&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Archive (if nobody needs it fast)&lt;/td&gt;
&lt;td&gt;S3 Glacier Deep Archive&lt;/td&gt;
&lt;td&gt;180 days&lt;/td&gt;
&lt;td&gt;Hours, after a restore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autoclass&lt;/td&gt;
&lt;td&gt;S3 Intelligent-Tiering&lt;/td&gt;
&lt;td&gt;No minimum&lt;/td&gt;
&lt;td&gt;Right away&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These numbers come from the &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-class-intro.html" rel="noopener noreferrer"&gt;S3 storage class guide&lt;/a&gt;. One more thing to know: in S3 Standard-IA and Glacier Instant Retrieval, every file is billed as if it were at least 128 KB. If you have millions of tiny files, a cold class can cost more, not less.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; if an app still reads old files, move Archive to Glacier Instant Retrieval. Use Deep Archive only for files nobody needs in a hurry, like old backups and legal copies.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Bucket settings
&lt;/h3&gt;

&lt;p&gt;Most bucket settings have a direct match on AWS. They just have new names:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cloud Storage&lt;/th&gt;
&lt;th&gt;Amazon S3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle rules (move or delete old files)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html" rel="noopener noreferrer"&gt;S3 Lifecycle&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object versioning (keep old copies)&lt;/td&gt;
&lt;td&gt;S3 Versioning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dual-region and multi-region buckets&lt;/td&gt;
&lt;td&gt;A bucket in one region, plus &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/replication.html" rel="noopener noreferrer"&gt;S3 Replication&lt;/a&gt; if you need a second copy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retention lock (files can't be deleted)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html" rel="noopener noreferrer"&gt;S3 Object Lock&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your own encryption keys (Cloud KMS)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html" rel="noopener noreferrer"&gt;Encryption with AWS KMS keys&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pub/Sub notifications when a file changes&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html" rel="noopener noreferrer"&gt;S3 Event Notifications&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signed URLs (short-lived download links)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html" rel="noopener noreferrer"&gt;Pre-signed URLs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gcloud storage&lt;/code&gt; command line&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;aws s3&lt;/code&gt; command line&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Who can read and write
&lt;/h3&gt;

&lt;p&gt;On GCP, most teams give people and apps a role on the bucket, like "Storage Object Viewer". On AWS, access comes from two places. IAM policies say what a user or app may do. A bucket policy says who may use the bucket.&lt;/p&gt;

&lt;p&gt;Leave &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" rel="noopener noreferrer"&gt;Block Public Access&lt;/a&gt; turned on. It stops a bucket from being opened to the whole internet by mistake. New buckets have it on by default. If files really must be public, like images on a website, serve them through CloudFront instead of opening the bucket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disks: Persistent Disk to EBS
&lt;/h2&gt;

&lt;p&gt;Disks for virtual machines move to &lt;a href="https://docs.aws.amazon.com/ebs/latest/userguide/what-is-ebs.html" rel="noopener noreferrer"&gt;Amazon EBS&lt;/a&gt;. The main change is how speed works.&lt;/p&gt;

&lt;p&gt;Disk speed is measured in IOPS: how many reads and writes the disk can do each second. On GCP, a bigger disk is often a faster disk. On AWS, the usual disk type (gp3) &lt;a href="https://docs.aws.amazon.com/ebs/latest/userguide/general-purpose.html" rel="noopener noreferrer"&gt;lets you choose the speed on its own&lt;/a&gt;. Every gp3 disk starts at 3,000 IOPS, and you pay extra to go higher.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GCP disk&lt;/th&gt;
&lt;th&gt;EBS disk&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pd-standard&lt;/td&gt;
&lt;td&gt;st1 (hard drive), or gp3&lt;/td&gt;
&lt;td&gt;Big files read from start to end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pd-balanced&lt;/td&gt;
&lt;td&gt;gp3&lt;/td&gt;
&lt;td&gt;Most apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pd-ssd&lt;/td&gt;
&lt;td&gt;gp3 with more IOPS&lt;/td&gt;
&lt;td&gt;Busy databases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pd-extreme, Hyperdisk Extreme&lt;/td&gt;
&lt;td&gt;io2&lt;/td&gt;
&lt;td&gt;Very busy databases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two GCP disks have no exact match:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Regional Persistent Disk&lt;/strong&gt; keeps a copy of the disk in two zones (two separate data centers). An EBS disk lives in one zone. On AWS, you get the same safety another way: a database copy in a second zone, or regular snapshots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local SSD&lt;/strong&gt; becomes EC2 instance store. The warning is the same on both clouds: the data is gone when the machine stops. Use it only for data you can rebuild.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disks usually move together with their machines. &lt;a href="https://docs.aws.amazon.com/mgn/latest/ug/what-is-application-migration-service.html" rel="noopener noreferrer"&gt;AWS Application Migration Service&lt;/a&gt; copies a running machine and its disks to AWS, and keeps them in sync until you switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared Folders: Filestore to EFS or FSx
&lt;/h2&gt;

&lt;p&gt;Filestore gives many machines one shared folder. Machines reach it over the network using NFS, a common way to share folders on Linux. AWS has a few choices. Pick by what your apps need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.aws.amazon.com/efs/latest/ug/whatisefs.html" rel="noopener noreferrer"&gt;Amazon EFS&lt;/a&gt;&lt;/strong&gt; is the simple match for most Filestore use. It uses NFS. It grows and shrinks on its own, so you never pick a size. It keeps your data in several zones. You pay for what you store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/what-is-fsx-ontap.html" rel="noopener noreferrer"&gt;Amazon FSx for NetApp ONTAP&lt;/a&gt;&lt;/strong&gt; fits apps that need both Linux and Windows sharing, or NetApp features like instant copies. It is also the direct match for Google Cloud NetApp Volumes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.aws.amazon.com/fsx/latest/LustreGuide/what-is.html" rel="noopener noreferrer"&gt;Amazon FSx for Lustre&lt;/a&gt;&lt;/strong&gt; is for very heavy work, like training AI models. It can read files straight from S3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.aws.amazon.com/fsx/latest/WindowsGuide/what-is.html" rel="noopener noreferrer"&gt;Amazon FSx for Windows File Server&lt;/a&gt;&lt;/strong&gt; is for Windows apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One tip: EFS is fast when many machines read at once, but each small file takes a little time to open. Apps that open thousands of tiny files one by one can feel slower. Test with your real app before you decide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving the Data
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm6mf52cl5run72jke8d5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm6mf52cl5run72jke8d5.png" alt="How the data moves: Cloud Storage buckets are copied to Amazon S3 by AWS DataSync without an agent; Filestore shares are copied to EFS or FSx by a DataSync agent; disks move with their virtual machines through AWS Application Migration Service; a final sync runs before the switch" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For buckets, the best tool is &lt;a href="https://docs.aws.amazon.com/datasync/latest/userguide/tutorial_transfer-google-cloud-storage.html" rel="noopener noreferrer"&gt;AWS DataSync&lt;/a&gt;. It reads Cloud Storage directly. You give it a key from GCP, and it copies files into S3. For bucket to bucket copies, you don't need to install anything on GCP.&lt;/p&gt;

&lt;p&gt;DataSync also checks every file after it copies it. On later runs, it only copies what changed. And it writes a report, so you can prove what was moved.&lt;/p&gt;

&lt;p&gt;For Filestore, DataSync needs a small helper machine (an "agent") running on GCP, close to the data. For disks, move the whole machine with Application Migration Service.&lt;/p&gt;

&lt;p&gt;A safe copy follows the same steps every time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;First full copy&lt;/strong&gt; while the old system keeps running. For big buckets, this can take days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catch-up copies&lt;/strong&gt; that only move what changed. Each one is shorter than the last.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Final copy.&lt;/strong&gt; Stop writes for a short time, run one last copy, then point your apps at AWS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check everything.&lt;/strong&gt; Compare the number of files, their sizes and their checksums. A checksum is like a fingerprint of a file. If the fingerprints match, the file arrived unchanged.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;"These are simple mechanisms that protect you from a large class of risks."&lt;/p&gt;

&lt;p&gt;— &lt;strong&gt;Andy Warfield&lt;/strong&gt;, VP and Distinguished Engineer at Amazon S3, in &lt;a href="https://www.allthingsdistributed.com/2023/07/building-and-operating-a-pretty-big-storage-system.html" rel="noopener noreferrer"&gt;&lt;em&gt;Building and operating a pretty big storage system called S3&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Checks are simple too, and they are what make a move safe. Don't delete anything on GCP until they pass.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ask about Google's exit offer.&lt;/strong&gt; Data leaving GCP normally costs money for every GB. Google offers &lt;a href="https://cloud.google.com/exit-cloud" rel="noopener noreferrer"&gt;free data transfer for customers who leave Google Cloud&lt;/a&gt;. You must apply first, and there are rules. Talk to your Google account team before the big copy starts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A Simple Way to Decide
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftdohwjua75t2y79qb90j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftdohwjua75t2y79qb90j.png" alt="Storage decision flow: if it is files in buckets, use Amazon S3 and pick the class by how often data is read; if it is a disk for one machine, use EBS gp3 or io2; if many machines share a folder, use Amazon EFS, or FSx for NetApp ONTAP, Lustre or Windows; if it is backups, use AWS Backup" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is it files in a bucket?&lt;/strong&gt; Use S3. Pick the class by how often the files are read. If you don't know, use Intelligent-Tiering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it a disk for one machine?&lt;/strong&gt; Use EBS gp3. Use io2 only for the busiest databases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do many machines share one folder?&lt;/strong&gt; Use EFS. Use FSx if you need Windows sharing, NetApp features or very heavy work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it a backup?&lt;/strong&gt; Use &lt;a href="https://docs.aws.amazon.com/aws-backup/latest/devguide/whatisbackup.html" rel="noopener noreferrer"&gt;AWS Backup&lt;/a&gt;. Keep a copy in a separate AWS account, so one mistake can't delete both.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Changes on the Bill
&lt;/h2&gt;

&lt;p&gt;Storage prices look simple: a price for each GB, each month. The surprises come from everything around that:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;GCP&lt;/th&gt;
&lt;th&gt;AWS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Keeping files&lt;/td&gt;
&lt;td&gt;Per GB per month, by class&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://aws.amazon.com/s3/pricing/" rel="noopener noreferrer"&gt;S3&lt;/a&gt;: per GB per month, by class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading and writing&lt;/td&gt;
&lt;td&gt;A fee per 1,000 actions&lt;/td&gt;
&lt;td&gt;A fee per 1,000 requests; higher for cold classes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading cold files&lt;/td&gt;
&lt;td&gt;A fee per GB&lt;/td&gt;
&lt;td&gt;A fee per GB, and a wait for Glacier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deleting too early&lt;/td&gt;
&lt;td&gt;You pay the minimum time&lt;/td&gt;
&lt;td&gt;You pay the minimum time, and for at least 128 KB per file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disks&lt;/td&gt;
&lt;td&gt;Per GB; speed comes with size&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://aws.amazon.com/ebs/pricing/" rel="noopener noreferrer"&gt;EBS&lt;/a&gt;: per GB, plus extra speed if you want it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared folders&lt;/td&gt;
&lt;td&gt;Filestore: for the size you set aside&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://aws.amazon.com/efs/pricing/" rel="noopener noreferrer"&gt;EFS&lt;/a&gt;: for what you actually store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data leaving the cloud&lt;/td&gt;
&lt;td&gt;Per GB&lt;/td&gt;
&lt;td&gt;Per GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things catch teams out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lots of tiny files.&lt;/strong&gt; Request fees and the 128 KB minimum add up fast. Bundle small files together, or keep them in S3 Standard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copying lifecycle rules as they are.&lt;/strong&gt; A rule that moves files to a cold class is cheap on GCP. On AWS, moving millions of small files into Glacier costs a fee for each one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NAT gateways.&lt;/strong&gt; Machines in a private network often reach S3 through a NAT gateway, which charges for every GB. Add a free &lt;a href="https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html" rel="noopener noreferrer"&gt;S3 gateway endpoint&lt;/a&gt; instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Turn on &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage_lens.html" rel="noopener noreferrer"&gt;S3 Storage Lens&lt;/a&gt; early. It shows which buckets are growing, which files nobody reads, and where a lifecycle rule would save money.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Looks Like in Code
&lt;/h2&gt;

&lt;p&gt;Uploading a file and making a one-hour download link, on GCP and on AWS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# GCP: Cloud Storage (Python)
from datetime import timedelta
from google.cloud import storage

bucket = storage.Client().bucket("shop-invoices")
blob = bucket.blob("2026/10/inv-1042.pdf")
blob.upload_from_filename("inv-1042.pdf")
url = blob.generate_signed_url(expiration=timedelta(hours=1))

# AWS: Amazon S3 (Python)
import boto3

s3 = boto3.client("s3")
s3.upload_file("inv-1042.pdf", "shop-invoices", "2026/10/inv-1042.pdf")
url = s3.generate_presigned_url(
    "get_object",
    Params={"Bucket": "shop-invoices", "Key": "2026/10/inv-1042.pdf"},
    ExpiresIn=3600,
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a bucket that keeps old versions and moves files to a cold class after 90 days, in Terraform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# GCP: a Cloud Storage bucket
resource "google_storage_bucket" "invoices" {
  name                        = "shop-invoices"
  location                    = "US-CENTRAL1"
  uniform_bucket_level_access = true
  versioning { enabled = true }
  lifecycle_rule {
    condition { age = 90 }
    action {
      type          = "SetStorageClass"
      storage_class = "COLDLINE"
    }
  }
}

# AWS: the same bucket in S3 (each setting is its own resource)
resource "aws_s3_bucket" "invoices" {
  bucket = "shop-invoices"   # names are global; it may be taken
}

resource "aws_s3_bucket_versioning" "invoices" {
  bucket = aws_s3_bucket.invoices.id
  versioning_configuration { status = "Enabled" }
}

resource "aws_s3_bucket_lifecycle_configuration" "invoices" {
  bucket = aws_s3_bucket.invoices.id
  rule {
    id     = "cold-after-90-days"
    status = "Enabled"
    filter {}
    transition {
      days          = 90
      storage_class = "GLACIER_IR"   # was COLDLINE
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These snippets show the shape, not every setting. Check the current docs before you copy them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things That Bite Later
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"Failures are a given and everything will eventually fail over time."&lt;/p&gt;

&lt;p&gt;— &lt;strong&gt;Werner Vogels&lt;/strong&gt;, CTO of Amazon, in &lt;a href="https://www.allthingsdistributed.com/2016/03/10-lessons-from-10-years-of-aws.html" rel="noopener noreferrer"&gt;&lt;em&gt;10 Lessons from 10 Years of AWS&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These are the small things that tend to break after the move:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Old file paths.&lt;/strong&gt; Paths like &lt;code&gt;gs://bucket/file&lt;/code&gt; and links to &lt;code&gt;storage.googleapis.com&lt;/code&gt; hide in code, settings, scripts and database rows. Search for all of them, including links you have already sent to customers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bucket names.&lt;/strong&gt; Bucket names must be unique across all of AWS. The name you used on GCP may already be taken. Choose names early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File labels.&lt;/strong&gt; Labels like &lt;code&gt;Content-Type&lt;/code&gt; tell a browser what a file is. If they are not copied, a browser may download a picture instead of showing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold files that need a restore.&lt;/strong&gt; Code that reads from Archive on GCP will fail on Glacier Deep Archive until the file is restored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sudden heavy traffic.&lt;/strong&gt; S3 handles &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/optimizing-performance.html" rel="noopener noreferrer"&gt;thousands of requests per second&lt;/a&gt; and grows over time. But a brand-new bucket hit hard on day one can slow down at first. Ramp up traffic step by step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File change alerts.&lt;/strong&gt; If apps react when a file is added, those alerts move to &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html" rel="noopener noreferrer"&gt;S3 Event Notifications&lt;/a&gt;. The alert looks different, so the code that reads it needs a small change.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Doing the Move, Step by Step
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;List everything.&lt;/strong&gt; Every bucket, disk, shared folder and backup, with its size, number of files, class, cost, owner and the apps that use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the AWS service&lt;/strong&gt; and storage class for each one, using the questions above. Write down why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up the base:&lt;/strong&gt; AWS accounts, networks, S3 gateway endpoints, encryption keys in &lt;a href="https://docs.aws.amazon.com/kms/latest/developerguide/overview.html" rel="noopener noreferrer"&gt;AWS KMS&lt;/a&gt;, access rules and backup plans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply for Google's exit offer&lt;/strong&gt; before the big copy starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with one bucket.&lt;/strong&gt; Copy it with DataSync, check it, and point one app at it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move the rest in waves.&lt;/strong&gt; For each wave: full copy, catch-up copies, a short freeze, a final copy, then checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean up.&lt;/strong&gt; Keep the GCP data read-only for a while, then delete it. Remove old keys and accounts. Review the AWS bill after the first full month.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Storage is the heaviest part of a cloud move, but it is also the most predictable. The services match closely. The work is in the details: cold classes, access rules, tiny files and a careful final copy. Get those right, and your data arrives whole.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I transfer data from GCS to S3?
&lt;/h3&gt;

&lt;p&gt;Use AWS DataSync. It reads Google Cloud Storage directly with a GCP key, so bucket-to-bucket copies need nothing installed on GCP. It checks every file, copies only what changed on later runs, and writes a report. Run a full copy, catch-up copies, then a short freeze and a final copy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Google Cloud Storage S3 compatible?
&lt;/h3&gt;

&lt;p&gt;Partly. Cloud Storage has an S3-style XML API, which is why DataSync, rclone and many S3 tools can read GCS buckets directly. Your apps still need small changes once they move to real S3, such as signed URLs becoming pre-signed URLs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use rclone instead of DataSync for GCS to S3?
&lt;/h3&gt;

&lt;p&gt;For small buckets, yes. For large moves, DataSync is safer: it checks every file, retries, runs incremental syncs and gives you a report you can show auditors.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the GCP Filestore equivalent in AWS?
&lt;/h3&gt;

&lt;p&gt;Amazon EFS for most NFS shares. Use FSx for NetApp ONTAP for mixed Linux and Windows sharing or NetApp features, FSx for Lustre for heavy AI and HPC work, and FSx for Windows File Server for Windows apps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Persistent Disk vs EBS: what is different?
&lt;/h3&gt;

&lt;p&gt;On GCP a bigger disk is a faster disk. On EBS gp3 you set speed on its own: every disk starts at 3,000 IOPS and you pay for more. pd-balanced maps to gp3, pd-ssd to gp3 with more IOPS, and pd-extreme to io2. Regional Persistent Disk has no direct match.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coldline vs Glacier: which S3 class replaces it?
&lt;/h3&gt;

&lt;p&gt;Coldline and Archive map to S3 Glacier Instant Retrieval if apps still read the files. Use Glacier Deep Archive only for files nobody needs fast, because it needs a restore that can take hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I avoid GCP egress fees when moving to AWS?
&lt;/h3&gt;

&lt;p&gt;Apply for Google's free data transfer for customers leaving Google Cloud before the big copy starts. Talk to your Google account team, since there are rules to follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  About MigrateX
&lt;/h2&gt;

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

&lt;p&gt;MigrateX is our platform and service for moving software, data and infrastructure from one technology to another. It combines automation with experienced engineers. The automation does the repetitive work quickly, and the engineers make the tricky decisions.&lt;/p&gt;

&lt;p&gt;Here's how we run a GCP to AWS storage move with MigrateX:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assessment.&lt;/strong&gt; We list every bucket, disk, shared folder and backup, with its size, file count, class, cost and owner. You see where the work is before anyone agrees on a timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan.&lt;/strong&gt; We work with your team to pick the AWS service and storage class for each one, set up access and encryption, and plan the copy waves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration, step by step.&lt;/strong&gt; Automation does the repetitive parts: setting up DataSync copies, turning lifecycle rules and bucket settings into their AWS versions, and updating &lt;code&gt;gs://&lt;/code&gt; paths and Terraform. Our engineers handle the parts that need thought: cold data, very large buckets, shared folders and the final switch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof that it works.&lt;/strong&gt; Every wave ends with checks on file counts, sizes and checksums. Apps are tested against the new storage before the switch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handover and clean-up.&lt;/strong&gt; We retire GCP storage one wave at a time, tune storage classes once real usage is known, and leave your team with a setup they understand and own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What you get along the way:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No lost data.&lt;/strong&gt; Every file is checked after it is copied. Nothing is deleted until the checks pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short freezes.&lt;/strong&gt; Catch-up copies keep the final switch down to minutes or hours, not days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A smaller bill, not a bigger one.&lt;/strong&gt; The right storage classes, lifecycle rules and endpoints are part of the move, not an afterthought.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One team from start to end.&lt;/strong&gt; The same people plan the move, copy the data and hand it over.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Planning a move from GCP to AWS? We'd love to hear about it. Tell us a little about your setup through &lt;a href="https://cobuildx.ai/contact" rel="noopener noreferrer"&gt;Contact Us&lt;/a&gt;, and we'll start with a free chat about where the work is likely to be.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;h3&gt;
  
  
  AWS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AWS. &lt;em&gt;What is Amazon S3?&lt;/em&gt; &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Understanding and managing Amazon S3 storage classes&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-class-intro.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-class-intro.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;S3 Intelligent-Tiering&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/intelligent-tiering.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/intelligent-tiering.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Restoring an archived object&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/restoring-objects.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/restoring-objects.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Managing the lifecycle of objects&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Replicating objects&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/replication.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/replication.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;S3 Object Lock&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Using server-side encryption with AWS KMS keys&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Amazon S3 Event Notifications&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Blocking public access to your Amazon S3 storage&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Sharing objects with presigned URLs&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Optimizing Amazon S3 performance&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/optimizing-performance.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/optimizing-performance.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Amazon S3 Storage Lens&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage_lens.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage_lens.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Gateway endpoints for Amazon S3&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;What is Amazon EBS?&lt;/em&gt; &lt;a href="https://docs.aws.amazon.com/ebs/latest/userguide/what-is-ebs.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/ebs/latest/userguide/what-is-ebs.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;General Purpose SSD volumes&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/ebs/latest/userguide/general-purpose.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/ebs/latest/userguide/general-purpose.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;What is Amazon EFS?&lt;/em&gt; &lt;a href="https://docs.aws.amazon.com/efs/latest/ug/whatisefs.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/efs/latest/ug/whatisefs.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;What is Amazon FSx for NetApp ONTAP?&lt;/em&gt; &lt;a href="https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/what-is-fsx-ontap.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/fsx/latest/ONTAPGuide/what-is-fsx-ontap.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;What is Amazon FSx for Lustre?&lt;/em&gt; &lt;a href="https://docs.aws.amazon.com/fsx/latest/LustreGuide/what-is.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/fsx/latest/LustreGuide/what-is.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;What is Amazon FSx for Windows File Server?&lt;/em&gt; &lt;a href="https://docs.aws.amazon.com/fsx/latest/WindowsGuide/what-is.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/fsx/latest/WindowsGuide/what-is.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;What is AWS Backup?&lt;/em&gt; &lt;a href="https://docs.aws.amazon.com/aws-backup/latest/devguide/whatisbackup.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/aws-backup/latest/devguide/whatisbackup.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;AWS Key Management Service&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/kms/latest/developerguide/overview.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/kms/latest/developerguide/overview.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;What is AWS Application Migration Service?&lt;/em&gt; &lt;a href="https://docs.aws.amazon.com/mgn/latest/ug/what-is-application-migration-service.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/mgn/latest/ug/what-is-application-migration-service.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Configuring AWS DataSync transfers with Google Cloud Storage&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/datasync/latest/userguide/tutorial_transfer-google-cloud-storage.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/datasync/latest/userguide/tutorial_transfer-google-cloud-storage.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;AWS Snowball Edge availability change&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/snowball/latest/developer-guide/snowball-edge-availability-change.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/snowball/latest/developer-guide/snowball-edge-availability-change.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Expert sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Andy Warfield. &lt;em&gt;Building and operating a pretty big storage system called S3&lt;/em&gt;. All Things Distributed, 2023. &lt;a href="https://www.allthingsdistributed.com/2023/07/building-and-operating-a-pretty-big-storage-system.html" rel="noopener noreferrer"&gt;https://www.allthingsdistributed.com/2023/07/building-and-operating-a-pretty-big-storage-system.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Werner Vogels. &lt;em&gt;10 Lessons from 10 Years of Amazon Web Services&lt;/em&gt;. All Things Distributed, 2016. &lt;a href="https://www.allthingsdistributed.com/2016/03/10-lessons-from-10-years-of-aws.html" rel="noopener noreferrer"&gt;https://www.allthingsdistributed.com/2016/03/10-lessons-from-10-years-of-aws.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Google Cloud
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Google Cloud. &lt;em&gt;Storage classes&lt;/em&gt;. &lt;a href="https://cloud.google.com/storage/docs/storage-classes" rel="noopener noreferrer"&gt;https://cloud.google.com/storage/docs/storage-classes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Cloud. &lt;em&gt;Cloud Storage interoperability&lt;/em&gt;. &lt;a href="https://cloud.google.com/storage/docs/interoperability" rel="noopener noreferrer"&gt;https://cloud.google.com/storage/docs/interoperability&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Cloud. &lt;em&gt;Exit Google Cloud&lt;/em&gt;. &lt;a href="https://cloud.google.com/exit-cloud" rel="noopener noreferrer"&gt;https://cloud.google.com/exit-cloud&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;CobuildX helps teams migrate legacy applications and cloud infrastructure with &lt;a href="https://cobuildx.ai/migratex" rel="noopener noreferrer"&gt;MigrateX&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/gcp-to-aws-storage" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>googlecloud</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>Moving Serverless from GCP to AWS: Cloud Run, Functions and Events</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Wed, 28 Oct 2026 10:28:58 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/moving-serverless-from-gcp-to-aws-cloud-run-functions-and-events-27nk</link>
      <guid>https://dev.to/cobuildx-ai/moving-serverless-from-gcp-to-aws-cloud-run-functions-and-events-27nk</guid>
      <description>&lt;p&gt;&lt;em&gt;A plain-English guide to moving serverless apps from Google Cloud to AWS: Cloud Run, Cloud Run functions, App Engine, Eventarc, Cloud Tasks, Scheduler and Workflows. What replaces what, what behaves differently, what changes on the bill, and a step-by-step plan.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"Serverless" means you give the cloud your code, and it runs it for you. You don't pick servers. You don't patch them. You don't keep them running. The cloud starts your code when it is needed and stops it when it is not.&lt;/p&gt;

&lt;p&gt;On Google Cloud (GCP), serverless usually means Cloud Run, Cloud Run functions and App Engine. It also means the services that connect them: Eventarc, Pub/Sub, Cloud Tasks, Cloud Scheduler and Workflows.&lt;/p&gt;

&lt;p&gt;Teams move these apps to AWS for simple reasons. The rest of the company is on AWS. The data has already moved. Or the apps need to sit close to other AWS services.&lt;/p&gt;

&lt;p&gt;Serverless looks like the easiest thing to move. There are no servers to copy. But each cloud has different default settings, and that is where the work is. Cloud Run sends many requests to one copy of your app. Lambda sends one request at a time. Cloud Run lets a request run for an hour. Lambda stops after 15 minutes. Small differences like these can break an app that worked fine on GCP.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Serverless is, instead, a mindset: make technology decisions that further your ability to focus on what differentiates you as a business."&lt;/p&gt;

&lt;p&gt;— &lt;strong&gt;Ben Kehoe&lt;/strong&gt;, AWS Serverless Hero, in his talk &lt;a href="https://www.meetup.com/serverless-boston/events/262376552/" rel="noopener noreferrer"&gt;&lt;em&gt;Serverless as a mindset&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a good test for the move too. The goal is not to copy every GCP service one to one. The goal is to end up with less to look after. This guide covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which AWS service replaces each GCP serverless service&lt;/li&gt;
&lt;li&gt;What works differently, and what tends to break&lt;/li&gt;
&lt;li&gt;How events, queues and schedules move&lt;/li&gt;
&lt;li&gt;What changes on the bill&lt;/li&gt;
&lt;li&gt;A simple step-by-step plan&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Quick Map
&lt;/h2&gt;

&lt;p&gt;Here is each GCP serverless service and its closest match on AWS. The rest of the post explains each row.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1qmit1uk576an8v90kdq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1qmit1uk576an8v90kdq.png" alt="Serverless service map from Google Cloud to AWS: Cloud Run services to ECS Express Mode on Fargate or Lambda, Cloud Run jobs to ECS tasks or AWS Batch, Cloud Run functions to AWS Lambda, App Engine to Elastic Beanstalk or ECS, Eventarc and Pub/Sub push to EventBridge and SNS plus SQS, and Workflows to Step Functions" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GCP&lt;/th&gt;
&lt;th&gt;Closest AWS match&lt;/th&gt;
&lt;th&gt;Watch out for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Run services&lt;/td&gt;
&lt;td&gt;Amazon ECS Express Mode, or AWS Lambda&lt;/td&gt;
&lt;td&gt;Cloud Run shares one copy of the app between many requests; Lambda does not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Run jobs&lt;/td&gt;
&lt;td&gt;ECS tasks on Fargate, or AWS Batch&lt;/td&gt;
&lt;td&gt;Task numbering and retries are set up differently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Run functions&lt;/td&gt;
&lt;td&gt;AWS Lambda&lt;/td&gt;
&lt;td&gt;15-minute limit and a new code entry point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App Engine standard&lt;/td&gt;
&lt;td&gt;Lambda, or Elastic Beanstalk&lt;/td&gt;
&lt;td&gt;Old built-in services must be replaced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App Engine flexible&lt;/td&gt;
&lt;td&gt;Elastic Beanstalk, or ECS on Fargate&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;app.yaml&lt;/code&gt; settings move to new config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API Gateway / Cloud Endpoints&lt;/td&gt;
&lt;td&gt;Amazon API Gateway&lt;/td&gt;
&lt;td&gt;Different login options and limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eventarc&lt;/td&gt;
&lt;td&gt;Amazon EventBridge&lt;/td&gt;
&lt;td&gt;Events look different, so code that reads them changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pub/Sub push&lt;/td&gt;
&lt;td&gt;SNS and SQS, or EventBridge&lt;/td&gt;
&lt;td&gt;Retries are set on the queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Tasks&lt;/td&gt;
&lt;td&gt;Amazon SQS with Lambda&lt;/td&gt;
&lt;td&gt;SQS holds tasks; something must read them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Scheduler&lt;/td&gt;
&lt;td&gt;EventBridge Scheduler&lt;/td&gt;
&lt;td&gt;Each schedule names its own time zone and target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workflows&lt;/td&gt;
&lt;td&gt;AWS Step Functions&lt;/td&gt;
&lt;td&gt;Workflows are rewritten, not converted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret Manager&lt;/td&gt;
&lt;td&gt;AWS Secrets Manager, or Parameter Store&lt;/td&gt;
&lt;td&gt;Access comes from the app's IAM role&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What about App Runner?&lt;/strong&gt; It looks like the obvious match for Cloud Run. But AWS &lt;a href="https://docs.aws.amazon.com/apprunner/latest/dg/apprunner-availability-change.html" rel="noopener noreferrer"&gt;closed App Runner to new customers on April 30, 2026&lt;/a&gt;. For new apps, AWS points to ECS Express Mode instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Cloud Run: Containers or Functions
&lt;/h2&gt;

&lt;p&gt;Cloud Run runs your app in a container (a package with your code and everything it needs). It adds copies when traffic grows and removes them when traffic stops. On AWS you have two good choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ECS Express Mode&lt;/strong&gt; is the closest match. You give it your container and two IAM roles. AWS &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/express-service-overview.html" rel="noopener noreferrer"&gt;sets up the rest&lt;/a&gt;: the service on Fargate, a load balancer, scaling and networking. Your container does not change. Like Cloud Run, one copy of the app can serve many requests at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Lambda&lt;/strong&gt; runs your code only when a request comes in. You pay per request. It is great for small APIs and for traffic that comes in bursts. But a normal Lambda copy handles &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html" rel="noopener noreferrer"&gt;one request at a time&lt;/a&gt;. Lambda can also run &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/images-create.html" rel="noopener noreferrer"&gt;container images&lt;/a&gt;, but your code must use Lambda's request format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhy8noou533hcdrl75xws.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhy8noou533hcdrl75xws.png" alt="How requests reach your code: on Cloud Run, up to 80 or more requests share one container instance; on standard AWS Lambda, each request gets its own execution environment, so 80 requests need 80 environments" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are the main differences side by side. The Lambda numbers come from the &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html" rel="noopener noreferrer"&gt;Lambda quotas page&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Cloud Run&lt;/th&gt;
&lt;th&gt;ECS Express Mode&lt;/th&gt;
&lt;th&gt;Lambda&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requests per copy&lt;/td&gt;
&lt;td&gt;Up to 1,000 (80 is common)&lt;/td&gt;
&lt;td&gt;As many as your app can take&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Longest request&lt;/td&gt;
&lt;td&gt;60 minutes&lt;/td&gt;
&lt;td&gt;Set by the load balancer&lt;/td&gt;
&lt;td&gt;15 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale to zero&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No, at least one copy runs&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stay warm&lt;/td&gt;
&lt;td&gt;Minimum instances&lt;/td&gt;
&lt;td&gt;Minimum number of tasks&lt;/td&gt;
&lt;td&gt;Provisioned concurrency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Largest request body&lt;/td&gt;
&lt;td&gt;32 MiB&lt;/td&gt;
&lt;td&gt;Load balancer limits&lt;/td&gt;
&lt;td&gt;6 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What you deploy&lt;/td&gt;
&lt;td&gt;Container&lt;/td&gt;
&lt;td&gt;Container&lt;/td&gt;
&lt;td&gt;Zip file or container&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things follow from this. First, database connections. On Cloud Run, 100 requests might share two copies of the app and a few connections. On Lambda, 100 requests means 100 copies, and each opens its own connections. The database can run out. Use &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html" rel="noopener noreferrer"&gt;RDS Proxy&lt;/a&gt; to share connections, or keep the app on ECS. Second, anything that runs for more than 15 minutes cannot go on Lambda as it is.&lt;/p&gt;

&lt;p&gt;Then there are cold starts. A cold start is the short wait while Lambda starts a new copy of your code. Many teams worry about it too much:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Cold starts that don't add to user-facing latency is generally not worth worrying about."&lt;/p&gt;

&lt;p&gt;— &lt;strong&gt;Yan Cui&lt;/strong&gt;, AWS Serverless Hero, in &lt;a href="https://www.slideshare.net/theburningmonk/running-serverless-at-scale" rel="noopener noreferrer"&gt;&lt;em&gt;Running Serverless at Scale&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If users do feel it, turn on &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html" rel="noopener noreferrer"&gt;provisioned concurrency&lt;/a&gt;, which keeps copies ready, or &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html" rel="noopener noreferrer"&gt;SnapStart&lt;/a&gt; for Java, Python and .NET.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; if the Cloud Run service is a full web app (a framework, a database, long requests), move the same container to ECS Express Mode. If it is a small API or webhook with spiky traffic, move it to Lambda.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is also a middle option. &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html" rel="noopener noreferrer"&gt;Lambda Managed Instances&lt;/a&gt; run Lambda on EC2 machines that AWS looks after, and one copy can handle many requests at once. It suits steady, busy traffic. Your code must be safe to run many requests in parallel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud Run jobs
&lt;/h3&gt;

&lt;p&gt;Cloud Run jobs run a container until the work is done, often as many tasks at once. On AWS, run the same container as an &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html" rel="noopener noreferrer"&gt;ECS task on Fargate&lt;/a&gt;. Start it from EventBridge Scheduler or Step Functions. For big jobs split into many parts, use &lt;a href="https://docs.aws.amazon.com/batch/latest/userguide/what-is-batch.html" rel="noopener noreferrer"&gt;AWS Batch&lt;/a&gt; array jobs. One small change: Cloud Run tells each task its number in &lt;code&gt;CLOUD_RUN_TASK_INDEX&lt;/code&gt;. AWS Batch uses &lt;code&gt;AWS_BATCH_JOB_ARRAY_INDEX&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Functions: Cloud Run Functions to Lambda
&lt;/h2&gt;

&lt;p&gt;Cloud Run functions (once called Cloud Functions) map most directly to &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" rel="noopener noreferrer"&gt;AWS Lambda&lt;/a&gt;. The idea is the same: a small piece of code that runs when a request or an event arrives. What changes is the code around it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Cloud Run functions&lt;/th&gt;
&lt;th&gt;Lambda&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Entry point&lt;/td&gt;
&lt;td&gt;Functions Framework (&lt;code&gt;@functions_framework.http&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;handler(event, context)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web address&lt;/td&gt;
&lt;td&gt;Built in&lt;/td&gt;
&lt;td&gt;Function URL or API Gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Started by events&lt;/td&gt;
&lt;td&gt;Eventarc or Pub/Sub&lt;/td&gt;
&lt;td&gt;SQS, SNS, EventBridge and others&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory and CPU&lt;/td&gt;
&lt;td&gt;Set one by one&lt;/td&gt;
&lt;td&gt;Set memory; CPU grows with it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Longest run&lt;/td&gt;
&lt;td&gt;60 minutes for HTTP&lt;/td&gt;
&lt;td&gt;15 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local files&lt;/td&gt;
&lt;td&gt;Kept in memory&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/tmp&lt;/code&gt;, up to 10 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most of the work is in the entry point and the trigger, not in the business logic. Keep the logic in its own file. Then write a thin entry point for each cloud. That way you can test both with the same code.&lt;/p&gt;

&lt;h2&gt;
  
  
  App Engine
&lt;/h2&gt;

&lt;p&gt;App Engine is GCP's oldest serverless service. How you move it depends on which kind you use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App Engine flexible&lt;/strong&gt; already runs containers. Move it to ECS on Fargate. Or use &lt;a href="https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/Welcome.html" rel="noopener noreferrer"&gt;Elastic Beanstalk&lt;/a&gt; if the team likes the "push code, get an app" style.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App Engine standard&lt;/strong&gt; apps on newer runtimes are normal web apps. Put them in a container and treat them like Cloud Run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Older App Engine apps&lt;/strong&gt; use built-in services like Memcache, Task Queues, Users and Datastore. Each needs a new home on AWS: ElastiCache, SQS, Amazon Cognito and DynamoDB. This is real code work, so find these apps early.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;App Engine also has small config files that are easy to forget. &lt;code&gt;cron.yaml&lt;/code&gt; becomes EventBridge Scheduler schedules. &lt;code&gt;dispatch.yaml&lt;/code&gt; becomes load balancer rules. &lt;code&gt;queue.yaml&lt;/code&gt; becomes SQS queues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Events, Queues and Schedules
&lt;/h2&gt;

&lt;p&gt;Serverless apps talk to each other through events: "an order was placed", "a file was uploaded". This is where small differences cause the most bugs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Eventarc to EventBridge.&lt;/strong&gt; Eventarc sends events in the CloudEvents format. &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html" rel="noopener noreferrer"&gt;EventBridge&lt;/a&gt; uses its own format, with fields like &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;detail-type&lt;/code&gt; and &lt;code&gt;detail&lt;/code&gt;. Every function that reads an event needs a small change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pub/Sub push to SNS and SQS.&lt;/strong&gt; Pub/Sub push calls your web address and tries again if it fails. On AWS, put an &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html" rel="noopener noreferrer"&gt;SQS queue&lt;/a&gt; in front of each reader. Lambda then &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html" rel="noopener noreferrer"&gt;reads the queue&lt;/a&gt; in batches. Messages that keep failing go to a dead-letter queue (a side queue for messages that could not be handled).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Tasks to SQS.&lt;/strong&gt; Cloud Tasks calls a web address for each task, at a rate you set. SQS only holds the tasks. Something has to read them. Use Lambda with a &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-scaling.html" rel="noopener noreferrer"&gt;maximum concurrency&lt;/a&gt; setting to control the rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Scheduler to EventBridge Scheduler.&lt;/strong&gt; &lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html" rel="noopener noreferrer"&gt;EventBridge Scheduler&lt;/a&gt; supports cron, time zones and one-time schedules. It can start Lambda, ECS tasks and Step Functions directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflows to Step Functions.&lt;/strong&gt; GCP Workflows are steps written in YAML. &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html" rel="noopener noreferrer"&gt;Step Functions&lt;/a&gt; are steps written in JSON. The ideas are the same (steps, retries, choices, waits), but each workflow is rewritten by hand.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Make every handler safe to run twice.&lt;/strong&gt; On both clouds, a message can arrive more than once. On AWS, if one message in a batch fails, the whole batch is retried unless you &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-errorhandling.html" rel="noopener noreferrer"&gt;report which ones failed&lt;/a&gt;. If a handler sends an email or charges a card, save an ID so it never does it twice.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  APIs, Access and Identity
&lt;/h2&gt;

&lt;p&gt;Who can call what works on the same idea in both clouds. Only the names change:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GCP&lt;/th&gt;
&lt;th&gt;AWS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service account on a service&lt;/td&gt;
&lt;td&gt;IAM role for the Lambda function or ECS task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;roles/run.invoker&lt;/code&gt; on a service&lt;/td&gt;
&lt;td&gt;Lambda resource policy, or IAM login on a function URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Services calling each other with tokens&lt;/td&gt;
&lt;td&gt;IAM-signed calls, or a private API in API Gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Allow unauthenticated"&lt;/td&gt;
&lt;td&gt;A public function URL or public API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity-Aware Proxy&lt;/td&gt;
&lt;td&gt;Load balancer login with Amazon Cognito&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API keys in API Gateway&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html" rel="noopener noreferrer"&gt;Usage plans and API keys&lt;/a&gt; in Amazon API Gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Give each function or service its own IAM role, with only the access it needs. Don't make one big role for everything, even if the old apps shared one service account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Networking You'll Trip Over
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reaching private databases.&lt;/strong&gt; Cloud Run reaches your private network through a connector or Direct VPC egress. Lambda can &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html" rel="noopener noreferrer"&gt;join your VPC&lt;/a&gt; (your private network on AWS). ECS tasks run inside it already.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reaching the internet from the VPC.&lt;/strong&gt; A Lambda function inside a VPC cannot reach the internet on its own. It needs a NAT gateway, which costs money per hour and per GB. For AWS services like S3 and DynamoDB, use VPC endpoints instead. They are cheaper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom domains.&lt;/strong&gt; A Cloud Run domain mapping becomes a certificate in ACM, plus a load balancer, CloudFront or an API Gateway custom domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calls back to GCP.&lt;/strong&gt; During the move, some apps still call GCP. That is slower and costs money for data leaving GCP. Remove these calls first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A Simple Way to Decide
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7qsaql6wqfe45wd7iqi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7qsaql6wqfe45wd7iqi.png" alt="Serverless decision flow: if it runs longer than 15 minutes, use ECS on Fargate or AWS Batch; if it is a web app with a connection pool or steady traffic, use ECS Express Mode; if it is a small API, webhook or event handler, use Lambda; if it is a scheduled job, use EventBridge Scheduler with Lambda or an ECS task; if it chains several steps, use Step Functions" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does it run for more than 15 minutes?&lt;/strong&gt; Use ECS on Fargate or AWS Batch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it a full web app&lt;/strong&gt; with a framework, a database or steady traffic? Use ECS Express Mode with the same container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it a small API, webhook or event handler?&lt;/strong&gt; Use Lambda.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it run on a schedule?&lt;/strong&gt; Use EventBridge Scheduler to start a Lambda function or an ECS task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it run several steps in a row, with retries and waits?&lt;/strong&gt; Use Step Functions.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Changes on the Bill
&lt;/h2&gt;

&lt;p&gt;Both clouds charge for the time your code runs and for each request. The details change the total:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;GCP&lt;/th&gt;
&lt;th&gt;AWS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pay per request&lt;/td&gt;
&lt;td&gt;Cloud Run: CPU time, memory time, requests&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://aws.amazon.com/lambda/pricing/" rel="noopener noreferrer"&gt;Lambda&lt;/a&gt;: memory time and requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Always-on containers&lt;/td&gt;
&lt;td&gt;Cloud Run instance billing&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://aws.amazon.com/fargate/pricing/" rel="noopener noreferrer"&gt;Fargate&lt;/a&gt;: CPU and memory per second, plus the load balancer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staying warm&lt;/td&gt;
&lt;td&gt;Minimum instances (lower idle price)&lt;/td&gt;
&lt;td&gt;Provisioned concurrency (paid while it is on)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APIs&lt;/td&gt;
&lt;td&gt;API Gateway per call&lt;/td&gt;
&lt;td&gt;API Gateway per call; HTTP APIs cost less than REST APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Events and queues&lt;/td&gt;
&lt;td&gt;Eventarc, Pub/Sub, Cloud Tasks&lt;/td&gt;
&lt;td&gt;EventBridge, SNS, SQS, all per request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discounts&lt;/td&gt;
&lt;td&gt;Committed use discounts&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://aws.amazon.com/savingsplans/compute-pricing/" rel="noopener noreferrer"&gt;Compute Savings Plans&lt;/a&gt; (cover Lambda and Fargate)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things catch teams out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The load balancer.&lt;/strong&gt; ECS Express Mode puts a load balancer in front of your app. It costs money even with no traffic. Several apps can share one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NAT gateways.&lt;/strong&gt; Lambda functions in a VPC that call the internet go through NAT. A lot of outgoing traffic becomes a surprise on the bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Waiting time.&lt;/strong&gt; On Lambda you pay while your code waits for other services. An app that spends most of its time waiting may be cheaper on Fargate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the new setup for a few weeks before you buy a Savings Plan. Real usage is the only good basis for a commitment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Looks Like in Code
&lt;/h2&gt;

&lt;p&gt;A simple HTTP function on GCP and the same function on Lambda:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# GCP: Cloud Run function (Python)
import functions_framework

@functions_framework.http
def hello(request):
    name = request.args.get("name", "world")
    return {"message": f"Hello, {name}"}

# AWS: Lambda behind a function URL or HTTP API (Python)
import json

def handler(event, context):
    params = event.get("queryStringParameters") or {}
    name = params.get("name", "world")
    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({"message": f"Hello, {name}"}),
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A function that reads Pub/Sub messages, and the same function reading from SQS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# GCP: triggered by a Pub/Sub topic through Eventarc
import base64, json
import functions_framework

@functions_framework.cloud_event
def on_order(cloud_event):
    data = base64.b64decode(cloud_event.data["message"]["data"])
    process(json.loads(data))

# AWS: triggered by an SQS queue, with partial batch failures
import json

def handler(event, context):
    failed = []
    for record in event["Records"]:
        try:
            process(json.loads(record["body"]))
        except Exception:
            failed.append({"itemIdentifier": record["messageId"]})
    return {"batchItemFailures": failed}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a Cloud Run service in Terraform, next to the same Lambda function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# GCP: a Cloud Run service
resource "google_cloud_run_v2_service" "api" {
  name     = "api"
  location = "us-central1"
  template {
    containers {
      image = "us-docker.pkg.dev/shop-prod/apps/api:1.4.2"
      resources { limits = { cpu = "1", memory = "512Mi" } }
    }
    scaling { min_instance_count = 1 }
    max_instance_request_concurrency = 80
    service_account = google_service_account.api.email
  }
}

# AWS: the same API as a Lambda function from a container image
resource "aws_lambda_function" "api" {
  function_name = "api"
  package_type  = "Image"
  image_uri     = "123456789012.dkr.ecr.us-east-1.amazonaws.com/api:1.4.2"
  memory_size   = 1024                    # CPU grows with memory
  timeout       = 30
  role          = aws_iam_role.api.arn    # was the service account
}

resource "aws_lambda_function_url" "api" {
  function_name      = aws_lambda_function.api.function_name
  authorization_type = "AWS_IAM"         # was roles/run.invoker
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These snippets show the shape, not every setting. Check the current docs before you copy them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things That Bite Later
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"Failures are a given and everything will eventually fail over time."&lt;/p&gt;

&lt;p&gt;— &lt;strong&gt;Werner Vogels&lt;/strong&gt;, CTO of Amazon, in &lt;a href="https://www.allthingsdistributed.com/2016/03/10-lessons-from-10-years-of-aws.html" rel="noopener noreferrer"&gt;&lt;em&gt;10 Lessons from 10 Years of AWS&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These are the small things that tend to break after the move:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timeouts in a chain.&lt;/strong&gt; API Gateway REST APIs stop waiting after &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-execution-service-limits-table.html" rel="noopener noreferrer"&gt;29 seconds by default&lt;/a&gt;. A Lambda behind it can run longer, but the caller has already given up. Check every timeout from the user to the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large uploads.&lt;/strong&gt; Lambda takes up to 6 MB per normal request. Big file uploads that worked on Cloud Run should go straight to S3 with a &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html" rel="noopener noreferrer"&gt;pre-signed URL&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Things kept in memory.&lt;/strong&gt; Code that keeps a cache or a counter in memory acts differently when every request has its own copy of the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs and alerts.&lt;/strong&gt; Cloud Logging searches and alerts become &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AnalyzingLogData.html" rel="noopener noreferrer"&gt;CloudWatch Logs Insights&lt;/a&gt; queries and CloudWatch alarms. Rebuild the alerts people rely on before you move traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets.&lt;/strong&gt; Secret Manager paths like &lt;code&gt;projects/.../secrets/...&lt;/code&gt; become &lt;a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html" rel="noopener noreferrer"&gt;Secrets Manager&lt;/a&gt; or Parameter Store names. Search your code for all of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limits.&lt;/strong&gt; New AWS accounts start with a low Lambda concurrency limit. Ask for more weeks before you move real traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Doing the Move, Step by Step
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;List everything.&lt;/strong&gt; Every Cloud Run service and job, function, App Engine app, trigger, queue, schedule and workflow, with its traffic, run time, cost and owner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the AWS service&lt;/strong&gt; for each one, using the questions above. Write down why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the base:&lt;/strong&gt; AWS accounts, networks, a container registry (ECR), IAM roles, logs, alarms and higher limits. The &lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/serverless-applications-lens/welcome.html" rel="noopener noreferrer"&gt;Serverless Applications Lens&lt;/a&gt; is a good checklist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with one small app.&lt;/strong&gt; Move it end to end: code, triggers, secrets, alarms and domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run both clouds side by side.&lt;/strong&gt; Send a small share of users to AWS using &lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-weighted.html" rel="noopener noreferrer"&gt;weighted DNS in Route 53&lt;/a&gt;. Compare errors and speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move the events.&lt;/strong&gt; Point schedules and queues at AWS one at a time. Empty the GCP queues before you turn them off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean up.&lt;/strong&gt; Turn off the GCP services, delete old service accounts and keys, and buy Savings Plans once usage is steady.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;"Serverless is still the future because the mindset shift is the thing we haven't gotten to."&lt;/p&gt;

&lt;p&gt;— &lt;strong&gt;Ben Kehoe&lt;/strong&gt;, AWS Serverless Hero, speaking to &lt;a href="https://sdtimes.com/cloud/sd-times-blog-getting-a-serverless-reality-check/" rel="noopener noreferrer"&gt;&lt;em&gt;SD Times&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Serverless is often the fastest part of a cloud move, because there are no servers to copy. The work is in the default settings: how many requests one copy takes, how long a request may run, and how events are retried. Get those right, and the rest is mostly config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the Cloud Run equivalent in AWS?
&lt;/h3&gt;

&lt;p&gt;ECS Express Mode is the closest: same container, one copy serving many requests, with AWS setting up Fargate, the load balancer and scaling. AWS Lambda fits small APIs and bursty traffic. App Runner is another option for simple web apps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud Run vs Lambda: which should I pick?
&lt;/h3&gt;

&lt;p&gt;Pick Lambda for small APIs, webhooks and event handlers that can scale to zero. Pick ECS for full web apps, steady traffic, requests longer than 15 minutes, or bodies over 6 MB. Watch database connections: on Lambda every request is its own copy, so use RDS Proxy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud Run vs Fargate: what changes?
&lt;/h3&gt;

&lt;p&gt;ECS on Fargate runs the same container, but it does not scale to zero, so at least one task runs all the time and a load balancer sits in front of it. In return you get long requests and many requests per copy, like Cloud Run.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I migrate Cloud Functions to Lambda?
&lt;/h3&gt;

&lt;p&gt;The business logic usually stays. What changes is the entry point (Functions Framework becomes &lt;code&gt;handler(event, context)&lt;/code&gt;), the trigger (Eventarc or Pub/Sub becomes SQS, SNS or EventBridge), and the 15-minute run limit. Keep the logic in its own file and write a thin entry point for each cloud.&lt;/p&gt;

&lt;h3&gt;
  
  
  Eventarc vs EventBridge: what is different?
&lt;/h3&gt;

&lt;p&gt;Eventarc sends CloudEvents. EventBridge uses its own shape with &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;detail-type&lt;/code&gt; and &lt;code&gt;detail&lt;/code&gt;. Every function that reads an event needs a small change.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the Cloud Tasks equivalent in AWS?
&lt;/h3&gt;

&lt;p&gt;Amazon SQS with Lambda. SQS holds the tasks but does not call anything, so Lambda reads the queue, and the maximum concurrency setting controls the rate. Failed tasks go to a dead-letter queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the App Engine equivalent in AWS?
&lt;/h3&gt;

&lt;p&gt;App Engine flexible moves to ECS on Fargate or Elastic Beanstalk. Standard apps on newer runtimes go in a container. Older apps that use Memcache, Task Queues, Users or Datastore need ElastiCache, SQS, Cognito and DynamoDB. &lt;code&gt;cron.yaml&lt;/code&gt; becomes EventBridge Scheduler.&lt;/p&gt;

&lt;h2&gt;
  
  
  About MigrateX
&lt;/h2&gt;

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

&lt;p&gt;MigrateX is our platform and service for moving software, data and infrastructure from one technology to another. It combines automation with experienced engineers. The automation does the repetitive work quickly, and the engineers make the tricky decisions.&lt;/p&gt;

&lt;p&gt;Here's how we run a GCP to AWS serverless move with MigrateX:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assessment.&lt;/strong&gt; We list every Cloud Run service, function, App Engine app, trigger, queue, schedule and workflow, with its traffic, run time, cost and owner. You see where the work is before anyone agrees on a timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan.&lt;/strong&gt; We work with your team to pick the AWS target for each service, design the IAM roles and networking, and decide the order of the move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration, step by step.&lt;/strong&gt; Automation does the repetitive parts: rewriting handlers and event formats, turning Cloud Run and App Engine config into ECS, Lambda and EventBridge, and converting Terraform. Our engineers handle the parts that need thought: long-running work, concurrency, App Engine bundled services and Workflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof that it works.&lt;/strong&gt; Both clouds run side by side. We shift traffic a little at a time and compare errors, response times and costs before each step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handover and clean-up.&lt;/strong&gt; We turn off GCP services one at a time, set up Savings Plans once usage is steady, and leave your team with a setup they understand and own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What you get along the way:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No big-bang switch.&lt;/strong&gt; Traffic moves a little at a time, with a quick way back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lost events.&lt;/strong&gt; Queues and schedules move one at a time and are drained before anything is turned off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A smaller bill, not a bigger one.&lt;/strong&gt; Right-sized functions, shared load balancers and Savings Plans are part of the move, not an afterthought.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One partner for the whole move.&lt;/strong&gt; The same team and approach covers your serverless apps, your compute and your data.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Planning a move from GCP to AWS? We'd love to hear about it. Tell us a little about your setup through &lt;a href="https://cobuildx.ai/contact" rel="noopener noreferrer"&gt;Contact Us&lt;/a&gt;, and we'll start with a free chat about where the work is likely to be.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;h3&gt;
  
  
  AWS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AWS. &lt;em&gt;AWS App Runner availability change&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/apprunner/latest/dg/apprunner-availability-change.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/apprunner/latest/dg/apprunner-availability-change.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Amazon ECS Express Mode&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/express-service-overview.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonECS/latest/developerguide/express-service-overview.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;AWS Fargate for Amazon ECS&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Understanding Lambda function scaling&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Lambda Managed Instances&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Create a Lambda function using a container image&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/images-create.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/images-create.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Lambda SnapStart&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Using Lambda with Amazon SQS&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Handling errors for an SQS event source in Lambda&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-errorhandling.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-errorhandling.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Giving Lambda functions access to resources in a VPC&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Amazon EventBridge&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Amazon EventBridge Scheduler&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;AWS Step Functions&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;AWS Batch&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/batch/latest/userguide/what-is-batch.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/batch/latest/userguide/what-is-batch.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Amazon RDS Proxy&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Lambda quotas&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Provisioned concurrency for Lambda&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Quotas for REST APIs in API Gateway&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-execution-service-limits-table.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-execution-service-limits-table.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Serverless Applications Lens, AWS Well-Architected&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/serverless-applications-lens/welcome.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/wellarchitected/latest/serverless-applications-lens/welcome.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Weighted routing in Route 53&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-weighted.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy-weighted.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;AWS Elastic Beanstalk&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/Welcome.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/Welcome.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Expert sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Ben Kehoe. &lt;em&gt;Serverless as a mindset&lt;/em&gt;. Serverless Boston meetup talk. &lt;a href="https://www.meetup.com/serverless-boston/events/262376552/" rel="noopener noreferrer"&gt;https://www.meetup.com/serverless-boston/events/262376552/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ben Kehoe, quoted in &lt;em&gt;Getting a serverless reality check&lt;/em&gt;. SD Times. &lt;a href="https://sdtimes.com/cloud/sd-times-blog-getting-a-serverless-reality-check/" rel="noopener noreferrer"&gt;https://sdtimes.com/cloud/sd-times-blog-getting-a-serverless-reality-check/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Yan Cui. &lt;em&gt;Running Serverless at Scale&lt;/em&gt;. &lt;a href="https://www.slideshare.net/theburningmonk/running-serverless-at-scale" rel="noopener noreferrer"&gt;https://www.slideshare.net/theburningmonk/running-serverless-at-scale&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Werner Vogels. &lt;em&gt;10 Lessons from 10 Years of Amazon Web Services&lt;/em&gt;. All Things Distributed, 2016. &lt;a href="https://www.allthingsdistributed.com/2016/03/10-lessons-from-10-years-of-aws.html" rel="noopener noreferrer"&gt;https://www.allthingsdistributed.com/2016/03/10-lessons-from-10-years-of-aws.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Google Cloud
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Google Cloud. &lt;em&gt;Cloud Run quotas and limits&lt;/em&gt;. &lt;a href="https://cloud.google.com/run/quotas" rel="noopener noreferrer"&gt;https://cloud.google.com/run/quotas&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Cloud. &lt;em&gt;Maximum concurrent requests for services&lt;/em&gt;. &lt;a href="https://cloud.google.com/run/docs/about-concurrency" rel="noopener noreferrer"&gt;https://cloud.google.com/run/docs/about-concurrency&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Cloud. &lt;em&gt;Cloud Run functions overview&lt;/em&gt;. &lt;a href="https://cloud.google.com/functions/docs/concepts/overview" rel="noopener noreferrer"&gt;https://cloud.google.com/functions/docs/concepts/overview&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;CobuildX helps teams migrate legacy applications and cloud infrastructure with &lt;a href="https://cobuildx.ai/migratex" rel="noopener noreferrer"&gt;MigrateX&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/gcp-to-aws-serverless" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>googlecloud</category>
      <category>serverless</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Moving Big Data from GCP to AWS: BigQuery, Dataproc and Dataflow</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Wed, 28 Oct 2026 10:28:22 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/moving-big-data-from-gcp-to-aws-bigquery-dataproc-and-dataflow-36nd</link>
      <guid>https://dev.to/cobuildx-ai/moving-big-data-from-gcp-to-aws-bigquery-dataproc-and-dataflow-36nd</guid>
      <description>&lt;p&gt;&lt;em&gt;A plain-English guide to moving big data from Google Cloud to AWS: BigQuery, Dataproc, Dataflow, Pub/Sub, Composer and the data catalog. What replaces what, what behaves differently, what changes on the bill, and a step-by-step plan.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"Big data" is the part of the cloud that stores and works on large amounts of data. It includes the data warehouse, the Spark jobs, the streaming pipelines, the message queues and the tools that run them on a schedule. For many teams on Google Cloud (GCP), this is the most valuable part of their setup. It is also the hardest part to move.&lt;/p&gt;

&lt;p&gt;Teams move it to AWS for simple reasons. The rest of the company already runs on AWS. A company they bought came with an AWS data lake. Or their apps have already moved, and sending every event back to GCP costs too much.&lt;/p&gt;

&lt;p&gt;On paper the move looks easy: BigQuery goes to Redshift, Dataproc goes to EMR, Pub/Sub goes to Kinesis. In real life, small things change. The SQL is a little different. Prices work differently. Messages arrive in a different order. Each of these can quietly break a report that people rely on.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Data integration is making all the data an organization has available in all its services and systems."&lt;/p&gt;

&lt;p&gt;— &lt;strong&gt;Jay Kreps&lt;/strong&gt;, co-creator of Apache Kafka, in &lt;a href="https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying" rel="noopener noreferrer"&gt;&lt;em&gt;The Log&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the goal of a big data move too. The data has to keep reaching every team and every report while it moves. This guide covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which AWS service replaces each GCP big data service&lt;/li&gt;
&lt;li&gt;What works differently, and what tends to break&lt;/li&gt;
&lt;li&gt;How to move the data itself and prove nothing was lost&lt;/li&gt;
&lt;li&gt;What changes on the bill&lt;/li&gt;
&lt;li&gt;A simple step-by-step plan&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Quick Map
&lt;/h2&gt;

&lt;p&gt;Here is each GCP big data service and its closest match on AWS. The rest of the post explains each row.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy19cl9dea1ywfsrfnsaw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy19cl9dea1ywfsrfnsaw.png" alt="Big data service map from Google Cloud to AWS: BigQuery to Amazon Redshift or Athena on S3, Dataproc to Amazon EMR, Dataflow to Managed Service for Apache Flink or AWS Glue, Pub/Sub to Kinesis Data Streams or Amazon MSK, Cloud Composer to Amazon MWAA, and Dataplex to the Glue Data Catalog and Lake Formation" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GCP&lt;/th&gt;
&lt;th&gt;Closest AWS match&lt;/th&gt;
&lt;th&gt;Watch out for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery&lt;/td&gt;
&lt;td&gt;Amazon Redshift (Serverless or provisioned)&lt;/td&gt;
&lt;td&gt;The SQL and the pricing are different&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery over files in a lake&lt;/td&gt;
&lt;td&gt;Amazon Athena on S3, with Apache Iceberg tables&lt;/td&gt;
&lt;td&gt;You pay per TB read, so file layout matters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dataproc&lt;/td&gt;
&lt;td&gt;Amazon EMR (on EC2, Serverless or EKS)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gs://&lt;/code&gt; paths and start-up scripts change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dataproc Serverless&lt;/td&gt;
&lt;td&gt;EMR Serverless&lt;/td&gt;
&lt;td&gt;Different job settings and limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dataflow (Apache Beam)&lt;/td&gt;
&lt;td&gt;Managed Service for Apache Flink, AWS Glue or EMR&lt;/td&gt;
&lt;td&gt;No exact match; some jobs need a rewrite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pub/Sub&lt;/td&gt;
&lt;td&gt;Kinesis Data Streams, Amazon MSK, or SNS + SQS&lt;/td&gt;
&lt;td&gt;Order, how long messages are kept, and push delivery differ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Composer&lt;/td&gt;
&lt;td&gt;Amazon MWAA&lt;/td&gt;
&lt;td&gt;Same Airflow, different operators and connections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Data Fusion&lt;/td&gt;
&lt;td&gt;AWS Glue (visual ETL)&lt;/td&gt;
&lt;td&gt;Pipelines are rebuilt, not converted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Datastream&lt;/td&gt;
&lt;td&gt;AWS DMS&lt;/td&gt;
&lt;td&gt;Change capture is set up differently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dataplex / Data Catalog&lt;/td&gt;
&lt;td&gt;Glue Data Catalog, Lake Formation, SageMaker Catalog&lt;/td&gt;
&lt;td&gt;Access rules are rebuilt by hand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery ML&lt;/td&gt;
&lt;td&gt;Redshift ML or SageMaker AI&lt;/td&gt;
&lt;td&gt;Models are trained again, not copied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Looker Studio&lt;/td&gt;
&lt;td&gt;Amazon QuickSight&lt;/td&gt;
&lt;td&gt;Dashboards are rebuilt by hand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bigtable&lt;/td&gt;
&lt;td&gt;Amazon DynamoDB or Keyspaces&lt;/td&gt;
&lt;td&gt;Different data model; plan it as its own project&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Warehouse: BigQuery to Redshift or Athena
&lt;/h2&gt;

&lt;p&gt;BigQuery is Google's data warehouse: a place to keep large tables and run SQL on them. It is usually the centre of a GCP data platform, so it's the biggest decision. AWS has two good answers, and many teams use both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Redshift&lt;/strong&gt; is AWS's data warehouse. Use it for busy dashboards, many people running queries at the same time, and fast joins. &lt;a href="https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-serverless.html" rel="noopener noreferrer"&gt;Redshift Serverless&lt;/a&gt; feels closest to BigQuery: there are no servers to size, it grows and shrinks on its own, and you pay for the time it spends working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Athena&lt;/strong&gt; runs SQL directly on files stored in Amazon S3. Like BigQuery on-demand, you &lt;a href="https://aws.amazon.com/athena/pricing/" rel="noopener noreferrer"&gt;pay for the data each query reads&lt;/a&gt;. Use it for a data lake and for queries that run now and then. Store the tables in the &lt;a href="https://docs.aws.amazon.com/athena/latest/ug/querying-iceberg.html" rel="noopener noreferrer"&gt;Apache Iceberg&lt;/a&gt; format so you can update and delete rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common setup: keep one copy of the data in S3 as Iceberg tables. Engineers query it with Athena and Spark. Redshift serves the dashboards that need to be fast, and it can read the same Iceberg tables, so you don't need two copies.&lt;/p&gt;

&lt;h3&gt;
  
  
  What changes in the SQL
&lt;/h3&gt;

&lt;p&gt;Most simple queries move with small edits. The trouble is in features that only BigQuery has:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;BigQuery&lt;/th&gt;
&lt;th&gt;Redshift&lt;/th&gt;
&lt;th&gt;Athena&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;STRUCT&lt;/code&gt; and &lt;code&gt;ARRAY&lt;/code&gt; columns, &lt;code&gt;UNNEST&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SUPER&lt;/code&gt; column type&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ROW&lt;/code&gt; and &lt;code&gt;ARRAY&lt;/code&gt;; &lt;code&gt;UNNEST&lt;/code&gt; works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partitioned and clustered tables&lt;/td&gt;
&lt;td&gt;Sort keys and distribution keys&lt;/td&gt;
&lt;td&gt;Partitions and Iceberg sorting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;SAFE_CAST&lt;/code&gt;, &lt;code&gt;IFNULL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CAST&lt;/code&gt; with checks, &lt;code&gt;NVL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TRY_CAST&lt;/code&gt;, &lt;code&gt;COALESCE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;project.dataset.table&lt;/code&gt; names&lt;/td&gt;
&lt;td&gt;&lt;code&gt;database.schema.table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;catalog.database.table&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scripts and stored procedures&lt;/td&gt;
&lt;td&gt;Stored procedures&lt;/td&gt;
&lt;td&gt;Not supported; move logic to Airflow or Glue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduled queries&lt;/td&gt;
&lt;td&gt;Redshift query scheduler or Airflow&lt;/td&gt;
&lt;td&gt;EventBridge or Airflow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Don't rewrite thousands of queries by hand. The &lt;a href="https://docs.aws.amazon.com/SchemaConversionTool/latest/userguide/CHAP_Source.BigQuery.html" rel="noopener noreferrer"&gt;AWS Schema Conversion Tool&lt;/a&gt; can convert BigQuery tables and much of the SQL for you. AWS also has a &lt;a href="https://docs.aws.amazon.com/dms/latest/sbs/bigquery-redshift.html" rel="noopener noreferrer"&gt;step-by-step BigQuery to Redshift guide&lt;/a&gt;. Treat the converted SQL as a first draft, and test every query behind an important report.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; find the 50 queries that matter most (top dashboards, finance reports, ML features). Test those first, and compare their results row by row with BigQuery. Fix the rest as people use them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Spark and Hadoop: Dataproc to EMR
&lt;/h2&gt;

&lt;p&gt;Dataproc is Google's service for running Spark and Hadoop clusters. On AWS the same service is &lt;strong&gt;Amazon EMR&lt;/strong&gt;. Both run the same open-source tools, so most jobs work on EMR with small changes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;On Dataproc&lt;/th&gt;
&lt;th&gt;On EMR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gs://bucket/path&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3://bucket/path&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initialization actions (start-up scripts)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-plan-bootstrap.html" rel="noopener noreferrer"&gt;Bootstrap actions&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster properties&lt;/td&gt;
&lt;td&gt;EMR configurations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autoscaling policies&lt;/td&gt;
&lt;td&gt;EMR managed scaling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spot secondary workers&lt;/td&gt;
&lt;td&gt;Spot task nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dataproc Metastore&lt;/td&gt;
&lt;td&gt;Glue Data Catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dataproc Serverless&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/emr/latest/EMR-Serverless-UserGuide/emr-serverless.html" rel="noopener noreferrer"&gt;EMR Serverless&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pick the kind of EMR by how your jobs run. Clusters that run all day stay on &lt;strong&gt;EMR on EC2&lt;/strong&gt;. Jobs that start, finish and stop are usually cheaper and simpler on &lt;strong&gt;EMR Serverless&lt;/strong&gt;. If your team already runs everything on Kubernetes, &lt;strong&gt;EMR on EKS&lt;/strong&gt; runs Spark on your existing cluster.&lt;/p&gt;

&lt;p&gt;Match versions first. Use the same Spark and Python versions on EMR as on Dataproc. Otherwise small changes in behaviour will look like migration bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pipelines: Dataflow Is the Hard One
&lt;/h2&gt;

&lt;p&gt;Dataflow is Google's service for running data pipelines written with Apache Beam. It handles the servers for you. AWS has no service that runs Beam in exactly the same way, so most of the engineering work goes here. There are three paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep the Beam code and run it on Flink.&lt;/strong&gt; Beam pipelines can run inside &lt;a href="https://docs.aws.amazon.com/managed-flink/latest/java/how-creating-apps-beam.html" rel="noopener noreferrer"&gt;Amazon Managed Service for Apache Flink&lt;/a&gt;. The code mostly stays the same, but your team has to learn some Flink tuning. This works well for streaming pipelines with a lot of logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite batch pipelines in Spark.&lt;/strong&gt; Pipelines that run on a schedule often become &lt;strong&gt;AWS Glue&lt;/strong&gt; jobs or Spark jobs on EMR Serverless. It's a rewrite, but Spark is widely known.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write new streaming jobs for Flink directly.&lt;/strong&gt; For simple streaming jobs, skip Beam and write them in Flink SQL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ready-made Dataflow templates (like "Pub/Sub to BigQuery") often need no code on AWS. &lt;a href="https://docs.aws.amazon.com/firehose/latest/dev/what-is-this-service.html" rel="noopener noreferrer"&gt;Amazon Data Firehose&lt;/a&gt; can load a stream straight into S3, Iceberg tables or Redshift.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Count your Dataflow jobs early.&lt;/strong&gt; They decide your timeline more than anything else. Sort them into three groups: "template, no code", "batch, rewrite in Spark" and "streaming, keep Beam or move to Flink".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Messaging: Pub/Sub to Kinesis, MSK or SQS
&lt;/h2&gt;

&lt;p&gt;Pub/Sub is Google's message service: one system sends messages, and others receive them. Pub/Sub does several jobs at once. AWS splits those jobs across different services, so pick by how each topic is used:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;How you use Pub/Sub&lt;/th&gt;
&lt;th&gt;Best AWS match&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;A stream of events for analytics&lt;/td&gt;
&lt;td&gt;Kinesis Data Streams&lt;/td&gt;
&lt;td&gt;Keeps order, lets you replay, feeds Firehose and Flink&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large streams, or teams that know Kafka&lt;/td&gt;
&lt;td&gt;Amazon MSK (managed Kafka)&lt;/td&gt;
&lt;td&gt;Works with Kafka tools; keeps data a long time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One message to many services&lt;/td&gt;
&lt;td&gt;SNS with SQS queues&lt;/td&gt;
&lt;td&gt;Simple fan-out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A work queue with retries&lt;/td&gt;
&lt;td&gt;Amazon SQS&lt;/td&gt;
&lt;td&gt;Simple, cheap, with a queue for failed messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pushing messages to a web address&lt;/td&gt;
&lt;td&gt;EventBridge or SNS&lt;/td&gt;
&lt;td&gt;Kinesis and SQS don't push&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three differences break the code that reads messages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Order.&lt;/strong&gt; Pub/Sub can keep messages in order by key. Kinesis keeps order inside each shard. SQS only keeps order in FIFO queues, which are slower.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How long messages are kept.&lt;/strong&gt; Pub/Sub keeps them for up to 31 days. Kinesis keeps them for 24 hours by default and &lt;a href="https://docs.aws.amazon.com/streams/latest/dev/kinesis-extended-retention.html" rel="noopener noreferrer"&gt;up to 365 days&lt;/a&gt;. SQS keeps them for up to 14 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicates.&lt;/strong&gt; On AWS, plan for the same message to arrive twice. Make sure handling a message twice does no harm.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Scheduling: Composer to MWAA
&lt;/h2&gt;

&lt;p&gt;Cloud Composer runs Apache Airflow, the tool that starts jobs in the right order on a schedule. On AWS the same thing is &lt;a href="https://docs.aws.amazon.com/mwaa/latest/userguide/what-is-mwaa.html" rel="noopener noreferrer"&gt;Amazon Managed Workflows for Apache Airflow (MWAA)&lt;/a&gt;. Your Airflow workflows (DAGs) move almost as they are. The work is in what they call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operators.&lt;/strong&gt; Steps like &lt;code&gt;BigQueryInsertJobOperator&lt;/code&gt; and &lt;code&gt;DataprocSubmitJobOperator&lt;/code&gt; become the Redshift, Athena, EMR and Glue operators from the Amazon provider package.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connections.&lt;/strong&gt; The Google service account becomes an IAM role for MWAA. Passwords and keys go into AWS Secrets Manager.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versions.&lt;/strong&gt; Use the same Airflow version on MWAA as on Composer. Upgrade later. Doing both at once makes it hard to tell what broke.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;During the move, one Airflow can run jobs on both clouds. That lets you move jobs one at a time instead of all at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving the Data Itself
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zrzchso0cku9ywru96j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zrzchso0cku9ywru96j.png" alt="Moving data from GCP to AWS: export BigQuery tables to Parquet in Cloud Storage, copy to S3 with AWS DataSync, load into Iceberg tables or Redshift, keep new data flowing with change data capture or dual writes, then compare row counts and checksums before switching readers" width="798" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Data outlives code."&lt;/p&gt;

&lt;p&gt;— &lt;strong&gt;Martin Kleppmann&lt;/strong&gt;, author of &lt;a href="https://dataintensive.net/" rel="noopener noreferrer"&gt;&lt;em&gt;Designing Data-Intensive Applications&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your pipelines will be rewritten many times. Your data will stay. So move it with care. Old data and new data move in different ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Export the old data.&lt;/strong&gt; Export BigQuery tables to Parquet files in Cloud Storage. Parquet keeps column types and takes up little space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy it across.&lt;/strong&gt; &lt;a href="https://docs.aws.amazon.com/datasync/latest/userguide/tutorial_transfer-google-cloud-storage.html" rel="noopener noreferrer"&gt;AWS DataSync can read straight from Google Cloud Storage&lt;/a&gt; and copy the files to S3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load it.&lt;/strong&gt; Register the files as Iceberg tables, or load them into Redshift with &lt;code&gt;COPY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep new data flowing.&lt;/strong&gt; While both clouds run, send new data to both. You can write from the apps to both sides, run the pipelines on both, or use AWS DMS to copy database changes as they happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check it.&lt;/strong&gt; Every day, compare row counts and totals for every table on both sides. Switch readers only when they match.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Moving data out of GCP costs money.&lt;/strong&gt; Google charges per GB for data leaving its network. For a large warehouse, ask Google about its free transfer offer for customers who are leaving, and ask AWS about migration credits, before you start copying.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Catalog and Access Rules
&lt;/h2&gt;

&lt;p&gt;On GCP, who can see which data is set with IAM on datasets and tables, plus column and row rules in BigQuery and Dataplex. On AWS, the &lt;strong&gt;Glue Data Catalog&lt;/strong&gt; lists your tables, and &lt;a href="https://docs.aws.amazon.com/lake-formation/latest/dg/what-is-lake-formation.html" rel="noopener noreferrer"&gt;AWS Lake Formation&lt;/a&gt; holds the access rules. Athena, Redshift, EMR and Glue all follow them.&lt;/p&gt;

&lt;p&gt;No tool converts these rules for you. Export your current rules, review them (many are old and too open), and rebuild them in Lake Formation. Use tags, so new tables get the right rules on their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Way to Decide
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvp4b0b2bbzqzynskw17h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvp4b0b2bbzqzynskw17h.png" alt="Big data decision flow: if it is a Dataflow pipeline, sort it into template, Spark rewrite or Flink; if it is Spark or Hadoop, EMR; if it is dashboards with many users, Redshift; if it is ad-hoc or lake queries, Athena on Iceberg; if it is a message stream, Kinesis or MSK; otherwise SQS or SNS" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For each workload, ask these questions in order and stop at the first yes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is it a Dataflow pipeline?&lt;/strong&gt; Sort it first: template, Spark rewrite, or Flink.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it a Spark or Hadoop job?&lt;/strong&gt; EMR. Serverless for short jobs, EC2 for long-running clusters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it a table behind busy dashboards?&lt;/strong&gt; Redshift. Start with Serverless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it lake data, or queries that run now and then?&lt;/strong&gt; Iceberg on S3, queried with Athena.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it a stream of events for analytics?&lt;/strong&gt; Kinesis Data Streams, or MSK if your team knows Kafka.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it a queue between services?&lt;/strong&gt; SQS, with SNS to send to many.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Changes on the Bill
&lt;/h2&gt;

&lt;p&gt;Big data costs depend more on how you are charged than on list prices.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;GCP&lt;/th&gt;
&lt;th&gt;AWS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pay per query&lt;/td&gt;
&lt;td&gt;BigQuery on-demand, per TiB read&lt;/td&gt;
&lt;td&gt;Athena, per TB read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pay for capacity&lt;/td&gt;
&lt;td&gt;BigQuery editions (slots per hour)&lt;/td&gt;
&lt;td&gt;Redshift Serverless (RPU-hours) or fixed nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commit for a discount&lt;/td&gt;
&lt;td&gt;Slot commitments&lt;/td&gt;
&lt;td&gt;Redshift reservations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Cheaper after 90 days without changes&lt;/td&gt;
&lt;td&gt;S3 storage classes; Redshift managed storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spark&lt;/td&gt;
&lt;td&gt;Dataproc fee on top of VM price&lt;/td&gt;
&lt;td&gt;EMR fee on top of EC2 price; Spot for extra workers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;Pub/Sub, per TiB of messages&lt;/td&gt;
&lt;td&gt;Kinesis per shard-hour or per GB; MSK per broker-hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Moving data in or out&lt;/td&gt;
&lt;td&gt;Charged per GB leaving GCP&lt;/td&gt;
&lt;td&gt;Data coming into AWS is free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things catch teams out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Athena charges for data read, just like BigQuery.&lt;/strong&gt; Badly organised tables cost money in the same way. Split tables by date and merge small files before people start querying them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redshift Serverless charges for time, not data.&lt;/strong&gt; One dashboard that refreshes every minute can keep it running all day. Set usage limits from day one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't buy reservations in the first weeks. Pay as you go until usage is steady, then commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Looks Like in Code
&lt;/h2&gt;

&lt;p&gt;A typical BigQuery query and the same query in Redshift:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- BigQuery
SELECT user_id, item.sku, SAFE_CAST(item.price AS NUMERIC) AS price
FROM `shop-prod.sales.orders`, UNNEST(items) AS item
WHERE DATE(created_at) = CURRENT_DATE();

-- Redshift (items stored as a SUPER column)
SELECT o.user_id, item.sku, item.price::DECIMAL(18,2) AS price
FROM sales.orders AS o, o.items AS item
WHERE TRUNC(o.created_at) = CURRENT_DATE;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Dataproc cluster in Terraform, and the same cluster on EMR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# GCP: a Spark cluster
resource "google_dataproc_cluster" "etl" {
  name   = "etl"
  region = "us-central1"
  cluster_config {
    master_config {
      num_instances = 1
      machine_type  = "n2-standard-8"
    }
    worker_config {
      num_instances = 2
      machine_type  = "n2-standard-8"
    }
    initialization_action { script = "gs://etl-scripts/setup.sh" }
  }
}

# AWS: the same cluster on EMR
resource "aws_emr_cluster" "etl" {
  name          = "etl"
  release_label = "emr-7.x.0"              # match your Spark version
  applications  = ["Spark"]
  service_role  = aws_iam_role.emr_service.arn
  ec2_attributes {
    subnet_id        = var.private_subnet_id
    instance_profile = aws_iam_instance_profile.emr_ec2.arn
  }
  master_instance_group { instance_type = "m7i.2xlarge" }
  core_instance_group {
    instance_type  = "m7i.2xlarge"
    instance_count = 2
  }
  bootstrap_action {
    name = "setup"
    path = "s3://etl-scripts/setup.sh"   # was an initialization action
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These snippets show the shape, not every setting. Check the current docs before you copy them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things That Bite Later
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"Failures are a given and everything will eventually fail over time."&lt;/p&gt;

&lt;p&gt;— &lt;strong&gt;Werner Vogels&lt;/strong&gt;, CTO of Amazon, in &lt;a href="https://www.allthingsdistributed.com/2016/03/10-lessons-from-10-years-of-aws.html" rel="noopener noreferrer"&gt;&lt;em&gt;10 Lessons from 10 Years of AWS&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Plan for the small things that fail after the move, not just the big ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time zones.&lt;/strong&gt; BigQuery &lt;code&gt;TIMESTAMP&lt;/code&gt; is always UTC. Redshift has two kinds of timestamp. Check every date filter in your top reports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small files.&lt;/strong&gt; Streaming into S3 creates thousands of tiny files. That makes Athena slow and expensive. Merge them on a schedule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden &lt;code&gt;gs://&lt;/code&gt; paths.&lt;/strong&gt; They hide in Spark settings, Airflow variables, notebooks and SQL. Search for all of them before the switch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key files.&lt;/strong&gt; Google service account key files used by pipelines become IAM roles. Find every one, then delete the keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BI tools.&lt;/strong&gt; Looker, Tableau and notebooks point at BigQuery. Every connection and saved query needs a new home.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limits.&lt;/strong&gt; New AWS accounts start with low limits for EMR, Kinesis and Glue. Ask for more weeks ahead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Doing the Move, Step by Step
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;List everything.&lt;/strong&gt; Every dataset, table, query, pipeline, topic, Airflow workflow and dashboard, with its size, cost, owner and who reads it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the AWS target&lt;/strong&gt; for each one, using the questions above. Write down why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the base:&lt;/strong&gt; an S3 data lake with Iceberg, the Glue Data Catalog, Lake Formation rules, Redshift, MWAA, a network link to GCP, and higher limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with a small pilot.&lt;/strong&gt; Move one data product end to end: its tables, pipeline, schedule and one dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy the old data, then keep both sides in sync.&lt;/strong&gt; Compare both sides every day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch readers&lt;/strong&gt; one dashboard at a time, then switch the pipelines that write data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean up.&lt;/strong&gt; Turn off GCP jobs, delete old copies once you're sure, and buy reservations only once AWS usage is steady.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;"Scope it to one to two weeks, with a few tables and a few queries only."&lt;/p&gt;

&lt;p&gt;— &lt;strong&gt;Saman Irfan&lt;/strong&gt;, Solutions Architect at AWS, on the pilot for a BigQuery move, in &lt;a href="https://builder.aws.com/content/3Gu1BkgphVPKgFak2mNFNZJ1Eiy/best-practices-for-a-bigquery-to-amazon-redshift-migration-proof-of-concept-poc" rel="noopener noreferrer"&gt;&lt;em&gt;Best practices for a BigQuery to Amazon Redshift migration proof of concept&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Big data is often the hardest part of a cloud move. The services are not that different. The hard part is that so many people depend on the numbers. Move the data carefully, prove the numbers match, and switch readers one at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the BigQuery equivalent in AWS?
&lt;/h3&gt;

&lt;p&gt;There are two. Amazon Redshift (Serverless feels closest) is the data warehouse for busy dashboards and many users. Amazon Athena runs SQL on files in S3 and, like BigQuery on-demand, charges for the data each query reads. Many teams keep Iceberg tables in S3 and use both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is BigQuery similar to Redshift?
&lt;/h3&gt;

&lt;p&gt;The idea is the same: large tables and SQL. The details differ. BigQuery &lt;code&gt;STRUCT&lt;/code&gt; and &lt;code&gt;ARRAY&lt;/code&gt; columns become &lt;code&gt;SUPER&lt;/code&gt;, partitioning and clustering become sort and distribution keys, and functions like &lt;code&gt;SAFE_CAST&lt;/code&gt; and &lt;code&gt;IFNULL&lt;/code&gt; need rewriting. The AWS Schema Conversion Tool converts much of this as a first draft.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I migrate data from BigQuery to Redshift?
&lt;/h3&gt;

&lt;p&gt;Export BigQuery tables to Parquet in Cloud Storage, copy the files to S3 with AWS DataSync, then load them with &lt;code&gt;COPY&lt;/code&gt; or register them as Iceberg tables. Keep new data flowing to both sides, and compare row counts and totals every day before you switch readers.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the Dataflow equivalent in AWS?
&lt;/h3&gt;

&lt;p&gt;There is no exact match. Streaming Beam pipelines can run on Amazon Managed Service for Apache Flink with little code change. Batch pipelines are usually rewritten as AWS Glue or EMR Serverless Spark jobs. Simple templates often become Amazon Data Firehose with no code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dataproc vs EMR: how hard is the move?
&lt;/h3&gt;

&lt;p&gt;Usually easy. Both run the same Spark and Hadoop. Change &lt;code&gt;gs://&lt;/code&gt; paths to &lt;code&gt;s3://&lt;/code&gt;, initialization actions to bootstrap actions, and Dataproc Metastore to the Glue Data Catalog. Keep the same Spark and Python versions so behaviour doesn't shift.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the Pub/Sub equivalent in AWS?
&lt;/h3&gt;

&lt;p&gt;It depends on how you use it: Kinesis Data Streams or Amazon MSK for event streams, SNS with SQS for fan-out, SQS for work queues, and EventBridge or SNS for push delivery. Plan for different ordering, retention and duplicate messages.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I migrate Cloud Composer to MWAA?
&lt;/h3&gt;

&lt;p&gt;Your DAGs move almost as they are. Swap the Google operators for the Amazon provider operators, move the service account to an IAM role and secrets to Secrets Manager, and keep the same Airflow version until the move is done.&lt;/p&gt;

&lt;h2&gt;
  
  
  About MigrateX
&lt;/h2&gt;

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

&lt;p&gt;MigrateX is our platform and service for moving software, data and infrastructure from one technology to another. It combines automation with experienced engineers. The automation does the repetitive work quickly, and the engineers make the tricky decisions.&lt;/p&gt;

&lt;p&gt;Here's how we run a GCP to AWS big data move with MigrateX:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assessment.&lt;/strong&gt; We list every BigQuery dataset, query, Dataproc job, Dataflow pipeline, Pub/Sub topic and Airflow workflow, with its size, cost, owner and who reads the results. You see where the work is before anyone agrees on a timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan.&lt;/strong&gt; We work with your team to pick the AWS target for each workload, design the data lake and access rules, and decide the order of the move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration, step by step.&lt;/strong&gt; Automation does the repetitive parts: converting SQL, rewriting paths and operators, and turning Dataproc and Composer setups into EMR and MWAA. Our engineers handle the parts that need thought: Dataflow rewrites, data modelling, streaming and access rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof that it works.&lt;/strong&gt; Both clouds run side by side. We compare row counts, totals and report results every day, and only switch readers once AWS matches GCP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handover and clean-up.&lt;/strong&gt; We turn off GCP pipelines one at a time, set up reservations once usage is steady, and leave your team with a data platform they understand and own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What you get along the way:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No frozen reports.&lt;/strong&gt; Dashboards keep working while the data moves, one piece at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Numbers you can trust.&lt;/strong&gt; Daily side-by-side checks prove the data matches before anyone switches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A smaller bill, not a bigger one.&lt;/strong&gt; Good table layout, Spot, right-sized serverless and reservations are part of the move, not an afterthought.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One partner for the whole move.&lt;/strong&gt; The same team and approach covers your data, your infrastructure and your apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Planning a move from GCP to AWS? We'd love to hear about it. Tell us a little about your setup through &lt;a href="https://cobuildx.ai/contact" rel="noopener noreferrer"&gt;Contact Us&lt;/a&gt;, and we'll start with a free chat about where the work is likely to be.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;h3&gt;
  
  
  AWS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AWS. &lt;em&gt;Migrating a BigQuery project to Amazon Redshift&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/dms/latest/sbs/bigquery-redshift.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/dms/latest/sbs/bigquery-redshift.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Using BigQuery as a source for AWS SCT&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/SchemaConversionTool/latest/userguide/CHAP_Source.BigQuery.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/SchemaConversionTool/latest/userguide/CHAP_Source.BigQuery.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Amazon Redshift Serverless&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-serverless.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-serverless.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Amazon Athena pricing&lt;/em&gt;. &lt;a href="https://aws.amazon.com/athena/pricing/" rel="noopener noreferrer"&gt;https://aws.amazon.com/athena/pricing/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Query Apache Iceberg tables in Athena&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/athena/latest/ug/querying-iceberg.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/athena/latest/ug/querying-iceberg.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Amazon EMR Serverless&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/emr/latest/EMR-Serverless-UserGuide/emr-serverless.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/emr/latest/EMR-Serverless-UserGuide/emr-serverless.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;EMR bootstrap actions&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-plan-bootstrap.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-plan-bootstrap.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Apache Beam on Managed Service for Apache Flink&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/managed-flink/latest/java/how-creating-apps-beam.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/managed-flink/latest/java/how-creating-apps-beam.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Amazon Data Firehose&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/firehose/latest/dev/what-is-this-service.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/firehose/latest/dev/what-is-this-service.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Kinesis Data Streams data retention&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/streams/latest/dev/kinesis-extended-retention.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/streams/latest/dev/kinesis-extended-retention.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Amazon MWAA&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/mwaa/latest/userguide/what-is-mwaa.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/mwaa/latest/userguide/what-is-mwaa.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Transferring data from Google Cloud Storage with DataSync&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/datasync/latest/userguide/tutorial_transfer-google-cloud-storage.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/datasync/latest/userguide/tutorial_transfer-google-cloud-storage.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;AWS Lake Formation&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/lake-formation/latest/dg/what-is-lake-formation.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/lake-formation/latest/dg/what-is-lake-formation.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Expert sources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Jay Kreps. &lt;em&gt;The Log: What every software engineer should know about real-time data's unifying abstraction&lt;/em&gt;. LinkedIn Engineering, 2013. &lt;a href="https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying" rel="noopener noreferrer"&gt;https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Martin Kleppmann. &lt;em&gt;Designing Data-Intensive Applications&lt;/em&gt;, chapter 4. O'Reilly, 2017. &lt;a href="https://dataintensive.net/" rel="noopener noreferrer"&gt;https://dataintensive.net/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Werner Vogels. &lt;em&gt;10 Lessons from 10 Years of Amazon Web Services&lt;/em&gt;. All Things Distributed, 2016. &lt;a href="https://www.allthingsdistributed.com/2016/03/10-lessons-from-10-years-of-aws.html" rel="noopener noreferrer"&gt;https://www.allthingsdistributed.com/2016/03/10-lessons-from-10-years-of-aws.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Saman Irfan. &lt;em&gt;Best practices for a BigQuery to Amazon Redshift migration proof of concept&lt;/em&gt;. AWS Builder Center. &lt;a href="https://builder.aws.com/content/3Gu1BkgphVPKgFak2mNFNZJ1Eiy/best-practices-for-a-bigquery-to-amazon-redshift-migration-proof-of-concept-poc" rel="noopener noreferrer"&gt;https://builder.aws.com/content/3Gu1BkgphVPKgFak2mNFNZJ1Eiy/best-practices-for-a-bigquery-to-amazon-redshift-migration-proof-of-concept-poc&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Google Cloud
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Google Cloud. &lt;em&gt;BigQuery pricing&lt;/em&gt;. &lt;a href="https://cloud.google.com/bigquery/pricing" rel="noopener noreferrer"&gt;https://cloud.google.com/bigquery/pricing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Cloud. &lt;em&gt;Exporting table data&lt;/em&gt;. &lt;a href="https://cloud.google.com/bigquery/docs/exporting-data" rel="noopener noreferrer"&gt;https://cloud.google.com/bigquery/docs/exporting-data&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Cloud. &lt;em&gt;Pub/Sub replay overview&lt;/em&gt;. &lt;a href="https://cloud.google.com/pubsub/docs/replay-overview" rel="noopener noreferrer"&gt;https://cloud.google.com/pubsub/docs/replay-overview&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;CobuildX helps teams migrate legacy applications and cloud infrastructure with &lt;a href="https://cobuildx.ai/migratex" rel="noopener noreferrer"&gt;MigrateX&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/gcp-to-aws-big-data" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>googlecloud</category>
      <category>dataengineering</category>
      <category>bigdata</category>
    </item>
    <item>
      <title>Moving Compute from GCP to AWS: VMs, Kubernetes and GPUs</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Wed, 28 Oct 2026 10:27:46 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/moving-compute-from-gcp-to-aws-vms-kubernetes-and-gpus-2g6j</link>
      <guid>https://dev.to/cobuildx-ai/moving-compute-from-gcp-to-aws-vms-kubernetes-and-gpus-2g6j</guid>
      <description>&lt;p&gt;&lt;em&gt;A plain-English guide to moving compute from Google Cloud to AWS: VMs, instance groups, Spot, GKE, GPUs, TPUs and batch. What replaces what, what behaves differently, what changes on the bill, and a step-by-step plan.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"Compute" is the part of the cloud that runs your code: virtual machines, Kubernetes nodes, GPU machines and batch jobs. When a team moves from Google Cloud (GCP) to AWS, compute is usually the first thing they plan.&lt;/p&gt;

&lt;p&gt;Teams move for simple reasons. The rest of the company is already on AWS. A company they bought came with an AWS account. Their credits ran out on one cloud and started on the other. Or the data they need already lives in AWS.&lt;/p&gt;

&lt;p&gt;Most teams start with a table: Compute Engine goes to EC2, GKE goes to EKS, GPUs go to GPUs. That table is a good start, but it hides the real work. Services that do the same job often behave a little differently. Machine sizes, maintenance, Spot, pod networking and discounts all change, and those changes show up in production, not on the diagram.&lt;/p&gt;

&lt;p&gt;In this post we go through every GCP compute option one by one. For each, we say what replaces it on AWS, what is different, and what changes on the bill. At the end we show a step-by-step plan and how we run these moves with MigrateX.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This guide covers compute.&lt;/strong&gt; For Cloud Run, Cloud Run functions and Lambda, see &lt;a href="https://dev.to/blog/gcp-to-aws-serverless"&gt;moving serverless from GCP to AWS&lt;/a&gt;. For Persistent Disk and Filestore, see &lt;a href="https://dev.to/blog/gcp-to-aws-storage"&gt;moving storage from GCP to AWS&lt;/a&gt;. For Dataproc and Dataflow, see &lt;a href="https://dev.to/blog/gcp-to-aws-big-data"&gt;moving big data from GCP to AWS&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Quick Map
&lt;/h2&gt;

&lt;p&gt;Here is every GCP compute option and its closest match on AWS. The rest of the post explains each row.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9hu6o8758gxm2mx17rpe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9hu6o8758gxm2mx17rpe.png" alt="Compute service map from Google Cloud to AWS: Compute Engine to EC2, managed instance groups to Auto Scaling groups, Spot VMs to EC2 Spot, GKE to EKS or EKS Auto Mode, GPUs and TPUs to EC2 P and G instances or Trainium, and Batch to AWS Batch, with what to watch for on each" width="799" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GCP&lt;/th&gt;
&lt;th&gt;Closest AWS match&lt;/th&gt;
&lt;th&gt;Watch out for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compute Engine VMs&lt;/td&gt;
&lt;td&gt;EC2&lt;/td&gt;
&lt;td&gt;No custom machine sizes; pick the next size up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instance templates&lt;/td&gt;
&lt;td&gt;EC2 launch templates&lt;/td&gt;
&lt;td&gt;Different fields; rebuild them, don't convert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed instance groups (MIGs)&lt;/td&gt;
&lt;td&gt;EC2 Auto Scaling groups&lt;/td&gt;
&lt;td&gt;Health checks and zones are set up differently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spot VMs / preemptible VMs&lt;/td&gt;
&lt;td&gt;EC2 Spot Instances&lt;/td&gt;
&lt;td&gt;Different warning time and pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sole-tenant nodes&lt;/td&gt;
&lt;td&gt;EC2 Dedicated Hosts&lt;/td&gt;
&lt;td&gt;Licence rules differ per host type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Machine images, custom images&lt;/td&gt;
&lt;td&gt;Amazon Machine Images (AMIs)&lt;/td&gt;
&lt;td&gt;Rebuild or replicate; you can't copy them across&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistent Disk on a VM&lt;/td&gt;
&lt;td&gt;Amazon EBS&lt;/td&gt;
&lt;td&gt;Covered in the Storage post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GKE Standard&lt;/td&gt;
&lt;td&gt;EKS with managed node groups or Karpenter&lt;/td&gt;
&lt;td&gt;Pods use real VPC IPs; plan subnets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GKE Autopilot&lt;/td&gt;
&lt;td&gt;EKS Auto Mode&lt;/td&gt;
&lt;td&gt;Nodes are replaced at least every 21 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU machines (A2, A3, A4, G2)&lt;/td&gt;
&lt;td&gt;EC2 P and G instances&lt;/td&gt;
&lt;td&gt;Getting capacity is the hard part&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud TPU&lt;/td&gt;
&lt;td&gt;Trainium / Inferentia, or GPUs&lt;/td&gt;
&lt;td&gt;There is no TPU on AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch&lt;/td&gt;
&lt;td&gt;AWS Batch&lt;/td&gt;
&lt;td&gt;Job settings and retries map loosely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App Engine&lt;/td&gt;
&lt;td&gt;Elastic Beanstalk or App Runner&lt;/td&gt;
&lt;td&gt;Often a good time to move to containers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Run / Cloud Run functions&lt;/td&gt;
&lt;td&gt;App Runner, ECS on Fargate, Lambda&lt;/td&gt;
&lt;td&gt;See the Serverless post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bare Metal Solution&lt;/td&gt;
&lt;td&gt;EC2 bare metal (&lt;code&gt;.metal&lt;/code&gt;) instances&lt;/td&gt;
&lt;td&gt;Check hardware and licence needs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Virtual Machines: Compute Engine to EC2
&lt;/h2&gt;

&lt;p&gt;This is the easiest part of the move. Both clouds group their machines by job: general purpose, compute-heavy, memory-heavy and GPU. So matching them is mostly a lookup:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GCP series&lt;/th&gt;
&lt;th&gt;Used for&lt;/th&gt;
&lt;th&gt;AWS family&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E2&lt;/td&gt;
&lt;td&gt;Cheap, light general use&lt;/td&gt;
&lt;td&gt;t3 / t4g&lt;/td&gt;
&lt;td&gt;AWS uses CPU credits; busy machines can slow down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;N2, N2D, N4&lt;/td&gt;
&lt;td&gt;General purpose&lt;/td&gt;
&lt;td&gt;m7i / m8i, m7a&lt;/td&gt;
&lt;td&gt;Intel and AMD options on both sides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tau T2A, Axion (C4A)&lt;/td&gt;
&lt;td&gt;Arm general purpose&lt;/td&gt;
&lt;td&gt;m8g, c8g (Graviton)&lt;/td&gt;
&lt;td&gt;Often the cheapest per unit of work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C3, C4, C2&lt;/td&gt;
&lt;td&gt;Compute-heavy&lt;/td&gt;
&lt;td&gt;c7i / c8i, c7a&lt;/td&gt;
&lt;td&gt;Match by vCPU and clock speed, not by name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M1, M2, M3&lt;/td&gt;
&lt;td&gt;Large memory&lt;/td&gt;
&lt;td&gt;r8i, x2iedn, u-series&lt;/td&gt;
&lt;td&gt;Check memory per vCPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A2, A3, G2&lt;/td&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;P4d, P5, G6&lt;/td&gt;
&lt;td&gt;See the GPU section below&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things don't carry over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom machine types.&lt;/strong&gt; On GCP you can ask for any shape, say 6 vCPUs and 20 GB of memory. EC2 only sells fixed sizes, so you round up and pay for the gap. Before you round up, look at what the VM really uses. Many custom shapes were guesses made years ago.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live migration.&lt;/strong&gt; When Google does maintenance, it moves your running VM to new hardware without a reboot. EC2 instead schedules maintenance events, which can mean a stop and start. If a single VM can't be restarted, put it behind an Auto Scaling group or run a second copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; size EC2 from measured CPU and memory, not from the GCP machine type. Rounding up every custom shape is the quickest way to make AWS look more expensive than it is.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What changes inside the VM
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;On GCP&lt;/th&gt;
&lt;th&gt;On AWS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google guest agent&lt;/td&gt;
&lt;td&gt;SSM Agent and cloud-init&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startup script in instance metadata&lt;/td&gt;
&lt;td&gt;EC2 user data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metadata server at &lt;code&gt;metadata.google.internal&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Instance metadata (IMDSv2) at &lt;code&gt;169.254.169.254&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VM service account&lt;/td&gt;
&lt;td&gt;IAM role attached through an instance profile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS Login / SSH keys in metadata&lt;/td&gt;
&lt;td&gt;SSM Session Manager (no open SSH port needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google-provided OS images&lt;/td&gt;
&lt;td&gt;Amazon Linux, Ubuntu, RHEL or Windows AMIs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Instance Groups: MIGs to Auto Scaling Groups
&lt;/h2&gt;

&lt;p&gt;A managed instance group (MIG) is a group of identical VMs that grows or shrinks on its own. It's built from an instance template. On AWS the same thing is an &lt;strong&gt;Auto Scaling group&lt;/strong&gt; built from a &lt;strong&gt;launch template&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Scaling on CPU, rolling updates and replacing broken VMs all have direct matches. Two details are different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zones.&lt;/strong&gt; A regional MIG spreads across zones by itself. An Auto Scaling group only spreads across the subnets you give it, so give it one subnet in each Availability Zone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health checks.&lt;/strong&gt; A MIG uses its own health check to replace broken VMs. An Auto Scaling group only checks that the EC2 machine is up, unless you switch it to use the load balancer's health check (&lt;code&gt;health_check_type = "ELB"&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Load balancers change too. Google's global HTTP(S) load balancer usually becomes an Application Load Balancer (ALB), and TCP/UDP load balancers become Network Load Balancers (NLB). AWS load balancers are regional, so for global traffic add Route 53 or CloudFront in front.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cheaper Capacity: Spot VMs
&lt;/h2&gt;

&lt;p&gt;Both clouds sell spare machines at a big discount and take them back when they need them. The rules are different:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;GCP Spot VMs&lt;/th&gt;
&lt;th&gt;EC2 Spot Instances&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Warning before it's taken back&lt;/td&gt;
&lt;td&gt;0 seconds by default, or 120 seconds if you set it&lt;/td&gt;
&lt;td&gt;Two minutes, best effort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;Fixed discount, changes at most once a month&lt;/td&gt;
&lt;td&gt;Changes slowly by instance type and zone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How you find out&lt;/td&gt;
&lt;td&gt;Shutdown signal, metadata server&lt;/td&gt;
&lt;td&gt;Instance metadata and an EventBridge event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best practice&lt;/td&gt;
&lt;td&gt;Spread across zones and machine types&lt;/td&gt;
&lt;td&gt;List many instance types and use capacity-optimized allocation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Shutdown scripts written for GCP usually still work. You just need to change them to read the AWS notice. The bigger change is to &lt;strong&gt;use many instance types&lt;/strong&gt;. A group that lists one instance type gets interrupted far more often than one that lists ten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dedicated Hardware and Licences
&lt;/h2&gt;

&lt;p&gt;Sole-tenant nodes become &lt;strong&gt;EC2 Dedicated Hosts&lt;/strong&gt;. Most teams use them for software licensed per physical core, like Windows Server or SQL Server under bring-your-own-licence terms. Read your licence terms for AWS before you move. Often it's cheaper to use licence-included EC2 instances and drop dedicated hardware completely.&lt;/p&gt;

&lt;p&gt;If you use &lt;strong&gt;Bare Metal Solution&lt;/strong&gt; (for example, for Oracle), look at EC2 bare metal instances, or Amazon RDS if the database can be managed for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving the VMs Themselves
&lt;/h2&gt;

&lt;p&gt;There are two ways to get a VM onto EC2: copy the running server, or rebuild it from code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxf0kd2k0y5y8j93e69vm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxf0kd2k0y5y8j93e69vm.png" alt="Two ways to move a Compute Engine VM: rehost with AWS Transform MGN, which installs an agent, replicates disks continuously, launches a test instance and then cuts over in minutes; or rebuild with Packer or EC2 Image Builder, producing an AMI that an Auto Scaling group launches" width="800" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Copy it (rehost).&lt;/strong&gt; AWS Transform MGN (formerly AWS Application Migration Service) copies servers to AWS with very little downtime. You install an agent on the GCP VM. It keeps copying the disks to AWS. You launch a test machine, check it, then switch over in minutes. Use this for old servers nobody can rebuild, licensed software, or hand-tuned databases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild it.&lt;/strong&gt; If the VM is built from a startup script, Ansible or Packer, use the same tools to build an AMI (Packer's &lt;code&gt;amazon-ebs&lt;/code&gt; builder, or EC2 Image Builder) and launch it from an Auto Scaling group. It's more work at first, but you get something you can rebuild any time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way, plan the network around the move. IP addresses change, so use DNS names, not hard-coded IPs. Lower DNS TTLs a few days before you switch. And set up a private link between the clouds (VPN or Interconnect to Direct Connect) so old and new servers can talk while both are running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes: GKE to EKS
&lt;/h2&gt;

&lt;p&gt;Your Kubernetes manifests and Helm charts mostly move as they are. The cluster around them doesn't. Four things catch almost every team:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Networking.&lt;/strong&gt; On EKS, every pod gets a real IP address from your VPC subnet. A busy cluster in a small subnet runs out of addresses fast. Use bigger subnets, or turn on prefix delegation, before you move workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity.&lt;/strong&gt; GKE Workload Identity becomes EKS Pod Identity (or the older IRSA). Each Kubernetes service account gets an IAM role. It's the same idea, but every link has to be made again. This is where most "access denied" errors come from in the first week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ingress.&lt;/strong&gt; GKE creates Google load balancers for you. On EKS, install the AWS Load Balancer Controller, which creates ALBs and NLBs. The annotations are different, so check every Ingress.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nodes.&lt;/strong&gt; If you use GKE Autopilot, EKS Auto Mode is the closest match. If you manage node pools yourself on GKE Standard, use managed node groups or Karpenter. Karpenter picks the right instance type for each workload, which often saves money and makes Spot much safer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One difference from Autopilot: EKS Auto Mode replaces every node after at most 21 days. Your workloads must be fine with nodes coming and going. Storage classes change too: GCE Persistent Disk volumes become EBS volumes through the EBS CSI driver.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;GKE&lt;/th&gt;
&lt;th&gt;EKS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pod identity&lt;/td&gt;
&lt;td&gt;Workload Identity Federation&lt;/td&gt;
&lt;td&gt;EKS Pod Identity (or IRSA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod IPs&lt;/td&gt;
&lt;td&gt;Separate IP ranges from nodes&lt;/td&gt;
&lt;td&gt;Pods take IPs from the node's subnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ingress&lt;/td&gt;
&lt;td&gt;GKE Ingress / Gateway creates Google LBs&lt;/td&gt;
&lt;td&gt;AWS Load Balancer Controller creates ALBs / NLBs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node autoscaling&lt;/td&gt;
&lt;td&gt;Cluster autoscaler, node auto-provisioning&lt;/td&gt;
&lt;td&gt;Karpenter, or built in with Auto Mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fully managed nodes&lt;/td&gt;
&lt;td&gt;Autopilot&lt;/td&gt;
&lt;td&gt;EKS Auto Mode (21-day max node life)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disks for pods&lt;/td&gt;
&lt;td&gt;GCE PD CSI driver&lt;/td&gt;
&lt;td&gt;EBS CSI driver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control plane cost&lt;/td&gt;
&lt;td&gt;Cluster fee, with a monthly free credit&lt;/td&gt;
&lt;td&gt;$0.10 per cluster per hour (standard support)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Plan the subnets first.&lt;/strong&gt; Running out of pod IPs is the most common EKS surprise for teams coming from GKE, and it's much easier to fix before the cluster exists.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  GPUs Move, TPUs Don't
&lt;/h2&gt;

&lt;p&gt;GPU workloads move fairly easily. Both clouds use the same NVIDIA chips, so CUDA code doesn't change:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GCP machine&lt;/th&gt;
&lt;th&gt;GPU&lt;/th&gt;
&lt;th&gt;AWS instance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A2&lt;/td&gt;
&lt;td&gt;NVIDIA A100&lt;/td&gt;
&lt;td&gt;P4d / P4de&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A3&lt;/td&gt;
&lt;td&gt;NVIDIA H100 / H200&lt;/td&gt;
&lt;td&gt;P5 / P5e / P5en&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A4&lt;/td&gt;
&lt;td&gt;NVIDIA B200&lt;/td&gt;
&lt;td&gt;P6-B200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G2&lt;/td&gt;
&lt;td&gt;NVIDIA L4&lt;/td&gt;
&lt;td&gt;G6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud TPU&lt;/td&gt;
&lt;td&gt;TPU&lt;/td&gt;
&lt;td&gt;No match: GPUs, or Trainium / Inferentia&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The hard part is getting the machines. New AWS accounts start with low GPU limits, so ask for more weeks ahead. If you need many GPUs for a fixed period, look at Capacity Blocks for ML.&lt;/p&gt;

&lt;p&gt;TPUs are different. AWS has no TPU, so TPU code has to be changed, not just moved. JAX and PyTorch/XLA models can run on GPUs with some work. AWS's own chips (Trainium for training, Inferentia for serving) can cost less, but they need the Neuron SDK and testing for each model. Treat this as its own engineering project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Batch Jobs
&lt;/h2&gt;

&lt;p&gt;GCP Batch becomes &lt;strong&gt;AWS Batch&lt;/strong&gt;. Both take a job, put it in a queue, and run it on machines they create for you. AWS Batch can run on EC2 (normal or Spot), on Fargate, or on EKS. For long jobs that can be stopped, run them on Spot with several instance types and save progress along the way (checkpoints), so a lost machine costs minutes, not hours. Retry settings are not the same on both sides, so set them on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Networking You'll Trip Over
&lt;/h2&gt;

&lt;p&gt;Networking isn't compute, but it's where compute moves go wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VPCs are regional on AWS.&lt;/strong&gt; One global VPC on GCP becomes one VPC per region, joined with Transit Gateway or peering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subnets live in one zone.&lt;/strong&gt; On GCP a subnet covers the whole region. On AWS each subnet is in one Availability Zone, so you need one per zone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firewall rules become security groups.&lt;/strong&gt; GCP firewall rules use network tags. AWS security groups are attached to each machine and can point at other security groups, which is cleaner but means rewriting every rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic between zones costs money.&lt;/strong&gt; On AWS, data that crosses Availability Zones and NAT Gateway traffic are charged per GB. Keep chatty services in the same zone where you can.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A Simple Way to Decide
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F89i5wzis0xd5sqr6ubre.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F89i5wzis0xd5sqr6ubre.png" alt="Compute decision flow: if it needs TPUs, port first; if it is on Kubernetes and staying there, EKS; if it is batch or interruptible, AWS Batch on Spot; if it needs per-core licences or dedicated hardware, Dedicated Hosts or licence-included EC2; otherwise EC2 in an Auto Scaling group" width="799" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For each workload, ask these questions in order and stop at the first yes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does it need TPUs?&lt;/strong&gt; Plan the move to GPUs or Trainium before anything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it on Kubernetes, and does the team want to keep it there?&lt;/strong&gt; EKS (Auto Mode if you were on Autopilot).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it batch work that can be stopped and restarted?&lt;/strong&gt; AWS Batch on Spot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it need per-core licences or dedicated hardware?&lt;/strong&gt; EC2 Dedicated Hosts, or licence-included EC2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it small, stateless and request-driven?&lt;/strong&gt; It may not need a VM at all. See the Serverless post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything else:&lt;/strong&gt; EC2 in an Auto Scaling group, sized from real usage.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Changes on the Bill
&lt;/h2&gt;

&lt;p&gt;Most cost surprises come from discounts, not list prices.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;GCP&lt;/th&gt;
&lt;th&gt;AWS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Automatic discounts&lt;/td&gt;
&lt;td&gt;Sustained use discounts on some series (N1, N2, N2D, C2, M1, M2)&lt;/td&gt;
&lt;td&gt;None; you pay the list price all month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commitments&lt;/td&gt;
&lt;td&gt;Committed use discounts&lt;/td&gt;
&lt;td&gt;Savings Plans and Reserved Instances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Billing unit&lt;/td&gt;
&lt;td&gt;Per second (1-minute minimum)&lt;/td&gt;
&lt;td&gt;Per second for Linux (1-minute minimum)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spot pricing&lt;/td&gt;
&lt;td&gt;Fixed discount, changes at most monthly&lt;/td&gt;
&lt;td&gt;Changes by instance type and zone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed Kubernetes&lt;/td&gt;
&lt;td&gt;Cluster fee, with a monthly free credit&lt;/td&gt;
&lt;td&gt;$0.10 per cluster per hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;Premium global network by default&lt;/td&gt;
&lt;td&gt;Cross-zone traffic and NAT Gateway charged per GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The discount that quietly disappears is the &lt;strong&gt;sustained use discount&lt;/strong&gt;. On GCP, a VM that runs most of the month gets up to 30% off automatically. AWS has no such thing. You only get that money back by buying a Savings Plan.&lt;/p&gt;

&lt;p&gt;Don't buy one in the first weeks. Run on normal pricing until usage is steady, then commit to the amount you're sure you'll use. A Compute Savings Plan also covers EKS nodes, Fargate and Lambda, so you don't have to commit to one instance family.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Looks Like in Terraform
&lt;/h2&gt;

&lt;p&gt;A regional managed instance group becomes a launch template plus an Auto Scaling group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# GCP: a group of 3 identical VMs across zones
resource "google_compute_region_instance_group_manager" "web" {
  name               = "web"
  base_instance_name = "web"
  region             = "us-central1"
  target_size        = 3
  version { instance_template = google_compute_instance_template.web.id }
}

# AWS: the same group as an Auto Scaling group
resource "aws_autoscaling_group" "web" {
  name                = "web"
  desired_capacity    = 3
  min_size            = 3
  max_size            = 6
  vpc_zone_identifier = var.private_subnet_ids # one subnet per zone
  health_check_type   = "ELB"                  # use the load balancer's check
  launch_template {
    id      = aws_launch_template.web.id
    version = "$Latest"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A group that uses Spot should list several instance types. Inside the &lt;code&gt;aws_autoscaling_group&lt;/code&gt;, replace the &lt;code&gt;launch_template&lt;/code&gt; block with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mixed_instances_policy {
  instances_distribution {
    on_demand_base_capacity                  = 1  # always keep one normal VM
    on_demand_percentage_above_base_capacity = 0  # the rest on Spot
    spot_allocation_strategy                 = "price-capacity-optimized"
  }
  launch_template {
    launch_template_specification { launch_template_id = aws_launch_template.web.id }
    override { instance_type = "m7i.large" }
    override { instance_type = "m7a.large" }
    override { instance_type = "m6i.large" }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These snippets show the shape, not every setting. Check the current provider docs before you copy them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things That Bite Later
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Service account keys.&lt;/strong&gt; Every VM service account becomes an IAM role, and every key file hidden in a script needs a new home.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata calls.&lt;/strong&gt; Code that calls &lt;code&gt;metadata.google.internal&lt;/code&gt; for project, zone or tokens must switch to IMDSv2 or the AWS SDK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limits.&lt;/strong&gt; New AWS accounts start with low limits for vCPUs, GPUs and Elastic IPs. Ask for more weeks ahead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AMI IDs differ per region.&lt;/strong&gt; Look them up per region, or use SSM parameters, instead of hard-coding them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-zone setups.&lt;/strong&gt; Check that every Auto Scaling group and node group covers at least two Availability Zones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring and logs.&lt;/strong&gt; Cloud Monitoring and Cloud Logging agents become the CloudWatch agent. Rebuild alerts and dashboards before you switch traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Doing the Move, Step by Step
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;List everything.&lt;/strong&gt; Every VM, instance group, cluster and batch job, with its machine type, real usage and what it talks to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the target&lt;/strong&gt; for each one using the questions above. Write down why, so you can revisit it later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the base:&lt;/strong&gt; AWS accounts, VPCs, subnets big enough for EKS, IAM roles, limits and a private link to GCP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with a pilot.&lt;/strong&gt; Move one small, low-risk service end to end to prove the path works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move in waves.&lt;/strong&gt; Stateless groups and clusters first, then rehost the harder servers with MGN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shift traffic slowly&lt;/strong&gt; with weighted DNS, and keep GCP running until AWS is steady.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean up.&lt;/strong&gt; Turn off GCP group by group, and buy Savings Plans only once AWS usage is steady.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compute is usually the easy part of a cloud move. The service map fits on one page. The real work is in the details: machine sizes, maintenance, Spot, pod IPs, identity and discounts. Decide those on purpose and the rest is mostly typing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Compute Engine vs EC2: what is actually different?
&lt;/h3&gt;

&lt;p&gt;The machines are close matches. Two things don't carry over: EC2 has no custom machine types, so you round up to the next fixed size, and EC2 has no live migration, so maintenance can mean a stop and start. The Google guest agent, startup scripts and metadata server are replaced by SSM Agent, user data and IMDSv2.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I map GCP machine types to EC2 instance types?
&lt;/h3&gt;

&lt;p&gt;Match by job first, then by vCPU, memory and clock speed, not by name. E2 maps to t3 or t4g, N2 and N2D to m7i or m7a, C3 and C4 to c7i or c8i, M-series to r8i or x2iedn, and Arm machines like Axion to Graviton (m8g, c8g). Size from what the VM really uses, not from its old shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I migrate a VM from GCP to AWS step by step?
&lt;/h3&gt;

&lt;p&gt;Either copy it or rebuild it. AWS Transform MGN (formerly AWS Application Migration Service) installs an agent on the GCP VM, keeps the disks in sync with AWS, and lets you switch over in minutes. If the VM is built from code with Packer or Ansible, build an AMI instead and run it in an Auto Scaling group.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the GKE to EKS migration checklist?
&lt;/h3&gt;

&lt;p&gt;Your manifests and Helm charts mostly move as they are. Plan four things: bigger subnets (every pod takes a VPC IP), Workload Identity to EKS Pod Identity, GKE Ingress to the AWS Load Balancer Controller, and nodes (EKS Auto Mode for Autopilot, managed node groups or Karpenter for GKE Standard). Swap the GCE PD CSI driver for the EBS CSI driver.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a TPU equivalent on AWS?
&lt;/h3&gt;

&lt;p&gt;No. TPU code has to be changed, not just moved. JAX and PyTorch/XLA models can run on NVIDIA GPUs with some work, or on AWS Trainium and Inferentia through the Neuron SDK. Treat it as its own project.&lt;/p&gt;

&lt;h3&gt;
  
  
  What replaces GCP sustained use discounts on AWS?
&lt;/h3&gt;

&lt;p&gt;Nothing automatic. On AWS you get that saving back by buying a Compute Savings Plan, which also covers EKS nodes, Fargate and Lambda. Wait until usage is steady before you commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  About MigrateX
&lt;/h2&gt;

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

&lt;p&gt;MigrateX is our platform and service for moving software, data and infrastructure from one technology to another. It combines automation with experienced engineers. The automation does the repetitive work quickly, and the engineers make the tricky decisions.&lt;/p&gt;

&lt;p&gt;This guide is how we approach GCP to AWS compute moves with MigrateX. Here's what that looks like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assessment.&lt;/strong&gt; We list every VM, instance group, GKE cluster, GPU machine and batch job, with real usage, dependencies, licences and the hidden ties to GCP, like metadata calls and service account keys. You see where the work is before anyone agrees on a timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan.&lt;/strong&gt; We work with your team to pick the AWS target for each workload, right-size the machines, design the network and accounts, and decide the order of the waves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration, step by step.&lt;/strong&gt; Automation does the repetitive parts, like turning Terraform and instance templates into launch templates, Auto Scaling groups and EKS setups, and replicating servers with MGN. Our engineers handle the parts that need thought: identity, networking, Spot design and TPU code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof that it works.&lt;/strong&gt; Each workload runs on both clouds side by side. We compare health, speed and cost, and only shift traffic once AWS matches or beats GCP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handover and clean-up.&lt;/strong&gt; We turn off GCP group by group, set up Savings Plans once usage is steady, and leave your team with AWS infrastructure they understand and own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What you get along the way:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No big switch-over weekend.&lt;/strong&gt; Your services keep running while the move happens one wave at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Less risk.&lt;/strong&gt; Side-by-side runs and slow traffic shifts mean you can always switch back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A smaller bill, not a bigger one.&lt;/strong&gt; Right-sizing, Spot and Savings Plans are part of the move, not an afterthought.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More than one kind of move.&lt;/strong&gt; We use the same approach for storage, serverless and big data, and for front ends like Angular, Ember and jQuery.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Planning a move from GCP to AWS? We'd love to hear about it. Tell us a little about your setup through &lt;a href="https://cobuildx.ai/contact" rel="noopener noreferrer"&gt;Contact Us&lt;/a&gt;, and we'll start with a free chat about where the work is likely to be.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;h3&gt;
  
  
  AWS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AWS. &lt;em&gt;What is AWS Transform MGN?&lt;/em&gt; &lt;a href="https://docs.aws.amazon.com/mgn/latest/ug/what-is-application-migration-service.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/mgn/latest/ug/what-is-application-migration-service.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Spot Instance interruption notices&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-instance-termination-notices.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-instance-termination-notices.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Amazon EKS Auto Mode&lt;/em&gt;. &lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/automode.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/eks/latest/userguide/automode.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Amazon EKS pricing&lt;/em&gt;. &lt;a href="https://aws.amazon.com/eks/pricing/" rel="noopener noreferrer"&gt;https://aws.amazon.com/eks/pricing/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Compute Savings Plans&lt;/em&gt;. &lt;a href="https://aws.amazon.com/savingsplans/compute-pricing/" rel="noopener noreferrer"&gt;https://aws.amazon.com/savingsplans/compute-pricing/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;Amazon EC2 Dedicated Hosts&lt;/em&gt;. &lt;a href="https://aws.amazon.com/ec2/dedicated-hosts/" rel="noopener noreferrer"&gt;https://aws.amazon.com/ec2/dedicated-hosts/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AWS. &lt;em&gt;AWS Batch&lt;/em&gt;. &lt;a href="https://aws.amazon.com/batch/" rel="noopener noreferrer"&gt;https://aws.amazon.com/batch/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Google Cloud
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Google Cloud. &lt;em&gt;Spot VMs&lt;/em&gt;. &lt;a href="https://docs.cloud.google.com/compute/docs/instances/spot" rel="noopener noreferrer"&gt;https://docs.cloud.google.com/compute/docs/instances/spot&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Cloud. &lt;em&gt;Sustained use discounts&lt;/em&gt;. &lt;a href="https://docs.cloud.google.com/compute/docs/sustained-use-discounts" rel="noopener noreferrer"&gt;https://docs.cloud.google.com/compute/docs/sustained-use-discounts&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Cloud. &lt;em&gt;GKE pricing&lt;/em&gt;. &lt;a href="https://cloud.google.com/kubernetes-engine/pricing" rel="noopener noreferrer"&gt;https://cloud.google.com/kubernetes-engine/pricing&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;CobuildX helps teams migrate legacy applications and cloud infrastructure with &lt;a href="https://cobuildx.ai/migratex" rel="noopener noreferrer"&gt;MigrateX&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/gcp-to-aws-compute-options" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>googlecloud</category>
      <category>cloud</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>A 14-Year-Old PHP Bug Lets Strangers Run Code on Your Server. Here's How to Stop It</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Thu, 22 Oct 2026 11:54:50 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/a-14-year-old-php-bug-lets-strangers-run-code-on-your-server-heres-how-to-stop-it-h50</link>
      <guid>https://dev.to/cobuildx-ai/a-14-year-old-php-bug-lets-strangers-run-code-on-your-server-heres-how-to-stop-it-h50</guid>
      <description>&lt;p&gt;&lt;em&gt;The code nobody touched for years just became everyone's problem. A bug hiding in PHP since 2012 lets strangers run code on web servers, and the NSA and India's CERT-In have both raised the alarm. Here's how PHP2Shell (CVE-2026-109618 and CVE-2026-109619) works, how it was found, and exactly what to do about it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On 21 October 2026, PHP shipped an emergency release for two critical bugs in the PHP engine itself: CVE-2026-109618 and CVE-2026-109619, nicknamed &lt;strong&gt;PHP2Shell&lt;/strong&gt;. Both let an attacker run code on a web server without logging in [1][2].&lt;/p&gt;

&lt;p&gt;Governments moved fast. The NSA told organizations that can't update right away to take their PHP systems offline [3]. The same day, India's national cybersecurity agency, &lt;strong&gt;CERT-In&lt;/strong&gt;, issued a critical-severity advisory (CIVN-2026-0510) warning that vulnerable PHP servers could be exposed to remote code execution [10][11]. CISA in the US issued its own alert [6].&lt;/p&gt;

&lt;p&gt;The unsettling part is where the bugs came from: code that has sat in PHP since 2012, and that attackers started probing before a patch existed. This post explains what the two bugs are, how they work, how they were found, and what to do about them. If you run PHP anywhere, including inside WordPress, Drupal, Magento or an internal tool, it applies to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Bugs at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;CVE-2026-109618&lt;/th&gt;
&lt;th&gt;CVE-2026-109619&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name in the advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RCE via OOM corruption&lt;/td&gt;
&lt;td&gt;RCE via stale global state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bug type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Memory corruption when PHP runs out of memory&lt;/td&gt;
&lt;td&gt;Use-after-free across two requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Severity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CVSS v4.0 9.2, critical&lt;/td&gt;
&lt;td&gt;CVSS v4.0 9.2, critical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Login needed?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Confirmed exploit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apache mod_php&lt;/td&gt;
&lt;td&gt;mod_php and PHP-FPM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Advisory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GHSA-w8hw-w9fx-j79w&lt;/td&gt;
&lt;td&gt;GHSA-6wmf-4rv5-p3rp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In both cases, a successful attack runs code as the web server's PHP user. From there an attacker can read files, steal database passwords and keys from config files, or plant a backdoor [1][2].&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Bugs Work
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcj8meg9td0ag9agfhxv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcj8meg9td0ag9agfhxv.png" alt="How PHP2Shell works. CVE-2026-109618: a crafted multipart request makes PHP run out of memory while the request is half set up; PHP jumps out early, and the clean-up step then uses the half-built data. CVE-2026-109619: request A ends in a fatal error, so clean-up is skipped and the worker keeps a pointer to freed memory; request B on the same worker uses that pointer before normal setup clears it" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-109618: running out of memory at the wrong moment
&lt;/h3&gt;

&lt;p&gt;When PHP runs out of memory, it doesn't return an error step by step. It jumps straight out of whatever it was doing, using a C function called &lt;code&gt;longjmp()&lt;/code&gt;. If that happens while a request is still being set up, some of the request's data is left half-built. Later, when PHP cleans up the request, it treats that half-built data as valid, and memory gets corrupted [1].&lt;/p&gt;

&lt;p&gt;An attacker can cause this on purpose. Weaknesses in how PHP parses multipart form uploads let a crafted request use up memory while still looking like it's within the configured limits. On Apache with mod_php, this happens before any application code runs, so the app itself doesn't need to have a bug [1][4].&lt;/p&gt;

&lt;h3&gt;
  
  
  CVE-2026-109619: one request's leftovers, used by the next
&lt;/h3&gt;

&lt;p&gt;A PHP worker handles many requests in a row, and it keeps some global state between them: error, session and extension data. Normally that state is reset at the end of each request. But a fatal error, such as running out of memory or hitting the execution time limit, can skip that reset. The worker is left holding a pointer to memory that's already been freed [2].&lt;/p&gt;

&lt;p&gt;If the next request on the same worker touches that state before it's reset, PHP uses freed memory. The advisory confirms two ways to reach it. One is &lt;strong&gt;session upload progress&lt;/strong&gt;, a feature that's on by default and works even if the app never calls &lt;code&gt;session_start()&lt;/code&gt;. The other is PHP 8.5's new &lt;strong&gt;fatal error backtraces&lt;/strong&gt; setting, also on by default. Besides running code, this bug can leak memory from other users' requests, including passwords and tokens [2].&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Was Found
&lt;/h2&gt;

&lt;p&gt;Both bugs were found in August 2026 by researchers auditing PHP's C source with the help of AI models. Researcher Ryan Austin at Calif found one during an audit run under OpenAI's "Patch the Planet" project. Wiz found the same flaws independently, using AI models alongside its own tooling [5]. The PHP advisories credit reporters from both efforts and others [1][2].&lt;/p&gt;

&lt;p&gt;The weakness wasn't entirely new. In 2024 a PHP core developer spotted the out-of-memory problem behind CVE-2026-109618 during an audit, but the team couldn't agree on a fix, and a working exploit was thought unlikely, so it wasn't prioritised [5]. The affected code dates back to PHP 5.4, released in 2012.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timeline
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F29qtk9sa9bhqpqlkm6t3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F29qtk9sa9bhqpqlkm6t3.png" alt="Timeline: 2012, PHP 5.4 ships with the affected code. 2024, the out-of-memory issue is spotted but not fixed. August 2026, Calif and Wiz report the bugs. 17 October, first exploit attempt seen by Amazon. 20 October, details leak and PHP announces a release. 21 October, patches and alerts from the NSA, CISA and CERT-In" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The release was meant to be a normal coordinated disclosure, but it didn't stay quiet. Amazon's honeypots logged an exploit attempt on 17 October, four days early. On 20 October news leaked on Hacker News, watchTowr posted about it, and a proof-of-concept video was published by mistake, so PHP announced the release a day ahead. The fixes and the two GitHub advisories went out on 21 October, the same day as alerts from CISA, the NSA, India's CERT-In and others [5][6][10].&lt;/p&gt;

&lt;p&gt;Some organizations didn't wait. Government portals in the Netherlands, Finland and France, many Wikimedia services and the Internet Archive went offline for a time as a precaution. Wiz compared the bug to Heartbleed, the 2014 OpenSSL flaw [5].&lt;/p&gt;

&lt;h2&gt;
  
  
  Am I Affected?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Versions:&lt;/strong&gt; everything from PHP 5.4 up to, but not including, &lt;strong&gt;8.2.35, 8.3.36, 8.4.27 and 8.5.12&lt;/strong&gt; [1][2].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server setups:&lt;/strong&gt; any PHP that serves web requests: Apache mod_php (highest risk), PHP-FPM, php-cgi, FrankenPHP and LiteSpeed [4].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old versions:&lt;/strong&gt; PHP 8.1 and older are end of life. The PHP team released source patches for 7.4, 8.0 and 8.1, but no official builds. Older versions get nothing [5].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden PHP:&lt;/strong&gt; PHP is often bundled inside other products, such as control panels, appliances and vendor software. Cisco, for example, published its own advisory for affected products [6][7].&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To check a server, look at the version and the two settings that matter most:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;php -v
php -i | grep -E 'session.upload_progress.enabled|fatal_error_backtraces|memory_limit'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this with the same PHP the web server uses. The command-line PHP can be a different version from PHP-FPM or mod_php, so also check your web server's &lt;code&gt;phpinfo()&lt;/code&gt; page or package list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Do Now
&lt;/h2&gt;

&lt;p&gt;For security teams, this goes beyond applying an update. The work falls into four steps: patch, mitigate, contain and investigate. Start by finding every PHP install, internet-facing ones first [6].&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Patch
&lt;/h3&gt;

&lt;p&gt;Upgrade to a fixed release: &lt;strong&gt;8.5.12, 8.4.27, 8.3.36 or 8.2.35&lt;/strong&gt;, or your Linux distribution's patched package. Then &lt;strong&gt;restart every PHP worker&lt;/strong&gt;: PHP-FPM, Apache and any long-running PHP processes. Until they restart, the old code is still running [1][2].&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Mitigate, if you can't patch today
&lt;/h3&gt;

&lt;p&gt;The PHP advisories list temporary settings. Set them in &lt;code&gt;php.ini&lt;/code&gt;, the PHP-FPM pool or the web server config. Setting them with &lt;code&gt;ini_set()&lt;/code&gt; or &lt;code&gt;.user.ini&lt;/code&gt; is too late to help [1][2][4].&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;; CVE-2026-109619: closes the confirmed session path.
; Worth keeping off after patching if you don't use upload progress bars.
session.upload_progress.enabled = 0

; CVE-2026-109619, PHP 8.5 only: closes the confirmed backtrace path.
fatal_error_backtraces = 0

; CVE-2026-109618: avoids the out-of-memory jump. Not a complete fix,
; so pair it with tight request limits below and at your proxy.
memory_limit = -1
post_max_size = 8M
upload_max_filesize = 2M
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Limit requests before they reach PHP.&lt;/strong&gt; At nginx, Apache or your load balancer, cap request size, the number of form fields and multipart parts, and headers per part. If your app doesn't take file uploads or multipart forms, block them entirely [1].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put a web application firewall in front&lt;/strong&gt; if you have one [6].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't rely on &lt;code&gt;RLIMIT_AS&lt;/code&gt; or &lt;code&gt;RLIMIT_DATA&lt;/code&gt;.&lt;/strong&gt; These operating system memory limits can trigger the very bug you're trying to avoid [1].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hiding errors doesn't help.&lt;/strong&gt; Turning off &lt;code&gt;display_errors&lt;/code&gt; or using &lt;code&gt;@&lt;/code&gt; doesn't stop the stale state from being used [4].&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;These settings only close the paths researchers have confirmed so far.&lt;/strong&gt; Both advisories say updating is the only complete fix [1][2].&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Contain
&lt;/h3&gt;

&lt;p&gt;If an internet-facing system can't be patched yet, or runs PHP 8.1 or older, take it offline until it's patched or upgraded. This is what the NSA and CISA both advise [3][6]. Several governments, Wikimedia and the Internet Archive did exactly this on 21 October [5].&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Investigate
&lt;/h3&gt;

&lt;p&gt;Exploit attempts started before the patch, so a patched server may already have been hit. Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP worker processes starting a shell (&lt;code&gt;sh&lt;/code&gt;, &lt;code&gt;bash&lt;/code&gt;) or tools like &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;wget&lt;/code&gt; [6].&lt;/li&gt;
&lt;li&gt;Signs of persistence: new or changed &lt;code&gt;.php&lt;/code&gt; files in web directories (especially upload folders), new cron jobs or new user accounts.&lt;/li&gt;
&lt;li&gt;Unexpected outbound connections from web servers.&lt;/li&gt;
&lt;li&gt;Bursts of large or unusual multipart POST requests in your access logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clean logs don't prove a server is safe [2]. If you find signs of compromise, treat it as an incident: rotate the database passwords and API keys stored on that server, not just the PHP version.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Lesson
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Legacy code isn't harmless just because it's familiar.&lt;/strong&gt; The code behind PHP2Shell had run on millions of servers for 14 years, and it was trusted because nothing had gone wrong yet.&lt;/p&gt;

&lt;p&gt;Neither bug was in anyone's PHP code. Both were memory-safety bugs in the PHP engine, which is written in C: memory used after it was freed, and data used before it was fully set up. These are the same classes of bug the NSA and CISA have pointed to for years when they urge a move to memory-safe languages for new code [8][9].&lt;/p&gt;

&lt;p&gt;Two other points stand out. AI-assisted audits found bugs that had sat in widely used code for over a decade, and attackers have the same tools. And PHP runs a large share of the web, yet the PHP Foundation has only 13 employees and relies on donations [5]. If your business depends on PHP, supporting the foundation is part of keeping it safe.&lt;/p&gt;

&lt;p&gt;For now, the steps are simple: find every PHP server, update it, restart it, and check it wasn't hit first.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Advisories&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP. &lt;em&gt;RCE via OOM corruption&lt;/em&gt; (CVE-2026-109618, GHSA-w8hw-w9fx-j79w). &lt;a href="https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w" rel="noopener noreferrer"&gt;https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PHP. &lt;em&gt;RCE via stale global state&lt;/em&gt; (CVE-2026-109619, GHSA-6wmf-4rv5-p3rp). &lt;a href="https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp" rel="noopener noreferrer"&gt;https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NSA Cybersecurity. Post on PHP vulnerabilities, 21 October 2026. &lt;a href="https://x.com/NSACyber/status/2112903220209098897" rel="noopener noreferrer"&gt;https://x.com/NSACyber/status/2112903220209098897&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Analysis and background&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rapid7. &lt;em&gt;CVE-2026-109618 and CVE-2026-109619: Critical Unauthenticated Remote Code Execution in PHP.&lt;/em&gt; &lt;a href="https://www.rapid7.com/blog/post/etr-cve-2026-109618-and-cve-2026-109619-critical-unauthenticated-remote-code-execution-in-php/" rel="noopener noreferrer"&gt;https://www.rapid7.com/blog/post/etr-cve-2026-109618-and-cve-2026-109619-critical-unauthenticated-remote-code-execution-in-php/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Wikipedia. &lt;em&gt;PHP2Shell.&lt;/em&gt; &lt;a href="https://en.wikipedia.org/wiki/PHP2Shell" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/PHP2Shell&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CISA. &lt;em&gt;Critical Vulnerabilities in PHP Require Immediate Patching&lt;/em&gt;, 21 October 2026. &lt;a href="https://www.cisa.gov/news-events/alerts/2026/10/21/critical-vulnerabilities-php-require-immediate-patching" rel="noopener noreferrer"&gt;https://www.cisa.gov/news-events/alerts/2026/10/21/critical-vulnerabilities-php-require-immediate-patching&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cisco. &lt;em&gt;Vulnerabilities in PHP Affecting Cisco Products: October 2026.&lt;/em&gt; &lt;a href="https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-php-october26-Jj384Daq" rel="noopener noreferrer"&gt;https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-php-october26-Jj384Daq&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NSA. &lt;em&gt;Software Memory Safety&lt;/em&gt; (Cybersecurity Information Sheet). &lt;a href="https://media.defense.gov/2022/Nov/10/2003112742/-1/-1/0/CSI_SOFTWARE_MEMORY_SAFETY.PDF" rel="noopener noreferrer"&gt;https://media.defense.gov/2022/Nov/10/2003112742/-1/-1/0/CSI_SOFTWARE_MEMORY_SAFETY.PDF&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CISA. &lt;em&gt;The Case for Memory Safe Roadmaps.&lt;/em&gt; &lt;a href="https://www.cisa.gov/resources-tools/resources/case-memory-safe-roadmaps" rel="noopener noreferrer"&gt;https://www.cisa.gov/resources-tools/resources/case-memory-safe-roadmaps&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CERT-In. &lt;em&gt;Vulnerability Note CIVN-2026-0510: PHP2Shell vulnerabilities in PHP&lt;/em&gt;, 21 October 2026. &lt;a href="https://www.cert-in.org.in/s2cMainServlet?VLCODE=CIVN-2026-0510&amp;amp;pageid=PUBVLNOTES01" rel="noopener noreferrer"&gt;https://www.cert-in.org.in/s2cMainServlet?VLCODE=CIVN-2026-0510&amp;amp;pageid=PUBVLNOTES01&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NewsBytes. &lt;em&gt;US NSA and India's CERT-In flag 'PHP2Shell' web risk.&lt;/em&gt; &lt;a href="https://www.newsbytesapp.com/news/science/us-nsa-and-indias-cert-in-flag-php2shell-web-risk/tldr" rel="noopener noreferrer"&gt;https://www.newsbytesapp.com/news/science/us-nsa-and-indias-cert-in-flag-php2shell-web-risk/tldr&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;CobuildX helps teams modernise legacy systems with &lt;a href="https://cobuildx.ai/migratex" rel="noopener noreferrer"&gt;MigrateX&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/php2shell-cve-2026-109618-109619" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>security</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Modernize Your Legacy Frontend Without a Rewrite</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Tue, 20 Oct 2026 08:06:01 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/migratex-modernize-legacy-frontends-one-feature-at-a-time-31oh</link>
      <guid>https://dev.to/cobuildx-ai/migratex-modernize-legacy-frontends-one-feature-at-a-time-31oh</guid>
      <description>&lt;p&gt;&lt;em&gt;Rewrites don't fail because of the new framework. They fail because of everything the old app does that nobody wrote down.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MigrateX&lt;/strong&gt; moves legacy frontends — Ember, Backbone, Angular, jQuery — to React &lt;strong&gt;one feature at a time&lt;/strong&gt;, while your product keeps shipping.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔍 &lt;strong&gt;Map everything first.&lt;/strong&gt; Every route, service and hidden side effect, before a line is ported.&lt;/li&gt;
&lt;li&gt;🔀 &lt;strong&gt;Ship behind a switch.&lt;/strong&gt; Each feature can be turned back to the old app instantly.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Prove parity.&lt;/strong&gt; The same tests run on old and new. It goes live only when both pass.&lt;/li&gt;
&lt;li&gt;🧹 &lt;strong&gt;Leave nothing behind.&lt;/strong&gt; Old code, switches and glue removed. One clean codebase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No feature freeze. No launch day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;See it working:&lt;/strong&gt; five real apps migrated in the open → &lt;a href="https://github.com/cobuild-tech/migratex-examples" rel="noopener noreferrer"&gt;migratex-examples&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://cobuildx.ai/migratex" rel="noopener noreferrer"&gt;cobuildx.ai/migratex&lt;/a&gt; · &lt;a href="https://migratex.cobuildx.ai" rel="noopener noreferrer"&gt;Try the platform&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>migration</category>
    </item>
    <item>
      <title>Modernizing Legacy Apps with RxJS: Streams as the Bridge Between Old and New</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Mon, 12 Oct 2026 05:32:08 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/modernizing-legacy-apps-with-rxjs-streams-as-the-bridge-between-old-and-new-20c9</link>
      <guid>https://dev.to/cobuildx-ai/modernizing-legacy-apps-with-rxjs-streams-as-the-bridge-between-old-and-new-20c9</guid>
      <description>&lt;p&gt;&lt;em&gt;In most legacy migrations, the difficult part is the async code, not the templates: callbacks, observers, global events and timers that every screen depends on. This post explains how we use RxJS as a bridge, so the old UI and the new React UI share the same streams while screens are moved one by one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In most legacy frontend migrations, the templates are not the hard part. Templates can be ported quite easily. The hard part is the &lt;strong&gt;async code&lt;/strong&gt;. A jQuery handler sends a request, sets a global flag and fires a custom event. A Backbone model has three views listening to its &lt;code&gt;change&lt;/code&gt; event. An Ember observer saves data on every keystroke. A &lt;code&gt;setInterval&lt;/code&gt; keeps polling the server, and nobody in the team knows why it was added.&lt;/p&gt;

&lt;p&gt;This is where the actual behavior of a legacy app lives, and it is spread across every page. We cannot move it one route at a time, because both the old screens and the new screens depend on it.&lt;/p&gt;

&lt;p&gt;In such projects, we use &lt;strong&gt;RxJS&lt;/strong&gt; as a &lt;strong&gt;bridge&lt;/strong&gt; between the old app and the new one. We do not use it as the final architecture, and we do not use it just because Angular uses it. The approach is simple. Wrap each legacy event source in an Observable and keep the shared state behind a stream. Now the old UI and the new React UI can subscribe to the same data while screens are moved one by one. As a bonus, a lot of hand-written debounce, retry and cancel logic becomes a few lines of operators. This is the &lt;a href="https://cobuildx.ai/blog/frontend-migration-strategies" rel="noopener noreferrer"&gt;branch by abstraction&lt;/a&gt; pattern, with streams as the interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Legacy pattern&lt;/th&gt;
&lt;th&gt;What goes wrong&lt;/th&gt;
&lt;th&gt;RxJS replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Callbacks and &lt;code&gt;$.Deferred&lt;/code&gt; chains&lt;/td&gt;
&lt;td&gt;Deeply nested, cannot be cancelled, errors get lost&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;from(promise)&lt;/code&gt;, &lt;code&gt;defer&lt;/code&gt;, &lt;code&gt;switchMap&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global or custom events (&lt;code&gt;$(document).trigger&lt;/code&gt;, Backbone &lt;code&gt;on&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Any code can fire them, and nobody removes the listeners&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fromEvent&lt;/code&gt;, &lt;code&gt;fromEventPattern&lt;/code&gt;, typed &lt;code&gt;Subject&lt;/code&gt;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observers and watchers (Ember &lt;code&gt;observer&lt;/code&gt;, &lt;code&gt;$scope.$watch&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Order is not clear, and they run too often&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;BehaviorSubject&lt;/code&gt; + &lt;code&gt;distinctUntilChanged&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hand-rolled debounce and timers&lt;/td&gt;
&lt;td&gt;Old responses overwrite new ones, timers keep running&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;debounceTime&lt;/code&gt;, &lt;code&gt;switchMap&lt;/code&gt;, &lt;code&gt;takeUntil&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polling with &lt;code&gt;setInterval&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Requests overlap, and polling never stops&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;timer&lt;/code&gt; + &lt;code&gt;switchMap&lt;/code&gt; / &lt;code&gt;exhaustMap&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry loops&lt;/td&gt;
&lt;td&gt;Too many retries, no delay between them&lt;/td&gt;
&lt;td&gt;&lt;code&gt;retry({ count, delay })&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSockets with manual reconnects&lt;/td&gt;
&lt;td&gt;Duplicate connections, lost messages&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;webSocket&lt;/code&gt; + &lt;code&gt;retry&lt;/code&gt; + &lt;code&gt;share&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every row follows the same idea: &lt;strong&gt;treat "something that happens over time" as a value that we can pass around, combine and unsubscribe from.&lt;/strong&gt; Once both apps use Observables, the framework on each side becomes much less important.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why RxJS Works Well for Modernization
&lt;/h2&gt;

&lt;p&gt;There are many state management libraries. For a migration, RxJS is useful for three specific reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It does not depend on any framework.&lt;/strong&gt; An Observable works the same way whether jQuery, Backbone, Ember, AngularJS or React subscribes to it. This is exactly what we need during a migration, when the old and new code run on the same page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One model for every kind of async.&lt;/strong&gt; DOM events, custom events, promises, timers, WebSockets and Node-style callbacks all become the same type. There is no need to write a different pattern for each source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancellation and cleanup are built in.&lt;/strong&gt; Every subscription can be closed. Legacy apps often leave handlers and timers running, and many of these leaks are bugs that users have simply got used to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also one clear reason to be careful: &lt;strong&gt;RxJS takes time to learn&lt;/strong&gt;, and a codebase where everything is a stream is difficult to read. We will discuss where to stop later in this post. In short, use it at the boundaries and for real event streams, not for every value in the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Wrap the Legacy Event Sources
&lt;/h2&gt;

&lt;p&gt;First, find every place where the old app produces events or async results, and create an Observable wrapper for each one. Do not change the legacy code at this stage. The wrapper only listens.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb6s9s85kbana56piym44.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb6s9s85kbana56piym44.png" alt="Legacy event sources on the left: jQuery events, Backbone models, Ember observers, AngularJS watchers, promises and callbacks, timers, WebSockets. Each passes through an RxJS wrapper such as fromEvent, fromEventPattern, from or webSocket into a layer of shared Observable services. Both the legacy UI and the new React UI subscribe to that layer" width="800" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DOM and jQuery events.&lt;/strong&gt; &lt;code&gt;fromEvent&lt;/code&gt; accepts DOM elements and any object with jQuery-style &lt;code&gt;on&lt;/code&gt;/&lt;code&gt;off&lt;/code&gt; methods, so a jQuery object can be passed directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fromEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The legacy app fires: $(document).trigger('cart:updated', [cart])&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;legacyCartUpdated$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fromEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart:updated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchInput$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fromEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Backbone, Ember and anything with its own add/remove API.&lt;/strong&gt; &lt;code&gt;fromEventPattern&lt;/code&gt; takes an add handler and a remove handler. This way, the Observable removes its listener from the legacy object properly when it is unsubscribed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fromEventPattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startWith&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Backbone model or collection&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromBackbone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Backbone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;fromEventPattern&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Backbone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;startWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toJSON&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Ember object property (Ember.addObserver / removeObserver)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromEmberProperty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;fromEventPattern&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;addObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;removeObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;startWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two points are important here. First, &lt;strong&gt;convert to plain objects at the boundary&lt;/strong&gt; (&lt;code&gt;toJSON()&lt;/code&gt;, &lt;code&gt;get&lt;/code&gt;). If the stream emits Backbone models or Ember records, the new code will also start calling &lt;code&gt;.get('title')&lt;/code&gt;, and the legacy model will spread into React. This is the &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer" rel="noopener noreferrer"&gt;anti-corruption layer&lt;/a&gt; from our strategies post, applied to streams. Second, &lt;code&gt;startWith&lt;/code&gt; gives the current value to late subscribers. So a React component that mounts later will not show an empty screen while it waits for the next change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Promises, callbacks and sockets&lt;/strong&gt; also have direct wrappers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bindNodeCallback&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;webSocket&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs/webSocket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// defer: the request only starts when someone subscribes, and runs again per subscriber&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadOrders$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;legacyApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Node-style callback API from an old SDK&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;readConfig$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bindNodeCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;legacySdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prices$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;webSocket&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PriceTick&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wss://example.com/prices&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For requests, prefer &lt;code&gt;defer(() =&amp;gt; promise)&lt;/code&gt; over &lt;code&gt;from(promise)&lt;/code&gt;. A promise starts running as soon as it is created. &lt;code&gt;defer&lt;/code&gt; waits until someone subscribes, and this is what makes cancellation and retries possible later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Put Shared State Behind a Stream
&lt;/h2&gt;

&lt;p&gt;Wrapping the sources makes the old app observable. The next step is to give each piece of &lt;strong&gt;shared state&lt;/strong&gt; (the cart, the current user, the selected account, feature flags) a single owner. This owner is a small service that holds the value and exposes it as an Observable. Both apps read from this service, and all updates go through its methods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;distinctUntilChanged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Cart&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CartItem&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="nl"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Cart&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cartService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="na"&gt;$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asObservable&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="na"&gt;$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;distinctUntilChanged&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CartItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Keep the legacy event working for screens that still listen to it&lt;/span&gt;
&lt;span class="nx"&gt;cartService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cart$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart:updated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flhfjbusqublef9krghvv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flhfjbusqublef9krghvv.png" alt="A cart service in the middle holds a BehaviorSubject and exposes cart$ and an add method. On the left, a legacy jQuery header subscribes to cart$ and calls add from a click handler. On the right, a new React checkout page reads cart$ through a small useStream hook and calls add from a button. Both stay in sync because there is only one source of truth" width="800" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The legacy side subscribes and updates the DOM the same way it does today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// legacy/header.js (jQuery)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cartService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#cart-badge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#add-to-cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cartService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;readItemFromForm&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

&lt;span class="c1"&gt;// Backbone view: tie the subscription to the view's lifetime&lt;/span&gt;
&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cartService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cart$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nx"&gt;Backbone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;View&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React reads the same stream through a small hook. &lt;code&gt;useSyncExternalStore&lt;/code&gt; is the official React API for subscribing to an external store. It also avoids the "tearing" issue that a &lt;code&gt;useEffect&lt;/code&gt; + &lt;code&gt;useState&lt;/code&gt; version can have during concurrent rendering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSyncExternalStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Subscribe to a stream that always has a current value (like a service backed by a BehaviorSubject)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useStream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useSyncExternalStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stream$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// In a component&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CheckoutSummary&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cartService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cart$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cartService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;formatMoney&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the complete seam. A screen can move from the old app to React, and the cart badge in the legacy header will still update, because neither side owns the cart. Once the last legacy subscriber is removed, the service internals can be replaced (with Zustand, TanStack Query or plain React context) without changing any component, because components only depend on &lt;code&gt;cart$&lt;/code&gt; and &lt;code&gt;add&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issues we faced&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exposing the Subject directly.&lt;/strong&gt; If the service exports &lt;code&gt;state&lt;/code&gt; instead of &lt;code&gt;state.asObservable()&lt;/code&gt;, any file can call &lt;code&gt;.next()&lt;/code&gt;, and we are back to global events. Expose streams for reading and methods for writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changing emitted objects in place.&lt;/strong&gt; &lt;code&gt;state.value.items.push(item)&lt;/code&gt; changes the object that both apps hold, but nothing is emitted. Always emit a new object, because &lt;code&gt;useSyncExternalStore&lt;/code&gt; depends on reference changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updates inside updates.&lt;/strong&gt; A legacy handler that calls &lt;code&gt;add()&lt;/code&gt; while handling a &lt;code&gt;cart$&lt;/code&gt; emission can create a loop. Using &lt;code&gt;distinctUntilChanged&lt;/code&gt; on derived streams, and keeping the legacy re-trigger one-way, prevents most of these cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loops with the legacy event.&lt;/strong&gt; If the legacy app both fires and listens to &lt;code&gt;cart:updated&lt;/code&gt;, either wrap it with &lt;code&gt;fromEvent&lt;/code&gt; &lt;em&gt;or&lt;/em&gt; re-trigger it from the service. Do not do both.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Replace Hand-Written Async Code with Operators
&lt;/h2&gt;

&lt;p&gt;Once events become streams, a lot of fragile legacy code can be rewritten in a few lines, and it usually becomes more correct too. A search box is the common example. Most legacy apps have some version of this jQuery code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastQuery&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;val&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;lastQuery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;lastQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renderResults&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// a slow early response can overwrite a newer one&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks fine, but it has a race condition. If the request for "rea" is slow and the request for "react" is fast, the old results arrive last and replace the correct ones. The RxJS version avoids this by design, because &lt;code&gt;switchMap&lt;/code&gt; unsubscribes from the previous request as soon as a new query comes in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;debounceTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;distinctUntilChanged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;switchMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;catchError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ajax&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs/ajax&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchInput$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;debounceTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;distinctUntilChanged&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;switchMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;ajax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getJSON&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nf"&gt;catchError&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;([])),&lt;/span&gt; &lt;span class="c1"&gt;// one failed search shouldn't kill the stream&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flmfkctbjqouxc9b2d208.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flmfkctbjqouxc9b2d208.png" alt="Marble diagram of a search box. Keystrokes r, re, rea, react on the input line. After debounceTime, only rea and react pass. switchMap starts a request for rea, then cancels it when react arrives, so only the results for react reach the screen. The hand-rolled version below lets the slow rea response arrive last and overwrite the correct results" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ajax&lt;/code&gt; from &lt;code&gt;rxjs/ajax&lt;/code&gt; cancels the actual HTTP request when it is unsubscribed. With &lt;code&gt;fetch&lt;/code&gt;, pass an &lt;code&gt;AbortSignal&lt;/code&gt; inside &lt;code&gt;new Observable&lt;/code&gt; (or use &lt;code&gt;fromFetch&lt;/code&gt; from &lt;code&gt;rxjs/fetch&lt;/code&gt;) to get the same behavior.&lt;/p&gt;

&lt;p&gt;The same small set of operators covers most of the other hand-written patterns we see in legacy apps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Operator&lt;/th&gt;
&lt;th&gt;Why it is better than the legacy version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Only the latest request matters (search, filters, route params)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;switchMap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cancels old requests, so results never arrive out of order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ignore clicks while a save is in flight&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exhaustMap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replaces &lt;code&gt;isSaving&lt;/code&gt; flags and disabled-button workarounds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every action must run, in order (queued writes)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;concatMap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writes do not mix, and no update is lost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poll until the user leaves the page&lt;/td&gt;
&lt;td&gt;&lt;code&gt;timer(0, 5000).pipe(switchMap(load), takeUntil(leave$))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Polls never overlap, and polling stops reliably&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry flaky calls with backoff&lt;/td&gt;
&lt;td&gt;&lt;code&gt;retry({ count: 3, delay: (_, i) =&amp;gt; timer(2 ** i * 500) })&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Limited attempts with a growing delay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Combine several sources&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;combineLatest&lt;/code&gt;, &lt;code&gt;withLatestFrom&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Replaces flags like &lt;code&gt;if (userLoaded &amp;amp;&amp;amp; settingsLoaded)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time out a slow call&lt;/td&gt;
&lt;td&gt;&lt;code&gt;timeout({ first: 8000 })&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Configured in one place, and errors are reported properly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Polling needs one more example, because legacy polling code often causes duplicate requests and battery drain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;switchMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;takeUntil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;share&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fromEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hidden$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fromEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jobStatus$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;switchMap&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;defer&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
    &lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;takeUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hidden$&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;share&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// several widgets can watch the same job with one poller&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Leaked subscriptions
&lt;/h3&gt;

&lt;p&gt;Every &lt;code&gt;subscribe()&lt;/code&gt; must be closed at some point. In legacy code, connect it to the lifecycle that already exists: a Backbone view's &lt;code&gt;remove&lt;/code&gt;, an Ember component's &lt;code&gt;willDestroy&lt;/code&gt;, an AngularJS &lt;code&gt;$scope.$on('$destroy')&lt;/code&gt;, or a jQuery plugin's &lt;code&gt;destroy&lt;/code&gt;. In React, the hook's cleanup takes care of it. For streams that should end on an event, keep &lt;code&gt;takeUntil(destroy$)&lt;/code&gt; as the &lt;strong&gt;last&lt;/strong&gt; operator in the pipe, so that nothing after it can subscribe again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hot and cold Observables
&lt;/h3&gt;

&lt;p&gt;A cold Observable (like &lt;code&gt;defer(() =&amp;gt; fetch(...))&lt;/code&gt;) runs again for every subscriber. If three components subscribe to the same stream, three requests are made. When a result should be shared, add &lt;code&gt;share()&lt;/code&gt; or &lt;code&gt;shareReplay({ bufferSize: 1, refCount: true })&lt;/code&gt;. Use &lt;code&gt;refCount: true&lt;/code&gt; unless the source really needs to run forever. Without it, &lt;code&gt;shareReplay&lt;/code&gt; keeps the inner subscription (and its polling or socket) running even after the last subscriber has left.&lt;/p&gt;

&lt;h3&gt;
  
  
  Turning everything into a stream
&lt;/h3&gt;

&lt;p&gt;A common mistake in RxJS projects is a codebase where a simple boolean flag becomes a &lt;code&gt;BehaviorSubject&amp;lt;boolean&amp;gt;&lt;/code&gt; passed through four operators. &lt;strong&gt;Use streams only where time matters&lt;/strong&gt;: events, sockets, polling, typeahead and state shared between the two apps. For normal server data in React (load a list, show it, refetch on change), &lt;a href="https://tanstack.com/query/latest" rel="noopener noreferrer"&gt;TanStack Query&lt;/a&gt; is simpler and also handles caching. For local component state, use &lt;code&gt;useState&lt;/code&gt;. A simple rule: if you cannot draw a marble diagram for it, it probably should not be an Observable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Errors stop streams
&lt;/h3&gt;

&lt;p&gt;When an error reaches a long-running stream, the stream ends and every subscriber stops getting updates, without any visible message. Catch errors &lt;strong&gt;inside&lt;/strong&gt; the &lt;code&gt;switchMap&lt;/code&gt;/&lt;code&gt;mergeMap&lt;/code&gt; (as shown in the search example) so that the outer stream keeps running, and log every error you catch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bundle size and RxJS versions
&lt;/h3&gt;

&lt;p&gt;RxJS 7 supports tree-shaking, and from version 7.2 the operators are exported directly from &lt;code&gt;rxjs&lt;/code&gt;. So a typical bridge layer adds only a small amount to the bundle. If the legacy app still uses RxJS 5 or 6 (common in older Angular apps), the &lt;a href="https://rxjs.dev/deprecations/breaking-changes" rel="noopener noreferrer"&gt;official migration guide&lt;/a&gt; explains the upgrade to 7. &lt;code&gt;rxjs-compat&lt;/code&gt; was meant only for the 5 to 6 upgrade and should not be used in a new layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Bridge
&lt;/h2&gt;

&lt;p&gt;Streams are easy to test without a browser. &lt;code&gt;TestScheduler&lt;/code&gt; runs time-based operators on virtual time, so a 300 ms debounce does not actually wait 300 ms. Marble strings describe the input and the expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TestScheduler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rxjs/testing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The search stream, with its inputs passed in instead of read from globals&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchPipeline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input$&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;input$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;debounceTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;distinctUntilChanged&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;switchMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scheduler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TestScheduler&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search only queries the last term after a pause&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;cold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expectObservable&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a 100ms b 400ms c|&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;cold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--r|&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`result for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="c1"&gt;// 'rea' is dropped by the debounce; 'react' settles at 401ms, its result arrives 2 frames later&lt;/span&gt;
    &lt;span class="nf"&gt;expectObservable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;searchPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;403ms x 99ms |&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result for react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing the pipeline as a function of its inputs is what makes it testable, and it is a good practice for the seam in any case. For checking that both apps behave the same, use the approach from our &lt;a href="https://cobuildx.ai/blog/jquery-to-react-migration-guide" rel="noopener noreferrer"&gt;migration guides&lt;/a&gt;: one end-to-end test suite that runs the same screen in the old app and in the new app, and checks for the same visible result. The stream layer is what keeps those results the same, so it needs good test coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Temporary Bridge or Long-Term Layer?
&lt;/h2&gt;

&lt;p&gt;At the end of the migration, decide what happens to the RxJS layer. Both options below are fine, as long as the team makes a clear decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Temporary bridge.&lt;/strong&gt; The bridge was added so that old and new code could share state. Once the legacy app is removed, plain React state, TanStack Query or a small store can handle most of it. Remove the Subjects, and keep only the few streams that represent real events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-term layer.&lt;/strong&gt; Apps with a lot of real-time behavior (trading screens, live dashboards using sockets, collaborative editors, IoT consoles) often keep RxJS as the event layer under React. In that case, document the rules: where streams are kept, how errors are handled, and which shared streams use &lt;code&gt;shareReplay&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faklt9chrr7c2wc97u2jm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faklt9chrr7c2wc97u2jm.png" alt="Checklist for retiring the RxJS bridge. Every legacy subscriber removed. Legacy event re-triggers deleted. Each service's Subject either replaced by a React-native store or kept and documented as a real event stream. No fromEventPattern wrappers left pointing at legacy objects. Marble tests moved to the new implementation or deleted with it" width="800" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Whichever option you choose, delete the &lt;code&gt;fromEventPattern&lt;/code&gt; wrappers around legacy objects in the same sprint as the last legacy screen. These wrappers are always temporary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further in This Series
&lt;/h2&gt;

&lt;p&gt;This post covers one technique within a larger migration plan. The posts below explain the overall plan and each starting framework in more detail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cobuildx.ai/blog/frontend-migration-strategies" rel="noopener noreferrer"&gt;Frontend migration strategies&lt;/a&gt;: strangler fig, branch by abstraction and vertical slicing, and how they combine.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cobuildx.ai/blog/one-way-two-way-binding" rel="noopener noreferrer"&gt;One-way vs two-way data binding&lt;/a&gt;: why observers and watchers are difficult to migrate, and what replaces them.&lt;/li&gt;
&lt;li&gt;Framework guides: &lt;a href="https://cobuildx.ai/blog/angular-to-react-migration-guide" rel="noopener noreferrer"&gt;Angular&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/ember-to-react-migration-guide" rel="noopener noreferrer"&gt;Ember&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/backbone-to-react-migration-guide" rel="noopener noreferrer"&gt;Backbone&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/jquery-to-react-migration-guide" rel="noopener noreferrer"&gt;jQuery&lt;/a&gt; and &lt;a href="https://cobuildx.ai/blog/vanilla-js-to-react-migration-guide" rel="noopener noreferrer"&gt;vanilla JS&lt;/a&gt; to React.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Our Migration Plugins
&lt;/h2&gt;

&lt;p&gt;Finding every event source, observer and shared service in a legacy app takes the most time, and it decides where the streams should go. We have packaged our approach as free, open-source plugins for Claude Code, one for each starting framework:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;Migrates from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/ember-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;ember-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Ember Classic and Octane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/angular-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;angular-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Angular 2+ and AngularJS 1.x, including hybrid ngUpgrade apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/backbone-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;backbone-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Backbone.js, Marionette, jQuery templates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/jquery-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;jquery-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;jQuery apps, plugins and jQuery UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/vanillajs-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;vanillajs-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Plain HTML, CSS and JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each plugin comes with a migration skill, a &lt;code&gt;/&amp;lt;plugin&amp;gt;:plan&lt;/code&gt; command that writes a migration plan (strategy, units, route map, test plan and risks) without touching your code, a read-only inventory agent that maps the old app, a parity-reviewer agent that compares each migrated piece with its original, and a bundled Context7 server for up-to-date library docs. To install one in Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="nx"&gt;marketplace&lt;/span&gt; &lt;span class="nx"&gt;add&lt;/span&gt; &lt;span class="nx"&gt;cobuild&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;tech&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;cbx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="nx"&gt;backbone&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;react&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;cbx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They also work in Cursor, in VS Code with GitHub Copilot, and in the GitHub Copilot CLI. Progress is kept in a &lt;code&gt;.migration/&lt;/code&gt; folder in your repo, so a migration can carry on across sessions and teammates. The source and setup steps are at &lt;a href="https://github.com/cobuild-tech/cbx-plugins" rel="noopener noreferrer"&gt;cobuild-tech/cbx-plugins&lt;/a&gt;, and issues are welcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  About MigrateX
&lt;/h2&gt;

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

&lt;p&gt;MigrateX is our platform and service for moving software, data and infrastructure from one technology to another. It combines automation with experienced engineers. The automation does the repetitive work quickly, and the engineers make the tricky decisions, like which shared state needs a stream-based seam and which can be moved directly.&lt;/p&gt;

&lt;p&gt;A MigrateX project usually runs like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assessment.&lt;/strong&gt; We map every route, shared service, event source and hidden dependency in your app, so you see where the work is before anyone agrees on a timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan.&lt;/strong&gt; With your team, we choose the strategy, the slice order, and what can simply be deleted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration, step by step.&lt;/strong&gt; Automation handles the repetitive parts. Our engineers handle the router, the seams and the code that connects old and new.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof that it works.&lt;/strong&gt; The same tests run against both versions. A piece only counts as moved when it passes on both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handover.&lt;/strong&gt; We remove the temporary code, the seams and the flags, and leave your team with a codebase they understand and own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your app keeps running and shipping features the whole time, so there's no big launch day. Our &lt;a href="https://github.com/cobuild-tech/migratex-examples" rel="noopener noreferrer"&gt;migratex-examples&lt;/a&gt; repo has small apps in each legacy framework next to their migrated versions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Working on the async code of a legacy app? Share a few details with us through &lt;a href="https://cobuildx.ai/contact" rel="noopener noreferrer"&gt;Contact Us&lt;/a&gt;, and we will start with a free discussion on where the seams should go.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;&lt;strong&gt;RxJS&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RxJS. &lt;em&gt;Overview&lt;/em&gt; and API reference. &lt;a href="https://rxjs.dev/guide/overview" rel="noopener noreferrer"&gt;https://rxjs.dev/guide/overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RxJS. &lt;em&gt;fromEventPattern&lt;/em&gt;. &lt;a href="https://rxjs.dev/api/index/function/fromEventPattern" rel="noopener noreferrer"&gt;https://rxjs.dev/api/index/function/fromEventPattern&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RxJS. &lt;em&gt;Testing RxJS Code with Marble Diagrams&lt;/em&gt;. &lt;a href="https://rxjs.dev/guide/testing/marble-testing" rel="noopener noreferrer"&gt;https://rxjs.dev/guide/testing/marble-testing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RxJS. &lt;em&gt;Breaking Changes in Version 7&lt;/em&gt;. &lt;a href="https://rxjs.dev/deprecations/breaking-changes" rel="noopener noreferrer"&gt;https://rxjs.dev/deprecations/breaking-changes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ben Lesh. &lt;em&gt;Hot vs Cold Observables&lt;/em&gt;. &lt;a href="https://benlesh.medium.com/hot-vs-cold-observables-f8094ed53339" rel="noopener noreferrer"&gt;https://benlesh.medium.com/hot-vs-cold-observables-f8094ed53339&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;React and migration patterns&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React. &lt;em&gt;useSyncExternalStore&lt;/em&gt;. &lt;a href="https://react.dev/reference/react/useSyncExternalStore" rel="noopener noreferrer"&gt;https://react.dev/reference/react/useSyncExternalStore&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Martin Fowler. &lt;em&gt;Branch By Abstraction&lt;/em&gt;. &lt;a href="https://martinfowler.com/bliki/BranchByAbstraction.html" rel="noopener noreferrer"&gt;https://martinfowler.com/bliki/BranchByAbstraction.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft. &lt;em&gt;Anti-corruption Layer pattern&lt;/em&gt;. &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TanStack Query. &lt;em&gt;Overview&lt;/em&gt;. &lt;a href="https://tanstack.com/query/latest/docs/framework/react/overview" rel="noopener noreferrer"&gt;https://tanstack.com/query/latest/docs/framework/react/overview&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Source code&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CobuildX AI. &lt;em&gt;cbx-plugins&lt;/em&gt;: Claude Code plugins for Backbone, Angular, Ember, jQuery and Vanilla JS to React migrations. &lt;a href="https://github.com/cobuild-tech/cbx-plugins" rel="noopener noreferrer"&gt;https://github.com/cobuild-tech/cbx-plugins&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CobuildX AI. &lt;em&gt;migratex-examples&lt;/em&gt;: small jQuery, Backbone, Angular, Ember and Vanilla JS apps and their migrated versions. &lt;a href="https://github.com/cobuild-tech/migratex-examples" rel="noopener noreferrer"&gt;https://github.com/cobuild-tech/migratex-examples&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/modernizing-legacy-apps-with-rxjs" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rxjs</category>
      <category>javascript</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Strangler Fig, Branch by Abstraction, Vertical Slices: Choosing a Frontend Migration Strategy</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Mon, 12 Oct 2026 05:31:32 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/strangler-fig-branch-by-abstraction-vertical-slices-choosing-a-frontend-migration-strategy-39kp</link>
      <guid>https://dev.to/cobuildx-ai/strangler-fig-branch-by-abstraction-vertical-slices-choosing-a-frontend-migration-strategy-39kp</guid>
      <description>&lt;p&gt;&lt;em&gt;Strangler fig, branch by abstraction, vertical slicing: the strategy decides whether a frontend migration ships, more than the framework you move to. Here's how each one works, where each one bites, and how we combine them on real projects.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Teams that plan a frontend migration spend weeks on the target: React or Vue, Next.js or Vite, Redux or TanStack Query. Then they spend almost no time on the part that decides whether it ships: &lt;strong&gt;how the old app and the new app will live together until the old one is gone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That part is the migration strategy. Pick the wrong one and a good framework choice still turns into an 18-month rewrite branch that never merges. Pick the right one and even a messy legacy app moves over a piece at a time, with users never noticing a launch day.&lt;/p&gt;

&lt;p&gt;This post covers the strategies we actually use: the &lt;strong&gt;big-bang rewrite&lt;/strong&gt; (as the baseline), the &lt;strong&gt;strangler fig&lt;/strong&gt;, &lt;strong&gt;branch by abstraction&lt;/strong&gt;, and &lt;strong&gt;vertical slicing&lt;/strong&gt;, plus the supporting tactics that make them safe: parallel runs, feature flags, micro-frontends, anti-corruption layers and expand-contract. Then it shows how they fit together, because on a real project you rarely use just one. Our framework-specific guides (&lt;a href="https://cobuildx.ai/blog/angular-to-react-migration-guide" rel="noopener noreferrer"&gt;Angular&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/ember-to-react-migration-guide" rel="noopener noreferrer"&gt;Ember&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/backbone-to-react-migration-guide" rel="noopener noreferrer"&gt;Backbone&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/jquery-to-react-migration-guide" rel="noopener noreferrer"&gt;jQuery&lt;/a&gt;, &lt;a href="https://cobuildx.ai/blog/vanilla-js-to-react-migration-guide" rel="noopener noreferrer"&gt;vanilla JS&lt;/a&gt;) go deeper on each starting point. This one is about the strategy underneath all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Unit of change&lt;/th&gt;
&lt;th&gt;What runs side by side&lt;/th&gt;
&lt;th&gt;Rollback&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Big-bang rewrite&lt;/td&gt;
&lt;td&gt;The whole app&lt;/td&gt;
&lt;td&gt;Nothing in production until launch day&lt;/td&gt;
&lt;td&gt;Hard: the old app has gone stale&lt;/td&gt;
&lt;td&gt;Small or frozen apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strangler fig&lt;/td&gt;
&lt;td&gt;A route or a screen&lt;/td&gt;
&lt;td&gt;Old and new apps behind one router&lt;/td&gt;
&lt;td&gt;Point the route back at the old app&lt;/td&gt;
&lt;td&gt;Apps with clear routes or pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branch by abstraction&lt;/td&gt;
&lt;td&gt;A shared dependency behind a seam&lt;/td&gt;
&lt;td&gt;Old and new implementations behind one interface&lt;/td&gt;
&lt;td&gt;Flip the switch back&lt;/td&gt;
&lt;td&gt;Services, stores and widgets used everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vertical slicing&lt;/td&gt;
&lt;td&gt;One user-facing feature, top to bottom&lt;/td&gt;
&lt;td&gt;Migrated and unmigrated features&lt;/td&gt;
&lt;td&gt;Per slice&lt;/td&gt;
&lt;td&gt;Choosing the order of work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first row is the one people start with. The other three are what actually works, and they answer different questions. &lt;strong&gt;Strangler fig decides where the line between old and new runs. Branch by abstraction handles the things that cross that line. Vertical slicing decides what to move next.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F68byzglixxoy7j0cr11g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F68byzglixxoy7j0cr11g.png" alt="Four strategies side by side. Big-bang: the old app runs until a single cut-over day. Strangler fig: a router sends some routes to the new app and the rest to the old one. Branch by abstraction: callers talk to an interface with an old and a new implementation behind it. Vertical slicing: one feature is moved through UI, state, API and tests at once" width="800" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Big-Bang Rewrite: the Baseline
&lt;/h2&gt;

&lt;p&gt;The big-bang rewrite is the default plan nobody writes down: start a new repo, rebuild everything, switch over on a chosen day. It's worth understanding properly, because every other strategy exists to avoid its failure modes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it is actually right&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The app is small.&lt;/strong&gt; A few screens that one or two people can rebuild in a few weeks. The overhead of running two apps side by side costs more than the rewrite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The app is frozen.&lt;/strong&gt; No new features are planned, so the old app won't move while you rebuild it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The old app can't host anything new.&lt;/strong&gt; Some legacy stacks make it so painful to mount new code that coexistence costs more than starting clean. This is rarer than it seems. Almost anything can serve an iframe or hand off a route.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why it usually fails&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The target keeps moving.&lt;/strong&gt; The business doesn't stop for your rewrite. Every feature shipped in the old app is one more thing the new one has to catch up on, and the gap rarely closes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing is proven until the end.&lt;/strong&gt; Months of work with zero real users means every wrong assumption surfaces on launch day, all at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The old app holds knowledge nobody wrote down.&lt;/strong&gt; Ten years of bug fixes are hidden in it. Joel Spolsky called rewriting from scratch &lt;a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/" rel="noopener noreferrer"&gt;the single worst strategic mistake&lt;/a&gt; a software company can make, for exactly this reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback is fiction.&lt;/strong&gt; By launch day the old app is months behind. "Roll back" means going back to software everyone has stopped maintaining.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The incremental strategies below all share one idea: &lt;strong&gt;get the new code in front of real users early, in small pieces, while the old app keeps running.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Strangler Fig
&lt;/h2&gt;

&lt;p&gt;Martin Fowler &lt;a href="https://martinfowler.com/bliki/StranglerFigApplication.html" rel="noopener noreferrer"&gt;named this pattern&lt;/a&gt; after a vine that grows around a host tree until the tree is gone and the vine stands on its own. In a frontend migration, the vine is the new app and the tree is the old one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Put something in front of both apps.&lt;/strong&gt; A reverse proxy, a CDN rule, or a thin shell app that owns the router. Every request goes through it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send every route to the old app at first.&lt;/strong&gt; Nothing has changed for users, but the switch now exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move one route.&lt;/strong&gt; Build it in the new app and flip that one path in the router.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeat&lt;/strong&gt; until the old app serves nothing. Then switch it off.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The routing layer can be as simple as a list of migrated paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// shell/router.ts: which app owns which path&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MIGRATED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/settings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/profile/:username&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/article/:slug&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ownerOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;legacy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;MIGRATED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;matchPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;legacy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production this is often a CDN or proxy rule instead of code, and each route can sit behind a feature flag so you can roll it out to 5% of users first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Route-level or widget-level&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are two grains to strangle at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Route-level.&lt;/strong&gt; Whole pages move at once. It's the cleanest option: each page is owned by one framework, and the boundary is a URL. Start here if your app has real routes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Widget-level.&lt;/strong&gt; New components are mounted inside old pages (a React date picker inside an Ember template, for example). This is how you migrate apps with one giant page, or a shared header that appears everywhere. It's more fragile, because the two frameworks now share a DOM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most projects use both: widget-level for the shared shell (nav, header, notifications), route-level for everything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it shines&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real users see real progress within weeks.&lt;/strong&gt; Every migrated route is in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback is one line.&lt;/strong&gt; Point the route back at the old app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature work never stops.&lt;/strong&gt; New features go straight into the new app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The end is visible.&lt;/strong&gt; "34 of 52 routes migrated" is a progress bar a manager can read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where it costs you&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two runtimes at once.&lt;/strong&gt; Users may download both frameworks for a while. Keep the old bundle off migrated pages where you can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared state across the boundary.&lt;/strong&gt; Login, the current user, feature flags and unsaved drafts have to be readable by both apps. Put them somewhere neutral: a cookie, &lt;code&gt;localStorage&lt;/code&gt;, or the API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS bleed.&lt;/strong&gt; Global styles from one app leak into the other. Scope the new app's styles (CSS Modules, a prefix, or Shadow DOM) before you mount anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Navigation between apps.&lt;/strong&gt; Crossing from a React page to an Ember page is a full page load unless the shell handles it. Users notice this, so group related routes and move them together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Gotchas we hit&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The router is the hard part, not the pages.&lt;/strong&gt; Agree on how deep links, redirects, 404s and query strings behave across both apps before moving a single route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth needs to move first.&lt;/strong&gt; If the new app can't read the old app's session, every migrated page looks logged out. A shared cookie is the usual fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't strangle by file.&lt;/strong&gt; Moving "all the components in &lt;code&gt;/shared&lt;/code&gt;" isn't strangling, it's a horizontal layer (see vertical slicing below). Strangle by what users see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete as you go.&lt;/strong&gt; Once a route is moved and stable, remove its old code. Otherwise the old app never shrinks and you end up maintaining both forever.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://cobuildx.ai/blog/angular-to-react-migration-guide" rel="noopener noreferrer"&gt;Angular guide&lt;/a&gt; walks through a strangler fig migration end to end, including the shell app and the staged rollout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Branch by Abstraction
&lt;/h2&gt;

&lt;p&gt;Strangler fig works when the thing you're replacing has a clear boundary, like a URL. But some parts of a frontend don't have one. The data layer, the auth service, the API client, the store, a date library: they're used from every page. You can't route around them. &lt;strong&gt;Branch by abstraction&lt;/strong&gt; is the pattern for those.&lt;/p&gt;

&lt;p&gt;The name comes from Paul Hammant and is described by &lt;a href="https://martinfowler.com/bliki/BranchByAbstraction.html" rel="noopener noreferrer"&gt;Fowler&lt;/a&gt; and &lt;a href="https://trunkbaseddevelopment.com/branch-by-abstraction/" rel="noopener noreferrer"&gt;trunkbaseddevelopment.com&lt;/a&gt;. The idea: instead of making a long-lived version-control branch to replace something, you make the "branch" in the code itself, behind an interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add a seam.&lt;/strong&gt; Write an interface that describes what callers need. Make the old implementation fit it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move callers onto the seam.&lt;/strong&gt; One at a time, change code that talks to the old thing directly so it talks to the interface instead. Ship each change. Nothing behaves differently yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the new implementation&lt;/strong&gt; behind the same interface, in the main branch, switched off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch over.&lt;/strong&gt; A flag picks the new implementation, first for internal users, then for everyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete the old implementation&lt;/strong&gt;, and the flag. If the seam isn't useful anymore, delete it too.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7grlce5rhk3i85n27vjr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7grlce5rhk3i85n27vjr.png" alt="Branch by abstraction in four steps. One: callers use the old implementation directly. Two: callers use an interface, still backed by the old implementation. Three: a new implementation is built behind the same interface and a flag picks between them. Four: the old implementation and flag are deleted" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what that looks like for a data layer. Say pages load articles through an Ember Data store or Backbone models, and you're replacing that with a plain API client and TanStack Query. The seam is a small repository interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// articles/repository.ts: the seam&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ArticleRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ArticleFilter&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ArticleInput&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// old: wraps the legacy store, converting its records to plain objects&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;legacyArticles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ArticleRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;toArticle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queryRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toArticle&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;toArticle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// new: talks to the API directly&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiArticles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ArticleRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/articles/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;article&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ArticleRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;articles-api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;apiArticles&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;legacyArticles&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details make this work. First, &lt;strong&gt;the interface speaks plain objects, not framework records.&lt;/strong&gt; If callers receive Ember Data models, they'll keep calling &lt;code&gt;.get('title')&lt;/code&gt; and you haven't built a seam, just a wrapper. Second, &lt;strong&gt;both implementations live in the main branch&lt;/strong&gt; and are tested in CI. The "branch" is a flag, not a Git branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where you'll use it in a frontend&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The data layer.&lt;/strong&gt; Ember Data, Backbone collections, Angular services with their own caches → a query library. This is the most common use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth and session.&lt;/strong&gt; One &lt;code&gt;session&lt;/code&gt; interface that both the old and new apps call, so login works on both sides of the strangler boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A shared component.&lt;/strong&gt; The date picker, the rich text editor, the data grid. Wrap it in one component API, then swap the implementation underneath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A library replacement&lt;/strong&gt; that isn't a framework change at all: moment.js → date-fns, a charting library, an HTTP client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why not just use a long-lived branch?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because a migration branch that lives for months is a big-bang rewrite in disguise. It drifts from main every day, the merge at the end is huge, and nothing in it has met a real user. Branch by abstraction keeps every step small and merged. It pairs naturally with &lt;a href="https://trunkbaseddevelopment.com/" rel="noopener noreferrer"&gt;trunk-based development&lt;/a&gt; and with &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;feature toggles&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost:&lt;/strong&gt; extra code for a while (the interface, two implementations and a flag), and the discipline to actually do step five. Seams and flags that outlive the migration become their own kind of legacy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vertical Slicing
&lt;/h2&gt;

&lt;p&gt;Strangler fig and branch by abstraction tell you &lt;em&gt;how&lt;/em&gt; to keep old and new running together. &lt;strong&gt;Vertical slicing tells you what to move next.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A vertical slice is one user-facing feature moved &lt;strong&gt;top to bottom&lt;/strong&gt;: its UI, its state, its data access, its routes and its tests, all together, until a user can do that thing entirely in the new code. The opposite is a horizontal layer: "first we'll port all the models, then all the services, then all the components".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3cgvea344g4w4hfn9m62.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3cgvea344g4w4hfn9m62.png" alt="Grid of features (login, feed, article, editor, settings) against layers (UI, state, data access, tests). A horizontal plan fills one whole layer at a time and no feature is finished until the end. A vertical plan fills one whole feature column at a time, and each finished column ships" width="799" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why horizontal layers fail&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nothing ships until the last layer is done.&lt;/strong&gt; Porting every model first gives you months of code with no user, which is the big-bang problem again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You guess at requirements.&lt;/strong&gt; Without a real screen using it, a ported service is shaped by what the old code did, not what the new UI needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progress is invisible.&lt;/strong&gt; "80% of the services are done" means nothing to the people paying for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Choosing the first slice&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first slice is mostly about proving the plumbing: routing between apps, shared auth, the build and deploy, the test setup. So pick one that is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real, but low-risk.&lt;/strong&gt; A settings page or a profile page, not checkout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Light on shared dependencies.&lt;/strong&gt; It shouldn't need half the app's services to render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to compare.&lt;/strong&gt; It has clear behavior you can test old against new.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Used often enough&lt;/strong&gt; that you'll hear quickly if something's wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the first one, order the rest by &lt;strong&gt;value against coupling.&lt;/strong&gt; Move high-value, loosely coupled features early. Leave the tangled core, the screen that touches everything, until the seams around it already exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How slices map onto the other strategies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a well-run migration, a vertical slice usually &lt;strong&gt;is&lt;/strong&gt; a strangler fig step: a feature is a set of routes, and moving the feature means flipping those routes. When a slice hits a dependency it shares with unmigrated features (the article store used by both the feed and the editor), that's where a branch-by-abstraction seam goes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supporting Tactics
&lt;/h2&gt;

&lt;p&gt;These aren't migration strategies on their own. They're tools that make the three above safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parallel run and dark launching
&lt;/h3&gt;

&lt;p&gt;Run the new code alongside the old one in production, &lt;strong&gt;compare the results, but only show the old one.&lt;/strong&gt; GitHub's &lt;a href="https://github.com/github/scientist" rel="noopener noreferrer"&gt;Scientist&lt;/a&gt; library made this famous for backend code. In a frontend it's useful for a new data layer or a new calculation (pricing, permissions, validation): call both implementations behind the seam, log any differences, return the old result. Once the differences hit zero, switch. &lt;a href="https://martinfowler.com/bliki/DarkLaunching.html" rel="noopener noreferrer"&gt;Dark launching&lt;/a&gt; is the same idea for whole features: ship them to production hidden, and turn them on later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// compare old and new behind the seam; users still get the old result&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;oldResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newResult&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;legacyArticles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;apiArticles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nf"&gt;reportDiff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;articles.get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;oldResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newResult&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;oldResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;oldResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the cost: a parallel run doubles API calls. Sample a percentage of traffic, and never run both sides of a write.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feature flags and canary releases
&lt;/h3&gt;

&lt;p&gt;Every switch in this post (a migrated route, a new implementation behind a seam) should be a flag you can flip without a deploy. That lets you release to staff first, then 5% of users, then everyone, and roll back in seconds. Name each flag after the migration step and give it an expiry date. The &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;feature toggles article&lt;/a&gt; on Fowler's site is the best guide to keeping them under control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Micro-frontends as a way to coexist
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://martinfowler.com/articles/micro-frontends.html" rel="noopener noreferrer"&gt;Micro-frontends&lt;/a&gt; (module federation, single-spa, web components) let several apps share one page. They're a useful &lt;strong&gt;vehicle&lt;/strong&gt; for widget-level strangling: the old app becomes one micro-frontend, the new one another. But think twice before making them the &lt;strong&gt;destination&lt;/strong&gt;. If the goal is one React app, use the micro-frontend setup as scaffolding and take it down when the migration ends. Otherwise you've swapped one legacy app for a distributed system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anti-corruption layer
&lt;/h3&gt;

&lt;p&gt;An &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer" rel="noopener noreferrer"&gt;anti-corruption layer&lt;/a&gt; translates between the old model and the new one so the old model's quirks don't leak into new code. The &lt;code&gt;toArticle()&lt;/code&gt; function in the branch-by-abstraction example is a tiny one: it turns a legacy record (with its computed properties, observers and odd field names) into a plain object the new code can trust. Keep these at the boundary and delete them when the old side is gone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expand and contract
&lt;/h3&gt;

&lt;p&gt;Also called &lt;a href="https://martinfowler.com/bliki/ParallelChange.html" rel="noopener noreferrer"&gt;parallel change&lt;/a&gt;. When old and new code share data (an API response, a &lt;code&gt;localStorage&lt;/code&gt; key, a URL format, a cookie), you can't change its shape in one go. &lt;strong&gt;Expand&lt;/strong&gt; first: support both the old and new shapes. &lt;strong&gt;Migrate&lt;/strong&gt; each reader and writer. &lt;strong&gt;Contract&lt;/strong&gt;: remove the old shape once nothing uses it. It's how you rename a query parameter or restructure saved user preferences without breaking the half of the app that hasn't moved yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining Them: the Playbook We Use
&lt;/h2&gt;

&lt;p&gt;On a real project these aren't alternatives. They are layers of one plan:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Answered by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where does the line between old and new run?&lt;/td&gt;
&lt;td&gt;Strangler fig (routes, then widgets)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What do we move next?&lt;/td&gt;
&lt;td&gt;Vertical slicing (value against coupling)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What about things every page depends on?&lt;/td&gt;
&lt;td&gt;Branch by abstraction (a seam, two implementations, a flag)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do we know the new one behaves the same?&lt;/td&gt;
&lt;td&gt;Parallel run and shared tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do we release and roll back?&lt;/td&gt;
&lt;td&gt;Feature flags and canaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do we change shared data safely?&lt;/td&gt;
&lt;td&gt;Expand and contract&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgha0thibr5uojqtheaj4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgha0thibr5uojqtheaj4.png" alt="Timeline of a combined migration in four phases. Foundation: shell router, shared auth, seams around the data layer. First slice: one low-risk feature moved end to end. Steady state: slices move route by route behind flags while seams switch to new implementations. Removal: the old app, the seams and the flags are deleted" width="800" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Foundation.&lt;/strong&gt; Put the shell or proxy in front of the old app, with every route still pointing at it. Make auth work on both sides. Set up one test suite that can run against either app. Add seams around the data layer and other shared services. Nothing changes for users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First slice.&lt;/strong&gt; Move one low-risk feature end to end. Its real job is to prove the plumbing: routing, auth, deploys, monitoring, rollback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steady state.&lt;/strong&gt; Move slices in order of value against coupling. Each slice is a set of routes flipped behind a flag. Switch seams to their new implementations as soon as their callers allow it, with a parallel run where correctness matters. Ship new features only in the new app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removal.&lt;/strong&gt; When the old app serves no routes, switch it off. Then delete the seams you no longer need, the anti-corruption layers and every migration flag. A migration isn't done until the scaffolding is gone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Choosing a Strategy
&lt;/h2&gt;

&lt;p&gt;If you're deciding where to start, these questions settle most of it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If your app…&lt;/th&gt;
&lt;th&gt;Lean towards&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Is a handful of screens, or is frozen&lt;/td&gt;
&lt;td&gt;Big-bang rewrite. Coexistence isn't worth it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has clear routes and pages&lt;/td&gt;
&lt;td&gt;Strangler fig at route level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is one giant page, or has a shared shell everywhere&lt;/td&gt;
&lt;td&gt;Strangler fig at widget level, often with a micro-frontend host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has a data layer, store or service every page uses&lt;/td&gt;
&lt;td&gt;Branch by abstraction around it, early&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has few tests&lt;/td&gt;
&lt;td&gt;Write tests at the user level (end-to-end) first, then any strategy. Without them, nothing tells you a slice is done.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has many teams shipping in parallel&lt;/td&gt;
&lt;td&gt;Vertical slices owned by one team each, behind flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Must keep shipping features during the migration&lt;/td&gt;
&lt;td&gt;Anything except big-bang&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Measuring Progress and Knowing When You're Done
&lt;/h2&gt;

&lt;p&gt;Incremental migrations have a known failure mode: &lt;strong&gt;they stall at 80%.&lt;/strong&gt; The easy routes are done, the hard core remains, and the team gets pulled onto feature work. You end up running two frameworks forever. Measuring progress openly is the best defense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Routes served by the new app&lt;/strong&gt;, as a percentage of traffic, not just a count. Ten rarely used admin pages don't equal one home feed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lines of legacy code left&lt;/strong&gt;, falling every week. If it stops falling, something is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seams and flags still open.&lt;/strong&gt; Each one is temporary and should have an owner and an end date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parity results.&lt;/strong&gt; For each slice: do the same tests pass against old and new, and does the parallel run show zero differences?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And a short definition of done that every slice has to meet before its old code is deleted: same tests passing on both, flag at 100% for a full release cycle with no rollbacks, no remaining imports of the old code, and the old code removed in the same sprint.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Big-bang rewrites fail on a deadline. Incremental migrations fail on discipline. Pick the failure you can manage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Get new code in front of real users early.&lt;/strong&gt; Every strategy that works is built on this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strangle by what users see,&lt;/strong&gt; route by route, not file by file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put a seam around anything every page depends on,&lt;/strong&gt; and switch it with a flag, not a merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move whole features, not layers.&lt;/strong&gt; A finished slice ships. A finished layer doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete as you go,&lt;/strong&gt; including the scaffolding. The migration ends when the seams and flags are gone, not when the last route moves.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Further in This Series
&lt;/h2&gt;

&lt;p&gt;Each of these takes the strategies above and applies them to one starting point or one design decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cobuildx.ai/blog/angular-to-react-migration-guide" rel="noopener noreferrer"&gt;Moving from Angular to React: A Practical Guide&lt;/a&gt;, with the strangler fig worked end to end.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cobuildx.ai/blog/ember-to-react-migration-guide" rel="noopener noreferrer"&gt;Leaving Ember for React: What Your Conventions Were Hiding&lt;/a&gt;, where Ember Data is a classic branch-by-abstraction candidate.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cobuildx.ai/blog/backbone-to-react-migration-guide" rel="noopener noreferrer"&gt;The Framework Nobody Wrote Down: Taking a Backbone App to React&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cobuildx.ai/blog/jquery-to-react-migration-guide" rel="noopener noreferrer"&gt;Who Updates the Total? Taking a jQuery App to React&lt;/a&gt;, mostly widget-level strangling.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cobuildx.ai/blog/vanilla-js-to-react-migration-guide" rel="noopener noreferrer"&gt;The State Is in the DOM: Taking a Vanilla JavaScript App to React&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cobuildx.ai/blog/client-patterns-mvc-mvvm" rel="noopener noreferrer"&gt;Client Patterns: MVC, MVVM, and What Came After&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cobuildx.ai/blog/one-way-two-way-binding" rel="noopener noreferrer"&gt;One-Way vs Two-Way Binding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cobuildx.ai/blog/csr-vs-ssr" rel="noopener noreferrer"&gt;Client-Side vs Server-Side Rendering&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Our Migration Plugins
&lt;/h2&gt;

&lt;p&gt;Choosing a strategy starts with knowing the app: every route, every shared service, every piece of hidden wiring. That map is slow to build by hand. We've packaged how we do it as free, open-source plugins for Claude Code, one per starting point. Each one plans an incremental, strangler-fig migration by default:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;Starts from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/ember-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;ember-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Ember Classic and Octane&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/angular-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;angular-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Angular 2+ and AngularJS 1.x, including hybrid ngUpgrade apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/backbone-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;backbone-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Backbone.js, Marionette, jQuery templates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/jquery-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;jquery-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;jQuery apps, plugins and jQuery UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cobuild-tech/cbx-plugins/tree/main/vanillajs-to-react" rel="noopener noreferrer"&gt;&lt;code&gt;vanillajs-to-react&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Plain HTML, CSS and JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each plugin comes with a migration skill, a &lt;code&gt;/&amp;lt;plugin&amp;gt;:plan&lt;/code&gt; command that writes a migration plan (strategy, units, route map, test plan and risks) without touching your code, a read-only inventory agent that maps the old app, a parity-reviewer agent that compares each migrated piece with its original, and a bundled Context7 server for up-to-date library docs. To install one in Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="nx"&gt;marketplace&lt;/span&gt; &lt;span class="nx"&gt;add&lt;/span&gt; &lt;span class="nx"&gt;cobuild&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;tech&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;cbx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="nx"&gt;ember&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;react&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;cbx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They also work in Cursor, in VS Code with GitHub Copilot, and in the GitHub Copilot CLI. Progress is kept in a &lt;code&gt;.migration/&lt;/code&gt; folder in your repo, so a migration can carry on across sessions and teammates. The source and setup steps are at &lt;a href="https://github.com/cobuild-tech/cbx-plugins" rel="noopener noreferrer"&gt;cobuild-tech/cbx-plugins&lt;/a&gt;, and issues are welcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  About MigrateX
&lt;/h2&gt;

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

&lt;p&gt;MigrateX is our platform and service for moving software, data and infrastructure from one technology to another. It combines automation with experienced engineers. The automation does the repetitive work quickly, and the engineers make the tricky decisions, like where the strangler boundary runs and which shared services need a seam first.&lt;/p&gt;

&lt;p&gt;A MigrateX project usually runs like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assessment.&lt;/strong&gt; We map every route, shared service and hidden dependency in your app, so you see where the work is before anyone agrees on a timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan.&lt;/strong&gt; With your team, we choose the strategy, the slice order, and what can simply be deleted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration, step by step.&lt;/strong&gt; Automation handles the repetitive parts. Our engineers handle the router, the seams and the code that connects old and new.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof that it works.&lt;/strong&gt; The same tests run against both versions. A piece only counts as moved when it passes on both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handover.&lt;/strong&gt; We remove the temporary code, the seams and the flags, and leave your team with a codebase they understand and own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your app keeps running and shipping features the whole time, so there's no big launch day. Our &lt;a href="https://github.com/cobuild-tech/migratex-examples" rel="noopener noreferrer"&gt;migratex-examples&lt;/a&gt; repo has small apps in each legacy framework next to their migrated versions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Planning a migration and not sure which strategy fits? Tell us a little about your app through &lt;a href="https://cobuildx.ai/contact" rel="noopener noreferrer"&gt;Contact Us&lt;/a&gt;, and we'll start with a free chat about where the work is likely to be.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;&lt;strong&gt;Further reading&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Martin Fowler. &lt;em&gt;Strangler Fig Application&lt;/em&gt;. &lt;a href="https://martinfowler.com/bliki/StranglerFigApplication.html" rel="noopener noreferrer"&gt;https://martinfowler.com/bliki/StranglerFigApplication.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Martin Fowler. &lt;em&gt;Branch By Abstraction&lt;/em&gt;. &lt;a href="https://martinfowler.com/bliki/BranchByAbstraction.html" rel="noopener noreferrer"&gt;https://martinfowler.com/bliki/BranchByAbstraction.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Paul Hammant. &lt;em&gt;Branch by Abstraction&lt;/em&gt;. &lt;a href="https://www.branchbyabstraction.com/" rel="noopener noreferrer"&gt;https://www.branchbyabstraction.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Trunk Based Development. &lt;em&gt;Branch by Abstraction&lt;/em&gt;. &lt;a href="https://trunkbaseddevelopment.com/branch-by-abstraction/" rel="noopener noreferrer"&gt;https://trunkbaseddevelopment.com/branch-by-abstraction/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Danilo Sato. &lt;em&gt;Parallel Change&lt;/em&gt;. &lt;a href="https://martinfowler.com/bliki/ParallelChange.html" rel="noopener noreferrer"&gt;https://martinfowler.com/bliki/ParallelChange.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ian Cartwright, Rob Horn and James Lewis. &lt;em&gt;Patterns of Legacy Displacement&lt;/em&gt;. &lt;a href="https://martinfowler.com/articles/patterns-legacy-displacement/" rel="noopener noreferrer"&gt;https://martinfowler.com/articles/patterns-legacy-displacement/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pete Hodgson. &lt;em&gt;Feature Toggles (aka Feature Flags)&lt;/em&gt;. &lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;https://martinfowler.com/articles/feature-toggles.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cam Jackson. &lt;em&gt;Micro Frontends&lt;/em&gt;. &lt;a href="https://martinfowler.com/articles/micro-frontends.html" rel="noopener noreferrer"&gt;https://martinfowler.com/articles/micro-frontends.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft. &lt;em&gt;Strangler Fig pattern&lt;/em&gt;. &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/strangler-fig" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/architecture/patterns/strangler-fig&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft. &lt;em&gt;Anti-corruption Layer pattern&lt;/em&gt;. &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub. &lt;em&gt;Scientist&lt;/em&gt;: a library for carefully refactoring critical paths. &lt;a href="https://github.com/github/scientist" rel="noopener noreferrer"&gt;https://github.com/github/scientist&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Joel Spolsky. &lt;em&gt;Things You Should Never Do, Part I&lt;/em&gt;. &lt;a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/" rel="noopener noreferrer"&gt;https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sam Newman. &lt;em&gt;Monolith to Microservices&lt;/em&gt;. O'Reilly, 2019. &lt;a href="https://samnewman.io/books/monolith-to-microservices/" rel="noopener noreferrer"&gt;https://samnewman.io/books/monolith-to-microservices/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Source code&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CobuildX AI. &lt;em&gt;cbx-plugins&lt;/em&gt;: Claude Code plugins for Backbone, Angular, Ember, jQuery and Vanilla JS to React migrations. &lt;a href="https://github.com/cobuild-tech/cbx-plugins" rel="noopener noreferrer"&gt;https://github.com/cobuild-tech/cbx-plugins&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CobuildX AI. &lt;em&gt;migratex-examples&lt;/em&gt;: small jQuery, Backbone, Angular, Ember and Vanilla JS apps and their migrated versions. &lt;a href="https://github.com/cobuild-tech/migratex-examples" rel="noopener noreferrer"&gt;https://github.com/cobuild-tech/migratex-examples&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/frontend-migration-strategies" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>frontend</category>
      <category>architecture</category>
      <category>react</category>
    </item>
    <item>
      <title>Jev as a Red-Teaming Guardrail: Putting TypeSafe's Model to Work in AI Security</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Thu, 08 Oct 2026 11:03:01 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/jev-as-a-red-teaming-guardrail-putting-typesafes-model-to-work-in-ai-security-1eno</link>
      <guid>https://dev.to/cobuildx-ai/jev-as-a-red-teaming-guardrail-putting-typesafes-model-to-work-in-ai-security-1eno</guid>
      <description>&lt;p&gt;&lt;em&gt;We ran TypeSafe's Jev as a prompt guardrail on 1,580 red-teaming prompts against GPT-5-mini, Claude Haiku 4.5 and three open-source guard models. It was five to six times faster, 6–27× cheaper, and had the highest block accuracy of the hosted models.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;TypeSafe's Jev promises LLM-grade decisions at a fraction of the time and cost. We put that claim to a practical test: using Jev as a guardrail that screens prompts before they reach an AI agent. Across 1,580 red-teaming prompts from public jailbreak, prompt-injection and harmful-content benchmarks, we compared Jev with OpenAI GPT-5-mini, Claude Haiku 4.5 and three open-source guard models on speed, cost, and how well each one decides what to block and names the risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hype around Jev
&lt;/h2&gt;

&lt;p&gt;TypeSafe launched Jev with this headline [1]:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy9o8q6flpsvpfg8y9t33.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy9o8q6flpsvpfg8y9t33.png" alt="TypeSafe's launch claim: 193.6x faster, 444.6x cheaper" width="800" height="237"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why the hype? Orders of magnitude? Is comparing it to an LLM even fair? Let's dig into where that speed comes from, and whether it holds up.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Jev differs from an LLM
&lt;/h2&gt;

&lt;p&gt;At a high level, Jev predicts the probability of each option in a set of choices or actions that you define, using the prompt or context you give it as the state. You might ask "should this input be blocked?" and "which of these 24 risk categories does it fall into?". Jev returns calibrated probabilities for every option in a single pass.&lt;/p&gt;

&lt;p&gt;An LLM works differently. It generates its output one token at a time (The 'decode' phase), and each step depends on the tokens it has already produced. This stepwise &lt;em&gt;decode&lt;/em&gt; phase is where most of an LLM's latency comes from. To use an LLM as a classifier, you ask it to write out its decision, usually as JSON, and then parse that text.&lt;/p&gt;

&lt;p&gt;That difference has practical consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The output is typed.&lt;/strong&gt; Jev returns probabilities and choices, not free text. There is nothing to parse, and no output can arrive malformed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cost structure is different.&lt;/strong&gt; Jev bills for input tokens only; its output is free [2]. An LLM bills for both, and at a higher rate for output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency does not grow with the answer.&lt;/strong&gt; A single scoring pass does not slow down as the answer gets longer, because there is no answer to write out token by token.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where Jev is a better fit
&lt;/h2&gt;

&lt;p&gt;Jev is not a replacement for an LLM. It does not write answers, summarise documents or generate code. What it targets is the large class of tasks inside AI systems where the output is a &lt;em&gt;decision&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;guardrails that decide whether an input or action should be blocked&lt;/li&gt;
&lt;li&gt;routing requests to the right model, tool or team&lt;/li&gt;
&lt;li&gt;classification, moderation and triage at high volume&lt;/li&gt;
&lt;li&gt;choosing the next action in an agent workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These decisions often sit in the critical path of every request. In that position, each additional second and each additional cent is paid on every call. That is where a model built only for decisions has the most to gain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we tested: Jev as a red-teaming guardrail
&lt;/h2&gt;

&lt;p&gt;Agent security is a natural test for this kind of model. A guardrail has to read one input and make two calls:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Should this input be blocked?&lt;/strong&gt; It might be a harmful request, a jailbreak attempt, or a prompt injection hidden inside a document or tool response.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What kind of risk is it?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the second question, every model chooses from &lt;strong&gt;24 risk categories&lt;/strong&gt;, and the risk category each prompt carries in its source dataset is the ground truth. 22 of these come from RedBench's risk taxonomy [3]. We added &lt;em&gt;No risk&lt;/em&gt; for benign inputs and &lt;em&gt;Prompt attack&lt;/em&gt; for jailbreak and injection attempts that target no specific harm.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Risk category&lt;/th&gt;
&lt;th&gt;Covers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No risk&lt;/td&gt;
&lt;td&gt;Benign request; safe to answer normally, even if it uses alarming words&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abusive content&lt;/td&gt;
&lt;td&gt;Harassment, insults, bullying or degrading language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bias content&lt;/td&gt;
&lt;td&gt;Stereotyping or unfair treatment of groups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CBRN information or capabilities&lt;/td&gt;
&lt;td&gt;Chemical, biological, radiological or nuclear weapons knowledge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Child safety&lt;/td&gt;
&lt;td&gt;Content endangering or sexualising minors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confabulation&lt;/td&gt;
&lt;td&gt;Eliciting fabricated facts presented as true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crime content&lt;/td&gt;
&lt;td&gt;Facilitating theft, fraud, drugs or other crimes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cybersecurity threats (beyond malware)&lt;/td&gt;
&lt;td&gt;Hacking, intrusion, phishing, system disruption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data privacy&lt;/td&gt;
&lt;td&gt;Obtaining or exposing personal or confidential data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision-making in critical systems&lt;/td&gt;
&lt;td&gt;Unsafe automated decisions in medical, legal, financial or safety-critical settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Economic harm&lt;/td&gt;
&lt;td&gt;Market manipulation, scams, financial damage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Election interference&lt;/td&gt;
&lt;td&gt;Voter manipulation or election disinformation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environmental harm&lt;/td&gt;
&lt;td&gt;Pollution, ecological damage, wildlife harm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extremism and radicalization&lt;/td&gt;
&lt;td&gt;Promoting extremist ideology or recruitment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hateful content&lt;/td&gt;
&lt;td&gt;Hate speech targeting protected groups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Illegal weapons (non-CBRN)&lt;/td&gt;
&lt;td&gt;Building or acquiring illegal conventional weapons or explosives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intellectual property&lt;/td&gt;
&lt;td&gt;Piracy or copyright and trademark infringement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Malware code&lt;/td&gt;
&lt;td&gt;Writing or deploying malicious software&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Misinformation&lt;/td&gt;
&lt;td&gt;False or misleading claims presented as fact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-harm content&lt;/td&gt;
&lt;td&gt;Suicide, self-injury or eating-disorder encouragement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sexual content&lt;/td&gt;
&lt;td&gt;Explicit or non-consensual sexual content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Violent content&lt;/td&gt;
&lt;td&gt;Violence, threats or physical harm to others&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other&lt;/td&gt;
&lt;td&gt;Harmful in a way not covered by the categories above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt attack&lt;/td&gt;
&lt;td&gt;Jailbreak or prompt-injection attempt when no more specific category applies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Data
&lt;/h3&gt;

&lt;p&gt;Our starting point was RedBench [3], which consolidates 37 red-teaming benchmarks. We added dedicated jailbreak and prompt-injection datasets [4–11], including agent transcripts where the attack is planted in a tool response [9]. From a pool of 41,443 prompts, we evaluated a stratified sample of &lt;strong&gt;1,580&lt;/strong&gt; (1,310 harmful, 270 benign), drawn from 14 datasets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Models
&lt;/h3&gt;

&lt;p&gt;We gave the same task, with identical instructions and the same 24-category risk taxonomy, to three hosted models: &lt;strong&gt;TypeSafe Jev&lt;/strong&gt; [2], &lt;strong&gt;OpenAI GPT-5-mini&lt;/strong&gt; [12] and &lt;strong&gt;Anthropic Claude Haiku 4.5&lt;/strong&gt; [13].&lt;/p&gt;

&lt;h2&gt;
  
  
  Results and observations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Speed, cost and quality
&lt;/h3&gt;

&lt;p&gt;Jev's median latency was &lt;strong&gt;0.17 seconds&lt;/strong&gt;, against 1.00 s for GPT-5-mini and 0.89 s for Claude Haiku 4.5. That is about &lt;strong&gt;six times faster&lt;/strong&gt; than GPT-5-mini and more than five times faster than Haiku. At the 95th percentile, Jev stayed at 0.26 s, while both LLMs were above 1.3 s.&lt;/p&gt;

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

&lt;p&gt;Classifying all 1,580 prompts cost &lt;strong&gt;$0.08&lt;/strong&gt; with Jev, &lt;strong&gt;$0.48&lt;/strong&gt; with GPT-5-mini and &lt;strong&gt;$2.23&lt;/strong&gt; with Claude Haiku 4.5 at list prices. That makes Jev about &lt;strong&gt;6× cheaper than GPT-5-mini&lt;/strong&gt; and &lt;strong&gt;27× cheaper than Haiku&lt;/strong&gt; on the same workload.&lt;/p&gt;

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

&lt;p&gt;Our measured gap is smaller than the headline figures TypeSafe published. That is expected. Their numbers describe workflows of System One tasks against their chosen baselines. Ours come from a single adversarial classification task, compared against two widely used, low-cost LLMs. The direction matches their claim; the size of the gap depends on the task and on what you compare against.&lt;/p&gt;

&lt;p&gt;Speed and cost only matter if the decisions hold up. Across all prompts, Jev flagged harm correctly &lt;strong&gt;85.5%&lt;/strong&gt; of the time (block accuracy), against 84.1% for GPT-5-mini and 80.1% for Claude Haiku 4.5.&lt;/p&gt;

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

&lt;p&gt;On naming the exact risk category out of 24 (categorization accuracy), Jev scored &lt;strong&gt;58.0%&lt;/strong&gt; against 56.0% and 55.4%.&lt;/p&gt;

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

&lt;p&gt;Its macro-F1 across the 24 categories, which weights every category equally regardless of size, was &lt;strong&gt;46.5%&lt;/strong&gt; against 41.7% and 38.1%.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Detection quality
&lt;/h3&gt;

&lt;p&gt;Categorization accuracy for each risk category with more than 20 labelled prompts:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8gn8xl55vmshqdykm18u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8gn8xl55vmshqdykm18u.png" alt="Categorization accuracy by risk category, categories with more than 20 labelled prompts" width="799" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of these categories, Jev has the highest categorization accuracy in nine: crime, abusive content, misinformation, cybersecurity threats, bias, data privacy, sexual content, economic harm and decision-making in critical systems. GPT-5-mini leads on hateful content, malware, violent content and illegal weapons. Haiku is strongest at recognising benign inputs (&lt;em&gt;No risk&lt;/em&gt;) and prompt attacks, and ties with GPT-5-mini ahead of Jev on self-harm.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparison with task-specific models
&lt;/h3&gt;

&lt;p&gt;We also ran three open-source models built specifically for guardrailing, on a T4 GPU: &lt;strong&gt;Qwen3Guard-Gen-4B&lt;/strong&gt; [14], &lt;strong&gt;Llama Prompt Guard 2 86M&lt;/strong&gt; [15] and &lt;strong&gt;ProtectAI DeBERTa-v3 prompt-injection v2&lt;/strong&gt; [16]. None of them answers in the 24 risk categories, so they are compared with Jev on block accuracy only.&lt;/p&gt;

&lt;p&gt;To compare performance across attack domains, we grouped the source benchmarks into nine &lt;strong&gt;benchmark groups&lt;/strong&gt; based on the kind of prompts each contains. The groups are used only to aggregate results; they are not the labels that the models predict.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benchmark group&lt;/th&gt;
&lt;th&gt;What it tests&lt;/th&gt;
&lt;th&gt;Benchmarks included&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Harmful instruction and question sets&lt;/td&gt;
&lt;td&gt;Plainly worded harmful requests and questions&lt;/td&gt;
&lt;td&gt;AdvBench, CatQA, CoNA, ControversialInstructions, DAN, DoNotAnswer, ForbiddenQuestions, GPTFuzzer, HarmBench, HarmfulQ, HarmfulQA, JBB-Behaviors, MaliciousInstruct, MaliciousInstructions, MedSafetyBench, QHarm, SGBench, StrongREJECT (all via RedBench [3])&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cyberattack assistance&lt;/td&gt;
&lt;td&gt;Requests for help with hacking, intrusion and malware&lt;/td&gt;
&lt;td&gt;CyberattackAssistance (via RedBench [3])&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed safety suites&lt;/td&gt;
&lt;td&gt;Broad safety suites whose prompts span many harm types&lt;/td&gt;
&lt;td&gt;XSafety, JADE (via RedBench [3])&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jailbreak prompt collections&lt;/td&gt;
&lt;td&gt;DAN-style, role-play and "developer mode" templates, with ordinary prompts as benign contrasts&lt;/td&gt;
&lt;td&gt;In-the-wild jailbreak and regular prompts [4], JailbreakBench JBC [5], Salad-Data jailbreak templates [6], jailbreak-classification and ChatGPT-Jailbreak-Prompts [10], LatentJailbreak (via RedBench [3])&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automated jailbreak attacks&lt;/td&gt;
&lt;td&gt;Attacks generated automatically (GCG, PAIR, random search, AutoDAN, GPTFuzzer, TAP)&lt;/td&gt;
&lt;td&gt;JailbreakBench GCG, PAIR and random-search artifacts [5], Salad-Data AutoDAN, GCG, GPTFuzzer and TAP attacks [6]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct prompt injection sets&lt;/td&gt;
&lt;td&gt;"Ignore previous instructions" and secret-extraction attempts, with benign contrasts&lt;/td&gt;
&lt;td&gt;Gandalf ignore-instructions and summarization (via RedBench [3]), Lakera Gandalf [8], deepset prompt-injections [7], safe-guard-prompt-injection [10]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Indirect prompt injection (InjecAgent)&lt;/td&gt;
&lt;td&gt;Instructions planted in tool responses to hijack an agent&lt;/td&gt;
&lt;td&gt;InjecAgent data-stealing and direct-harm cases [9], plus clean counterparts built for this study&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toxic statements and unsafe advice&lt;/td&gt;
&lt;td&gt;Toxic statements and prompts that invite unsafe advice&lt;/td&gt;
&lt;td&gt;ToxiGen, SafeText, PhysicalSafetyInstructions (via RedBench [3])&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Over-refusal suites&lt;/td&gt;
&lt;td&gt;Benign prompts that &lt;em&gt;sound&lt;/em&gt; dangerous, plus unsafe contrasts&lt;/td&gt;
&lt;td&gt;XSTest and CoCoNot [11], OR-Bench &lt;a href="https://dev.tovia%20RedBench%20[3]"&gt;11&lt;/a&gt;, JBB-Behaviors benign set [5]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Qwen3Guard is scored on every benchmark group. Prompt Guard 2 and ProtectAI detect prompt attacks only, so they appear only in the jailbreak and prompt-injection groups they were run on in full.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19uy4plh7twmcxthbi38.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19uy4plh7twmcxthbi38.png" alt="Block accuracy by benchmark group, Jev and task-specific models" width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Jev matches or beats every task-specific model in eight of the nine benchmark groups.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Clear leads:&lt;/strong&gt; direct prompt injection (95%, against 88% for ProtectAI and 80% for Qwen3Guard), cyberattack assistance (83% against 67%), toxic statements and unsafe advice (49% against 38%) and indirect prompt injection (77% against 70%).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Narrow leads:&lt;/strong&gt; harmful instructions (92% against 90%) and mixed safety suites (47% against 42%).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level:&lt;/strong&gt; automated jailbreaks (96% for both) and over-refusal (72% for both).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behind:&lt;/strong&gt; jailbreak prompt collections, where Qwen3Guard is slightly ahead (93.3% against 92.9%).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two prompt-attack classifiers trail Jev throughout. Prompt Guard 2 is behind Jev and Qwen3Guard in every group it covers. ProtectAI beats Qwen3Guard on direct injection but not Jev. Both score 33% on indirect injection hidden in tool responses.&lt;/p&gt;

&lt;p&gt;Two caveats apply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mixed safety suites and toxic statements are hard for every model, with all scores below 50%.&lt;/li&gt;
&lt;li&gt;Indirect prompt injection remains Jev's weakest group against the hosted LLMs, which both reached 91% there.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this means for enterprises
&lt;/h2&gt;

&lt;p&gt;For most organisations, the expensive part of AI safety is not a single model call. It is making that call on every request, every agent step, and every test case in every evaluation run. A guardrail that sits in front of a production agent may run millions of times a month. An evaluation suite that re-scores thousands of cases after each prompt or model change runs constantly during development.&lt;/p&gt;

&lt;p&gt;In those settings, the gap between a one-second LLM judgement and a sub-200-millisecond typed decision is not a rounding error. It decides whether security checks can run inline or have to be sampled, and whether evaluation runs on every change or once a week. Customers running their evaluation workloads through OptiX with Jev are already seeing reduced evaluation costs and lower latency, with performance on par with or ahead of the LLMs.&lt;/p&gt;

&lt;p&gt;The broader lesson from this study is that decision tasks and generation tasks do not need the same model. Matching the model to the job, and measuring it on your own data, pays off quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Explore OptiX
&lt;/h2&gt;

&lt;p&gt;OptiX is our platform for testing, evaluating and monitoring AI systems. It brings together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security scans&lt;/strong&gt; for jailbreaks, prompt injection and other adversarial inputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-agent evaluation&lt;/strong&gt; across complex, multi-step workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; for AI systems in production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use-case evaluation&lt;/strong&gt; for RAG, text-to-SQL, document parsing, entity extraction, summarisation and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jev support&lt;/strong&gt;, so fast, low-cost typed decisions can power guardrails and evaluations alongside LLM-based judges&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;To see how your own agents hold up, visit &lt;strong&gt;&lt;a href="https://optix.cobuildx.ai/" rel="noopener noreferrer"&gt;optix.cobuildx.ai&lt;/a&gt;&lt;/strong&gt; and get in touch with our team.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;&lt;strong&gt;Benchmark and claim&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeSafe AI. "193.6x faster, 444.6x cheaper" launch claim. &lt;a href="https://typesafe.ai/" rel="noopener noreferrer"&gt;https://typesafe.ai/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TypeSafe AI. Jev model documentation. &lt;a href="https://docs.typesafe.ai/models" rel="noopener noreferrer"&gt;https://docs.typesafe.ai/models&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Q.-A. Dang, C. Ngo, T.-S. Hy. &lt;em&gt;RedBench: A Universal Dataset for Comprehensive Red Teaming of Large Language Models.&lt;/em&gt; arXiv:2601.03699, 2026. Dataset: &lt;a href="https://huggingface.co/datasets/knoveleng/redbench" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/knoveleng/redbench&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Jailbreak and prompt-injection datasets&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;X. Shen et al. &lt;em&gt;"Do Anything Now": Characterizing and Evaluating In-The-Wild Jailbreak Prompts on Large Language Models.&lt;/em&gt; ACM CCS 2024. arXiv:2308.03825. Dataset: &lt;a href="https://huggingface.co/datasets/TrustAIRLab/in-the-wild-jailbreak-prompts" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/TrustAIRLab/in-the-wild-jailbreak-prompts&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;P. Chao et al. &lt;em&gt;JailbreakBench: An Open Robustness Benchmark for Jailbreaking Large Language Models.&lt;/em&gt; arXiv:2404.01318, 2024. Artifacts: &lt;a href="https://github.com/JailbreakBench/artifacts" rel="noopener noreferrer"&gt;https://github.com/JailbreakBench/artifacts&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;L. Li et al. &lt;em&gt;SALAD-Bench: A Hierarchical and Comprehensive Safety Benchmark for Large Language Models.&lt;/em&gt; arXiv:2402.05044, 2024. Dataset: &lt;a href="https://huggingface.co/datasets/OpenSafetyLab/Salad-Data" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/OpenSafetyLab/Salad-Data&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;deepset. prompt-injections dataset. &lt;a href="https://huggingface.co/datasets/deepset/prompt-injections" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/deepset/prompt-injections&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lakera. gandalf_ignore_instructions dataset. &lt;a href="https://huggingface.co/datasets/Lakera/gandalf_ignore_instructions" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/Lakera/gandalf_ignore_instructions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Q. Zhan et al. &lt;em&gt;InjecAgent: Benchmarking Indirect Prompt Injections in Tool-Integrated Large Language Model Agents.&lt;/em&gt; arXiv:2403.02691, 2024. &lt;a href="https://github.com/uiuc-kang-lab/InjecAgent" rel="noopener noreferrer"&gt;https://github.com/uiuc-kang-lab/InjecAgent&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Community datasets on Hugging Face: jackhhao/jailbreak-classification, xTRam1/safe-guard-prompt-injection, rubend18/ChatGPT-Jailbreak-Prompts.&lt;/li&gt;
&lt;li&gt;Over-refusal benchmarks: P. Röttger et al. &lt;em&gt;XSTest: A Test Suite for Identifying Exaggerated Safety Behaviours in Large Language Models.&lt;/em&gt; arXiv:2308.01263, 2023; F. Brahman et al. &lt;em&gt;The Art of Saying No: Contextual Noncompliance in Language Models&lt;/em&gt; (CoCoNot). arXiv:2407.12043, 2024; J. Cui et al. &lt;em&gt;OR-Bench: An Over-Refusal Benchmark for Large Language Models.&lt;/em&gt; arXiv:2405.20947, 2024.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Models&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI. GPT-5-mini. &lt;a href="https://platform.openai.com/docs/models" rel="noopener noreferrer"&gt;https://platform.openai.com/docs/models&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Anthropic. Claude Haiku 4.5. &lt;a href="https://docs.anthropic.com/en/docs/about-claude/models" rel="noopener noreferrer"&gt;https://docs.anthropic.com/en/docs/about-claude/models&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;H. Zhao et al. &lt;em&gt;Qwen3Guard Technical Report.&lt;/em&gt; arXiv:2510.14276, 2025.&lt;/li&gt;
&lt;li&gt;S. Chennabasappa et al. &lt;em&gt;LlamaFirewall: An Open Source Guardrail System for Building Secure AI Agents&lt;/em&gt; (Llama Prompt Guard 2). arXiv:2505.03574, 2025.&lt;/li&gt;
&lt;li&gt;ProtectAI. deberta-v3-base-prompt-injection-v2. &lt;a href="https://huggingface.co/protectai/deberta-v3-base-prompt-injection-v2" rel="noopener noreferrer"&gt;https://huggingface.co/protectai/deberta-v3-base-prompt-injection-v2&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/jev-red-teaming-guardrail" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Collections AI: The Compliance Constraint That Shapes Everything</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Thu, 08 Oct 2026 11:02:25 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/collections-ai-the-compliance-constraint-that-shapes-everything-4edo</link>
      <guid>https://dev.to/cobuildx-ai/collections-ai-the-compliance-constraint-that-shapes-everything-4edo</guid>
      <description>&lt;p&gt;&lt;em&gt;AI can meaningfully improve collections performance within the regulatory framework — propensity scoring, channel optimisation, timing models. What it cannot do is automate the things TCPA, FDCPA, and state-level consumer protection laws prohibit. Here is what the boundary looks like.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Collections is one of the highest-value AI applications in financial services by the numbers — small improvements in recovery rates on large portfolios compound quickly. It is also one of the most heavily regulated operational functions at any lender. TCPA, FDCPA, state-level consumer protection laws, and the CFPB's interpretive guidance on unfair, deceptive, or abusive acts and practices all constrain what an AI-driven collections operation can do. The practical question is not whether to apply AI in collections — the ROI case is clear — but what AI can do within the constraint set.&lt;/p&gt;

&lt;p&gt;Collections AI projects fail in two ways. The first: the team builds a sophisticated propensity or segmentation model but does not integrate it into collections workflows in a way that changes collector behaviour. The model exists; nothing changes. The second: the team deploys automated outreach capabilities without adequately scoping the regulatory framework, and the compliance review catches problems that require significant redesign.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; TCPA and FDCPA do not prevent AI from improving collections — they define the boundaries within which AI can operate. Designing to those boundaries from the start produces a system that is both effective and defensible.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"We built the model. Then compliance reviewed the outreach automation we had planned and we had to redesign half of it."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What TCPA Actually Constrains
&lt;/h2&gt;

&lt;p&gt;The Telephone Consumer Protection Act (TCPA) restricts the use of automatic telephone dialling systems (ATDS) and artificial or prerecorded voice messages to contact consumers without their prior express written consent. In a collections context, this constrains automated outreach — AI-driven diallers, automated SMS campaigns, and prerecorded voice messages — significantly.&lt;/p&gt;

&lt;p&gt;Prior express written consent is required for autodialled calls or texts to mobile phones. For debt collection, this consent must be obtained at the time the credit relationship is established or through a subsequent consent process. Consent obtained at origination covers most account holders for initial collection contact, but consumers can revoke consent at any time, and revocations must be honoured immediately — a process that needs to be automated and audited.&lt;/p&gt;

&lt;p&gt;The TCPA does not prohibit human-initiated calls using manual dialling. Collections operations can use predictive diallers in a compliant way if the dialler does not meet the ATDS definition — an increasingly nuanced technical and legal question after the Facebook v. Duguid Supreme Court decision in 2021. Any collections AI involving automated outreach should be reviewed by counsel with TCPA expertise before deployment.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TCPA constrains automated outreach to mobile phones without consent — the consent management process is as important to design as the outreach automation itself&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  FDCPA: What You Can Say and When
&lt;/h2&gt;

&lt;p&gt;The Fair Debt Collection Practices Act (FDCPA) governs third-party debt collectors and imposes specific requirements on the timing, content, and frequency of collection communications. The CFPB's Regulation F, which took effect in 2021, extended some FDCPA principles and added specific guidance on electronic communications including email and text.&lt;/p&gt;

&lt;p&gt;Key constraints with direct implications for AI-driven collections:&lt;/p&gt;

&lt;p&gt;Contact frequency limits: Regulation F creates a presumption that contacting a consumer more than seven times in a seven-day period, or within seven days of a prior telephone conversation, violates the FDCPA. AI-driven outreach optimisation needs to track and enforce these limits across all communication channels.&lt;/p&gt;

&lt;p&gt;Time-of-day restrictions: calls are permitted only between 8am and 9pm in the consumer's local time zone. An AI scheduling model optimising for contact rates needs to incorporate this constraint — and needs to use the consumer's time zone, not the operations centre's.&lt;/p&gt;

&lt;p&gt;Cease and desist: if a consumer requests that contact cease, all communication must stop except to confirm receipt of the cease notice or to advise of specific legal action. This revocation process needs to be immediate and auditable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Seven-in-seven contact frequency limits, time-of-day restrictions, and immediate cease-and-desist compliance are non-negotiable constraints that AI outreach must enforce&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What AI Can Do Within These Constraints
&lt;/h2&gt;

&lt;p&gt;Within the regulatory framework, AI delivers meaningful collections improvement through three specific applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Propensity to pay modelling.&lt;/strong&gt; A model that predicts which accounts are most likely to pay in response to outreach — and under what conditions (settlement offer, payment plan, specific timing) — allows collections operations to prioritise contacts more effectively than simple days-past-due segmentation. Accounts with high propensity to pay at full balance get early personal contact; accounts that respond to settlement offers get offers earlier in the delinquency cycle when recovery is higher.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Channel preference optimisation.&lt;/strong&gt; Different consumers respond to different channels. Some respond to text, some to email, some to phone calls. A model trained on historical contact data can predict which channel is most likely to produce a response for a given consumer profile, reducing contact attempts while improving contact rates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timing optimisation.&lt;/strong&gt; Within the time-of-day constraints and the contact frequency limits, there is still meaningful variation in when a consumer is most likely to answer or respond. ML models trained on response rates by day of week and time of day — by consumer segment — can improve contact rates without increasing total contact attempts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Propensity to pay modelling, channel preference, and timing optimisation deliver collections lift within the regulatory framework — together, improvements of 10–20% in recovery rates on targeted portfolios are realistic&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  State-Level Overlay
&lt;/h2&gt;

&lt;p&gt;Federal TCPA and FDCPA requirements are the floor. A significant number of states have consumer protection laws that are more restrictive, and compliance at the federal level does not mean compliance in every state where borrowers are located.&lt;/p&gt;

&lt;p&gt;California's Rosenthal Fair Debt Collection Practices Act applies FDCPA-equivalent requirements to first-party collectors (lenders collecting their own debt) in addition to third-party collectors — a broader scope than federal law. New York and other states have similar first-party coverage extensions.&lt;/p&gt;

&lt;p&gt;Several states have more restrictive contact frequency rules than Regulation F, shorter time windows for permissible calling hours, or additional disclosure requirements for automated communications.&lt;/p&gt;

&lt;p&gt;For a lender with a national portfolio, the compliance standard is the most restrictive applicable state law for each consumer, not the most permissive interpretation of federal law. A collections AI system with national scope needs to implement state-level rule variations or operate to the most conservative standard across the full portfolio.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;State-level consumer protection laws can be more restrictive than federal TCPA and FDCPA — compliance requires the most restrictive applicable standard for each consumer's state&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Building the Business Case Within the Constraints
&lt;/h2&gt;

&lt;p&gt;The ROI case for collections AI is real within these constraints. The constraint set limits automated outreach, but it does not limit propensity scoring, segmentation, channel optimisation, or timing models — all of which improve outcomes without requiring automation of contact initiation.&lt;/p&gt;

&lt;p&gt;Realistic targets for a well-scoped collections AI program: 10–20% improvement in early-stage recovery rates through better prioritisation and channel selection on accounts where propensity to pay is high. 15–25% reduction in average collection cost per recovered dollar through more efficient contact allocation. Measurable reduction in complaints from improved compliance with contact frequency and timing rules.&lt;/p&gt;

&lt;p&gt;The compliance investment — consent management, revocation tracking, state-level rule implementation, audit logging — is not small. It is also not optional, and treating it as overhead rather than part of the system design leads to the pattern of building first and redesigning after compliance review. The right approach: scope the compliance infrastructure at the start, design the AI system to operate within it, and build the business case on what is achievable within that framework.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;10–20% early-stage recovery improvement and 15–25% cost reduction per recovered dollar are achievable within the regulatory framework — scope the compliance infrastructure at the start, not after&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/collections-ai-compliance-constraints" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>fintech</category>
      <category>machinelearning</category>
      <category>compliance</category>
    </item>
    <item>
      <title>Credit Risk Scoring: The Data Problems That Haven't Changed</title>
      <dc:creator>Emmanuel R</dc:creator>
      <pubDate>Thu, 08 Oct 2026 11:01:49 +0000</pubDate>
      <link>https://dev.to/cobuildx-ai/credit-risk-scoring-the-data-problems-that-havent-changed-3n8c</link>
      <guid>https://dev.to/cobuildx-ai/credit-risk-scoring-the-data-problems-that-havent-changed-3n8c</guid>
      <description>&lt;p&gt;&lt;em&gt;Better models have not solved the thin-file problem, the data quality issues in alternative data, or the validation requirements that govern what can be deployed in regulated lending. The data problems are still the hard part.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Credit risk modelling has changed significantly over the past decade. Gradient boosting has replaced logistic regression as the standard approach at most sophisticated lenders. Alternative data — cash flow data, rental payment history, utility payments — has broadened the credit-visible population. Explainability tooling has made complex models more deployable in regulated environments. What has not changed: the data quality problems that determine whether any model, regardless of sophistication, produces reliable predictions.&lt;/p&gt;

&lt;p&gt;The limiting factor in credit risk modelling is not model architecture. Given reasonable data, a well-tuned gradient boosting model and a well-tuned logistic regression produce different but both useful predictions. The limiting factors are: the thin-file problem (a significant fraction of credit-worthy borrowers have insufficient traditional credit history to model reliably), the alternative data quality problem (much of the data that could address thin-file has its own reliability and coverage gaps), and the model validation requirements that govern what can be deployed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; Lenders who have improved credit risk modelling in recent years have done so primarily through better data — specifically cash flow data and expanded credit bureau data — not better model architectures. The model is less important than the signal.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"We spent six months building a more sophisticated model. When we went back and cleaned the training data, the simple model performed just as well."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Thin-File Problem
&lt;/h2&gt;

&lt;p&gt;Approximately 45 million Americans have thin or no credit files — too little traditional credit history for the major bureaus to generate a reliable score. This population is not uniformly high risk. It includes recent immigrants with strong financial management histories in their home countries, young adults who have avoided debt but have stable income, and individuals who have historically used cash and community lending. A lender that cannot score them declines credit-worthy borrowers and cedes that business to competitors who can.&lt;/p&gt;

&lt;p&gt;Traditional bureau data does not solve the thin-file problem because the thin-file is definitionally not in the bureau data. Expanding the modelling approach requires either incorporating non-traditional data sources (rental payments, utility payments, cash flow data) or accepting that a segment of the credit-worthy population will not be scoreable through traditional approaches.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;45 million Americans are not scoreable through traditional bureau data — addressing the thin-file problem requires non-traditional data sources, not better models on the same data&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Alternative Data: What Works and What Does Not
&lt;/h2&gt;

&lt;p&gt;Cash flow data — transaction-level bank account data showing income deposits, recurring payments, and spending patterns — is the most consistently useful alternative data source for credit risk in consumer lending. It is highly predictive of ability to pay, has reasonable coverage through open banking integrations, and is relatively clean compared to other alternative data sources. Cash flow features (income consistency, average balance, payment-to-income ratios) add meaningful lift for thin-file borrowers on top of bureau data.&lt;/p&gt;

&lt;p&gt;Rent payment history, when available and reliably reported, is also useful — a demonstrated track record of on-time rent payments is predictive of future credit performance. The coverage problem is significant: most rent payments are not reported to any bureau, and the coverage of rent payment data from alternative providers is incomplete and variable by geography.&lt;/p&gt;

&lt;p&gt;Data sources that have shown less consistent value: social media signals, education and employment data from non-traditional sources, and 'character' signals derived from digital behaviour. These data types often introduce fair lending risk (proxies for protected characteristics appear in unexpected places) without delivering equivalent predictive lift. Several lenders have retreated from these approaches after fair lending testing revealed disparate impact.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cash flow data is the most consistently predictive alternative data source. Character and digital behaviour signals introduce fair lending risk without reliable predictive lift&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Recency Problem
&lt;/h2&gt;

&lt;p&gt;Credit risk models trained on pre-2020 data reflect credit behaviour in a low-interest-rate, low-unemployment economic environment. Models trained primarily on 2020–2022 data reflect an unprecedented economic disruption period with stimulus payments, forbearance programs, and unusual payment behaviour. Neither training period is a reliable guide to current credit risk in a normalising interest rate environment.&lt;/p&gt;

&lt;p&gt;The practical implication: models trained on data more than two to three years old should be retested against recent performance data before being treated as reliable. Performance metrics that looked stable during model development may have degraded significantly as the economic environment shifted.&lt;/p&gt;

&lt;p&gt;This is not a new problem — economic cycle sensitivity in credit models has been understood for decades. It became more acute after 2020 because the economic disruption was severe enough to make pre-crisis training data significantly less predictive of post-crisis behaviour. Lenders whose models date from 2018 or earlier and have not been recalibrated against post-2022 performance data are running on models whose accuracy may be materially worse than their validation metrics suggest.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Models trained on pre-2020 data should be retested against recent performance data — the economic environment shift has degraded accuracy in ways validation metrics from that period do not reflect&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Model Validation in a Regulated Environment
&lt;/h2&gt;

&lt;p&gt;For banks and supervised lenders, credit risk models are subject to model risk management requirements under SR 11-7. The validation requirement is not just that the model performs well by internal metrics — it is that the model has been independently validated, the validation covers conceptual soundness and outcome analysis, limitations are documented, and ongoing monitoring is in place.&lt;/p&gt;

&lt;p&gt;For the models with the most regulatory scrutiny — models used in underwriting decisions for consumer credit at scale — the validation needs to cover fair lending analysis as a standard component. A model that validates well on predictive performance metrics but has not been tested for disparate impact is not ready for production at a regulated lender.&lt;/p&gt;

&lt;p&gt;The practical implication for AI/ML models: more complex models require more documentation and a more sophisticated validation function. The validation team needs to understand the feature engineering, the training data, the hyperparameter selection rationale, and the limitations of the model well enough to write a challenge that addresses each. This is achievable but it requires planning — presenting a finished ML model to model risk management without having engaged them in the development process is a common reason for long validation timelines.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Engaging model risk management during development, not at the end of it, is the single most effective way to avoid long validation delays&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cobuildx.ai/blog/credit-risk-model-data-quality-2025" rel="noopener noreferrer"&gt;CobuildX blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>fintech</category>
      <category>machinelearning</category>
      <category>compliance</category>
    </item>
  </channel>
</rss>
