<?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: Atelje Vagabond</title>
    <description>The latest articles on DEV Community by Atelje Vagabond (@ateljevagabond).</description>
    <link>https://dev.to/ateljevagabond</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3873744%2F07072336-f4ee-4140-8bd8-8191770019d5.png</url>
      <title>DEV Community: Atelje Vagabond</title>
      <link>https://dev.to/ateljevagabond</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ateljevagabond"/>
    <language>en</language>
    <item>
      <title>[Boost]</title>
      <dc:creator>Atelje Vagabond</dc:creator>
      <pubDate>Sat, 25 Apr 2026 09:19:16 +0000</pubDate>
      <link>https://dev.to/ateljevagabond/-1h0f</link>
      <guid>https://dev.to/ateljevagabond/-1h0f</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/ateljevagabond/why-you-need-mlops-when-cicd-for-machine-learning-becomes-mandatory-3icb" class="crayons-story__hidden-navigation-link"&gt;Why You Need MLOps: When CI/CD for Machine Learning Becomes Mandatory&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/ateljevagabond" class="crayons-avatar  crayons-avatar--l  "&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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3873744%2F07072336-f4ee-4140-8bd8-8191770019d5.png" alt="ateljevagabond profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/ateljevagabond" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Atelje Vagabond
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Atelje Vagabond
                
              
              &lt;div id="story-author-preview-content-3545787" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/ateljevagabond" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3873744%2F07072336-f4ee-4140-8bd8-8191770019d5.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Atelje Vagabond&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/ateljevagabond/why-you-need-mlops-when-cicd-for-machine-learning-becomes-mandatory-3icb" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 24&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/ateljevagabond/why-you-need-mlops-when-cicd-for-machine-learning-becomes-mandatory-3icb" id="article-link-3545787"&gt;
          Why You Need MLOps: When CI/CD for Machine Learning Becomes Mandatory
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mlops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mlops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/machinelearning"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;machinelearning&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/datascience"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;datascience&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/ateljevagabond/why-you-need-mlops-when-cicd-for-machine-learning-becomes-mandatory-3icb" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;2&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/ateljevagabond/why-you-need-mlops-when-cicd-for-machine-learning-becomes-mandatory-3icb#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              1&lt;span class="hidden s:inline"&gt; comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Why You Need MLOps: When CI/CD for Machine Learning Becomes Mandatory</title>
      <dc:creator>Atelje Vagabond</dc:creator>
      <pubDate>Fri, 24 Apr 2026 11:01:39 +0000</pubDate>
      <link>https://dev.to/ateljevagabond/why-you-need-mlops-when-cicd-for-machine-learning-becomes-mandatory-3icb</link>
      <guid>https://dev.to/ateljevagabond/why-you-need-mlops-when-cicd-for-machine-learning-becomes-mandatory-3icb</guid>
      <description>&lt;p&gt;For six months, the team did everything right. They had a brilliant lead data scientist, Dr. Alan. They had a unique dataset. After hundreds of experiments and countless hours of training, they finally hit the magic number.&lt;/p&gt;

&lt;p&gt;Accuracy: &lt;strong&gt;94%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The model converged. The investor demo was flawless. The funding was secured. The excitement in the room was palpable.&lt;/p&gt;

&lt;p&gt;Then, they made the decision that breaks almost every new ML team:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It works on Alan's machine. Let's just wrap it in an API and ship it to production tomorrow."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Six months of hard science was about to collide with the hard reality of software engineering.&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%2Fblog.ateljevagabond.se%2Fassets%2Fimages%2Fposts%2Fmlops-production-failure-comic-v2.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%2Fblog.ateljevagabond.se%2Fassets%2Fimages%2Fposts%2Fmlops-production-failure-comic-v2.png" alt="The MLOps disaster story: From working notebook to production failure, high cloud costs, and eventual disciplined pipeline." width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The comic above isn't just a funny illustration; it is the autobiography of thousands of companies trying to deploy machine learning for the first time.&lt;/p&gt;

&lt;p&gt;What followed for Dr. Alan's team wasn't a &lt;em&gt;model&lt;/em&gt; failure. It was a &lt;em&gt;system&lt;/em&gt; failure. The live data didn't match the clean training data. Predictions started silently degrading. Cloud costs exploded because GPU instances were left running idle. When things broke, no one could reproduce the exact combination of code and data that built the original model.&lt;/p&gt;

&lt;p&gt;They learned an expensive lesson: A model in a Jupyter notebook is a hypothesis. A model in production is an obligation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Engineering Necessity of MLOps
&lt;/h2&gt;

&lt;p&gt;The transition from a research prototype to a live production service introduces engineering challenges that break traditional software deployment methodologies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Machine Learning Operations (MLOps)&lt;/strong&gt; isn't just a buzzword or a set of "best practices." It is the engineering discipline required to apply DevOps principles—continuous integration, continuous delivery, infrastructure-as-code—specifically to the unique lifecycle of machine learning.&lt;/p&gt;

&lt;p&gt;Unlike traditional software CI/CD, which primarily manages code versions, MLOps must manage &lt;strong&gt;three distinct, intertwined artifact types&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Code:&lt;/strong&gt; The training scripts, feature engineering logic, and serving wrappers.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Data:&lt;/strong&gt; The training datasets, validation splits, and live inference data.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Models:&lt;/strong&gt; The serialized artifacts (pickles, ONNX files), container images, and hyperparameters.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The complexity multiplies because a single production "rollout" must atomically coordinate all three. Furthermore, you need the ability to rollback any one artifact independently without causing cascading failures in the others.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining the Architectural Threshold: When Is MLOps Mandatory?
&lt;/h3&gt;

&lt;p&gt;How do you know when you've moved past the "prototype" phase and need a formal MLOps framework? It's not based on your model's accuracy score. It is determined by the operational tempo and complexity of your system.&lt;/p&gt;

&lt;p&gt;If your system meets these criteria, MLOps is no longer optional; it's a mandatory architectural requirement.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System Characteristic&lt;/th&gt;
&lt;th&gt;Local/Prototype Stage&lt;/th&gt;
&lt;th&gt;Production Stage (MLOps Required)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment Frequency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ad-hoc (Manual, periodic updates)&lt;/td&gt;
&lt;td&gt;High velocity (Weekly, daily, or automated triggers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Variability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fixed, frozen CSVs or tables&lt;/td&gt;
&lt;td&gt;Streaming data, semi-structured inputs, evident concept drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;System Scale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single user, local laptop GPU&lt;/td&gt;
&lt;td&gt;Distributed throughput, high concurrent users, auto-scaling clusters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auditability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unknown (It's somewhere in a source Notebook)&lt;/td&gt;
&lt;td&gt;Fully traceable lineage for compliance (banking, healthcare)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Failure Mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Easily reproducible and debugged locally&lt;/td&gt;
&lt;td&gt;Non-deterministic, difficult to trace (e.g., Training-Serving Skew)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The engineering threshold is crossed the moment the cost of manual monitoring, debugging, and firefighting exceeds the cost of building robust automation tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Key Architectural Pain Points MLOps Solves
&lt;/h2&gt;

&lt;p&gt;Without a formal MLOps architecture, your system accumulates specific types of technical debt that degrade reliability and performance over time.&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.amazonaws.com%2Fuploads%2Farticles%2Fwx6r587t7porq3oqj5ge.jpg" 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.amazonaws.com%2Fuploads%2Farticles%2Fwx6r587t7porq3oqj5ge.jpg" alt="Diagram showing the difference between manual, error-prone ML deployments and structured MLOps using feature stores and CI/CD pipelines." width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Training-Serving Skew (The Silent Killer)
&lt;/h3&gt;

&lt;p&gt;This is the most critical engineering failure. It happens when the logic used to calculate features during training runs differently than the logic used during real-time inference.&lt;/p&gt;

&lt;p&gt;For example, if your data scientist calculates a "7-day rolling average" in Pandas for training, but the production engineer reimplements that logic in Java for the serving API, tiny discrepancies will creep in. The model receives data in production that is mathematically different from what it saw during training, leading to junk predictions despite a high accuracy score. MLOps solves this through standardized &lt;strong&gt;feature stores&lt;/strong&gt; that ensure consistent logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Model Drift and Data Decay
&lt;/h3&gt;

&lt;p&gt;Model performance degrades over time not because the model "breaks," but because the world changes. The statistical properties of the input data shift. Without automated monitoring and &lt;strong&gt;automated retraining triggers&lt;/strong&gt;, your model will confidently serve obsolete predictions.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Reproducibility Failure
&lt;/h3&gt;

&lt;p&gt;When an incident occurs in production at 3 AM, can your team immediately reproduce the exact state—the specific code commit, the exact slice of data, the hyperparameters, and library dependencies—that led to that deployed model? If not, you don't have a production system; you have a black box. MLOps ensures every deployed artifact is immutable and traceable back to its origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cloud Native MLOps Stack: A Deep Dive
&lt;/h2&gt;

&lt;p&gt;Modern MLOps architectures rely on cloud-native managed services to handle the heavy lifting of compute scheduling and container management, allowing teams to focus on the workflow logic.&lt;/p&gt;

&lt;p&gt;While many clouds offer solutions, the choice often comes down to existing infrastructure and compliance needs.&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.amazonaws.com%2Fuploads%2Farticles%2Fffch5sx7fyi9x72txone.jpg" 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.amazonaws.com%2Fuploads%2Farticles%2Fffch5sx7fyi9x72txone.jpg" alt="Architectural comparison between Google Vertex AI and Microsoft Azure ML. Detailed diagrams comparing the MLOps architectures of Google Cloud Vertex AI and Microsoft Azure Machine Learning." width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚙️ Google Cloud Platform: Vertex AI
&lt;/h3&gt;

&lt;p&gt;Vertex AI emphasizes unified workflows where pipeline steps run as isolated containers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Functionality Focus:&lt;/strong&gt; &lt;strong&gt;Vertex AI Pipelines&lt;/strong&gt; is the core orchestration engine, supporting Kubeflow Pipelines (KFP) and TFX. It allows you to define your workflow as a Directed Acyclic Graph (DAG): Data Validation → Feature Engineering → Training → Evaluation.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Key Component Notes:&lt;/strong&gt; The &lt;strong&gt;Vertex AI Feature Store (V2)&lt;/strong&gt; is now built on BigQuery for offline storage. It uses timestamp-based resolution to ensure point-in-time correctness, reducing training-serving skew.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Important Deprecation Warning:&lt;/strong&gt; Be aware that Google's Legacy Feature Store API will be shut down in early 2027. Furthermore, the "Optimized online serving" for V2 is also deprecated; Google is directing users toward &lt;strong&gt;Bigtable online serving&lt;/strong&gt; for low-latency scenarios. Plan any new architecture accordingly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ☁️ Microsoft Azure: Azure Machine Learning (Azure ML)
&lt;/h3&gt;

&lt;p&gt;Azure ML shines in regulated enterprise environments due to its deep integration with Azure's governance and security fabric.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Architectural Strength:&lt;/strong&gt; Security is paramount. &lt;strong&gt;Role-Based Access Control (RBAC)&lt;/strong&gt; is handled via &lt;strong&gt;Microsoft Entra ID&lt;/strong&gt; (formerly Azure AD), meaning identity policy is centrally managed rather than replicated in the ML platform.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Automation:&lt;/strong&gt; It relies heavily on event-driven automation via &lt;strong&gt;Azure Event Grid&lt;/strong&gt;. Events like &lt;code&gt;ModelRegistered&lt;/code&gt; or &lt;code&gt;RunCompleted&lt;/code&gt; can trigger downstream pipelines, automated validation checks, or deployments.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Lifecycle Management:&lt;/strong&gt; Azure ML has strong model registry capabilities with full lineage tracking. While it supports MLflow, native deployment workflows often rely on Azure SDK v2 &lt;strong&gt;tags&lt;/strong&gt; to manage lifecycle states (e.g., tagging a model as &lt;code&gt;candidate&lt;/code&gt; vs &lt;code&gt;production&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Operational Caveats: The Hidden Costs of ML Infrastructure
&lt;/h2&gt;

&lt;p&gt;Before rolling out any architecture, you must address the elephant in the room: Operational Expenditure (OpEx).&lt;/p&gt;

&lt;p&gt;Scaling ML infrastructure—especially GPU-accelerated distributed training—introduces massive cost variability that shocks organizations unprepared for it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;The GPU Price Tag:&lt;/strong&gt; On Google Cloud, a single node with 8x H100 GPUs (the current gold standard for LLM work) can run upwards of &lt;strong&gt;$90 per hour&lt;/strong&gt; on-demand. A 24-hour training run is a $2,000+ event. If you need multi-node distributed training, those costs scale linearly per day.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The "Zombie Cluster" Problem:&lt;/strong&gt; In Azure ML, if compute clusters are configured with a minimum node count greater than zero, those nodes run &lt;em&gt;continuously&lt;/em&gt;, regardless of whether a job is active. Without automated teardown triggers on job completion (or failure!), idle GPU hours will accumulate silently. You won't know until the five-figure bill arrives at the end of the month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Architectural Requirement:&lt;/strong&gt; Operational budget planning and FinOps practices must be integrated from Day 1. You need automated cluster teardown triggers, strict GPU utilization alerts, and scheduled pipeline runs to avoid on-demand burst pricing.&lt;/p&gt;

&lt;p&gt;Implementing strict &lt;a href="https://ateljevagabond.se/services/cloud-cost-optimization-finops/" rel="noopener noreferrer"&gt;Cloud Cost Optimization and FinOps Practices&lt;/a&gt; is just as critical as the ML code itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes When Adopting MLOps
&lt;/h2&gt;

&lt;p&gt;The failures we see are rarely related to the math; they are related to the process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Assigning MLOps to the wrong team.&lt;/strong&gt;&lt;br&gt;
MLOps sits at the intersection of Data Science, Data Engineering, and DevOps. Handing the entire responsibility to a data science team with no infrastructure experience, or a DevOps team with no ML exposure, is a recipe for disaster. Pipelines will technically "run," but they won't be robust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Skipping the Data Audit.&lt;/strong&gt;&lt;br&gt;
MLOps is architecture built on data assumptions. If you build pipelines before auditing your data reality—schema consistency, null distributions, ingestion latency—you will build a very expensive system that automates the ingestion of garbage data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Treating MLOps as a "One-Time Setup."&lt;/strong&gt;&lt;br&gt;
MLOps infrastructure is not static. As noted in the Vertex AI section above, cloud APIs deprecate, SDK versions end-of-life, and pricing models change. If you don't budget for ongoing platform engineering maintenance, your pipelines &lt;em&gt;will&lt;/em&gt; break within 18 months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary: MLOps is System Resilience
&lt;/h2&gt;

&lt;p&gt;MLOps isn't a product you buy; it's the discipline that shifts machine learning from a research science experiment into a reliable, scalable production service.&lt;/p&gt;

&lt;p&gt;Ultimately, the infrastructure choices made during the prototype phase—feature contracts, data formats, registry design—become severe technical debt at scale. Establishing a rigorous foundation for your &lt;a href="https://ateljevagabond.se/services/mlops-and-data-management/" rel="noopener noreferrer"&gt;MLOps and Data Management&lt;/a&gt; ensures your system survives production loads without requiring continuous, expensive rework.&lt;/p&gt;

</description>
      <category>mlops</category>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>ai</category>
    </item>
    <item>
      <title>Connecting Physical and Digital: An ESP32 Spotify Display Built on Cloudflare Workers</title>
      <dc:creator>Atelje Vagabond</dc:creator>
      <pubDate>Sat, 11 Apr 2026 15:57:40 +0000</pubDate>
      <link>https://dev.to/ateljevagabond/connecting-physical-and-digital-an-esp32-spotify-display-built-on-cloudflare-workers-3j3l</link>
      <guid>https://dev.to/ateljevagabond/connecting-physical-and-digital-an-esp32-spotify-display-built-on-cloudflare-workers-3j3l</guid>
      <description>&lt;p&gt;At &lt;a href="https://ateljevagabond.se" rel="noopener noreferrer"&gt;Ateljé Vagabond&lt;/a&gt; we build small, well-crafted systems. This article is about one of them: a 1.83-inch ESP32 display on the studio desk that shows what is currently playing on Spotify — and the Cloudflare Worker behind it that simultaneously powers a live widget on our website.&lt;/p&gt;

&lt;p&gt;The interesting part is not the display itself. It is the architecture that makes it work without storing credentials in firmware, without duplicating API calls, and without two separate Spotify integrations fighting each other. One Worker at the edge handles everything: OAuth, token refresh, PNG-to-RGB565 image conversion, caching, and authentication for two completely different types of client.&lt;/p&gt;

&lt;p&gt;This post covers the full system — the hardware quirks, the Worker design, the zero-heap-allocation JSON parser on the microcontroller, the OTA update pipeline, and why it is built the way it is.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/5jMgarDr5s8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Call Spotify Directly From the Device?
&lt;/h2&gt;

&lt;p&gt;We wanted a small LCD on the studio desk that shows what is currently playing on Spotify. We had an ESP32 and a 1.83-inch display. The obvious first idea was to call the Spotify API directly from the firmware.&lt;/p&gt;

&lt;p&gt;In practice, this does not work well. The Spotify Web API uses OAuth tokens that expire every hour. Storing a client secret in firmware is not safe — anyone who reads the flash has your credentials. The device would also need to handle token refresh, manage TLS certificates, and stay within rate limits, all on a microcontroller with limited memory. We also wanted the same now-playing data shown as a widget on our website. That would mean two separate clients calling Spotify independently, which doubles the rate-limit risk and doubles the credential exposure for no good reason.&lt;/p&gt;

&lt;p&gt;The right solution is one backend that owns the Spotify communication, and serves both clients from there. That is what we built.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;A Cloudflare Worker sits between Spotify and everything else. It holds the OAuth credentials, refreshes the token, calls &lt;code&gt;/currently-playing&lt;/code&gt;, and returns different responses depending on who is asking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  ┌─────────────────────────────┐
                  │      Cloudflare Worker       │
                  │   (iot.ateljevagabond.se)    │
                  │                             │
  Spotify API ───►│ - Token refresh             │
                  │ - /currently-playing fetch  │
                  │ - PNG → RGB565 + RLE encode │
                  │ - KV cover cache (7 days)   │
                  └──────────┬──────────────────┘
                             │
              ┌──────────────┴──────────────┐
              │                             │
   IoT Device Request              Browser Request
  (mTLS / gateway secret)        (Origin header check)
              │                             │
              ▼                             ▼
    ┌──────────────────┐       ┌────────────────────────┐
    │  ESP32 + ST7789  │       │  ateljevagabond.se      │
    │  Waveshare 1.83" │       │  Now Playing Web Widget │
    │  240×280 pixels  │       │  (live in your browser) │
    └──────────────────┘       └────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Spotify API is called once per polling cycle — one token refresh, one &lt;code&gt;currently-playing&lt;/code&gt; fetch — regardless of how many clients are connected. The browser gets a JSON response with the cover URL. The ESP32 gets the same JSON plus the album art already converted to the binary pixel format the display expects: RGB565, run-length encoded, base64-encoded so it travels safely inside a JSON string. Cloudflare's edge cache means the Worker does not execute on every single poll. Spotify rate limits stop being a concern.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: The Hardware and Its Quirks
&lt;/h2&gt;

&lt;p&gt;The hardware is a &lt;a href="https://www.dfrobot.com/product-1590.html" rel="noopener noreferrer"&gt;DFRobot FireBeetle ESP32&lt;/a&gt; connected to a &lt;a href="https://www.waveshare.com/1.83inch-lcd-module.htm" rel="noopener noreferrer"&gt;Waveshare 1.83-inch IPS LCD&lt;/a&gt; with an ST7789 controller — 240×280 pixels over SPI.&lt;/p&gt;

&lt;p&gt;Getting the display to render correctly took longer than expected. Three PlatformIO flags are essential and none are documented clearly for this specific panel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TFT_OFFSET_Y=20&lt;/code&gt; — The ST7789 controller's internal frame buffer is 240×320, but the 1.83" panel only uses 240×280, starting at row 20. Without this, every image renders shifted upward, clipped at the top.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TFT_RGB_ORDER=TFT_BGR&lt;/code&gt; — This panel swaps the red and blue channels. Without this flag, reds look blue and blues look red.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TFT_INVERSION_ON&lt;/code&gt; — The panel requires color inversion enabled. Without this, everything renders as a washed-out gray.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the kind of things that datasheets should say clearly. We found all three through trial and error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming, Not Buffering
&lt;/h3&gt;

&lt;p&gt;The ESP32 has limited heap. Loading an entire 115 KB RGB565 image into RAM before displaying it is not reliable. Instead, the firmware streams the cover image response in 512-byte steps. Two FreeRTOS tasks run in parallel: one fetches track metadata every 3 seconds, the other handles the image stream and writes pixels to the display as they arrive. A &lt;code&gt;tftMutex&lt;/code&gt; serializes all display writes between the two tasks, and a watchdog triggers a soft display reset if the lock fails repeatedly — a necessary guard when two tasks compete for the same SPI bus.&lt;/p&gt;

&lt;p&gt;The result is that album art appears from top to bottom while it is still downloading — not something we planned explicitly, but a natural consequence of the streaming approach, and we kept it.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Parser With No Heap Allocation
&lt;/h3&gt;

&lt;p&gt;The main JSON response includes the encoded pixel data, which makes it too large to deserialize with a standard library without risking heap fragmentation. We wrote a character-by-character state machine instead. It parses the JSON as bytes arrive, and when it reaches the &lt;code&gt;cover_rgb565&lt;/code&gt; field it switches into decode mode: every four base64 characters produce three raw bytes, which go directly into the RLE decoder, which expands them into pixel values and writes them to the display. The full image passes through the device without ever being held in memory as a whole.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paused and Idle
&lt;/h3&gt;

&lt;p&gt;After 60 seconds without playback the display switches to an alternate screen: the studio boot image, the current time, and the date. Time is synced on boot via NTP from &lt;code&gt;time.cloudflare.com&lt;/code&gt;. The track title scrolls as a marquee when it is too long for the display width.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: The Cloudflare Worker
&lt;/h2&gt;

&lt;p&gt;The Worker is a single TypeScript file. It handles two very different clients — a browser and an embedded device — with one &lt;code&gt;fetch&lt;/code&gt; handler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication Without Shared State
&lt;/h3&gt;

&lt;p&gt;Browsers and ESP32s authenticate differently. Browsers send an &lt;code&gt;Origin&lt;/code&gt; header; the Worker checks it against an allowlist and handles CORS normally. The ESP32 sends no &lt;code&gt;Origin&lt;/code&gt; header; instead it presents a client certificate over mTLS. The certificate and private key are stored in LittleFS on the device. Cloudflare's infrastructure verifies the certificate before the request reaches the Worker.&lt;/p&gt;

&lt;p&gt;As a fallback for environments where mTLS is not available, the Worker also accepts a shared secret in a custom header. The comparison uses a constant-time function to prevent timing attacks — a small detail that matters when the secret is all that stands between an attacker and your Spotify data.&lt;/p&gt;

&lt;p&gt;Neither authentication path exposes the Spotify credentials. Tokens live in the Worker's environment variables and never leave the edge.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Image Pipeline
&lt;/h3&gt;

&lt;p&gt;This was the most technically interesting part. When the ESP32 requests the cover image, the Worker:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetches the album art PNG from Spotify's CDN&lt;/li&gt;
&lt;li&gt;Decodes the PNG using a pure TypeScript implementation with no external dependencies&lt;/li&gt;
&lt;li&gt;Converts every pixel to RGB565 — the native 16-bit format of the ST7789 display: 5 bits red, 6 bits green, 5 bits blue&lt;/li&gt;
&lt;li&gt;Run-length encodes the result. Album art compresses well — large areas of similar color are common. A 115 KB raw image typically becomes 20–40 KB&lt;/li&gt;
&lt;li&gt;Base64-encodes the compressed data so it fits inside a JSON string&lt;/li&gt;
&lt;li&gt;Caches the result in Cloudflare KV for 7 days, keyed by a hash of the source URL and encoding parameters&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The expensive work — PNG decoding, pixel conversion, RLE encoding — happens once per unique album cover. Every subsequent request for the same cover hits KV directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: The Website Widget
&lt;/h2&gt;

&lt;p&gt;The website at &lt;a href="https://ateljevagabond.se" rel="noopener noreferrer"&gt;ateljevagabond.se&lt;/a&gt; calls the same Worker endpoint, without the cover encoding parameters. It gets a plain JSON response and renders the track name, artist, cover image, and a progress bar. The widget polls every 5 seconds. No separate backend. No separate Spotify integration. The same Worker call, a different response shape.&lt;/p&gt;




&lt;h2&gt;
  
  
  Four Days From First Commit to OTA Updates
&lt;/h2&gt;

&lt;p&gt;The Worker came first. We built it in November 2025 while working on the main website. The now-playing feature grew from there: cover image proxying, then the RGB565 conversion, then mTLS authentication.&lt;/p&gt;

&lt;p&gt;The ESP32 firmware started on December 26, 2025. The commit log shows the actual progression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dec 26 13:15 — Initial commit with CI
Dec 26 15:16 — Improve display recovery and add Husky hooks
Dec 26 22:40 — Use CA bundle and speed up cover fetch
Dec 28 22:42 — Add OTA update flow and release workflow
Dec 28 23:30 — Add paused/idle UI behavior
Dec 28 23:38 — Use remote R2 for OTA uploads
Dec 29 00:02 — Gate OTA release on CI and document Husky
Dec 29 10:36 — chore(ci): avoid duplicate runs and stage firmware
Dec 29 11:19 — chore(ci): upload to R2 via S3 endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four days from first commit to a working CI pipeline, OTA firmware updates via Cloudflare R2, a display recovery watchdog, paused/idle UI states, and Husky hooks enforcing code quality. The OTA setup works like this: on boot, the device fetches a JSON manifest from the Worker containing the latest version string, binary URL, SHA256 hash, and size. If the version is newer, it downloads the binary over HTTPS and performs an A/B partition swap — no USB cable needed after the first flash. GitHub Actions builds the firmware with &lt;code&gt;APP_VERSION&lt;/code&gt; injected at compile time, uploads the binary to R2, and publishes the manifest.&lt;/p&gt;

&lt;p&gt;One commit worth noting: &lt;code&gt;"Dump wrangler logs on OTA failure"&lt;/code&gt; on December 29. The OTA uploads to R2 were failing silently in CI — Wrangler was swallowing its own error output. The fix was two lines: pipe the output to a file and print it. Once we could see the error, the underlying issue was clear and fixed in minutes. Silent failures waste time. Making failures visible is part of good engineering.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Have Now
&lt;/h2&gt;

&lt;p&gt;On the studio desk, a 1.83-inch display shows the current track, artist, album cover, a progress bar that advances locally between fetches, and the remaining time. After one minute without playback it switches to a clock and date screen.&lt;/p&gt;

&lt;p&gt;On the website, the same playback state appears as a small widget, updating every 5 seconds.&lt;/p&gt;

&lt;p&gt;The Spotify API sees one token refresh and one &lt;code&gt;currently-playing&lt;/code&gt; call per polling cycle, regardless of how many clients are connected. The KV cache means the expensive work — PNG decoding and RGB565 conversion — happens at most once per unique album cover over a 7-day window.&lt;/p&gt;

&lt;p&gt;The security is straightforward. The ESP32 identifies itself with a client certificate. Browsers are restricted by CORS and the Origin allowlist. Spotify credentials never leave the Worker's environment variables.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Source Code
&lt;/h2&gt;

&lt;p&gt;We have open-sourced the complete system. The repository contains both the ESP32 firmware and the Cloudflare Worker code, with full setup instructions, mTLS configuration, and the CI/CD pipeline for OTA releases.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Atelje-Vagabond/esp32-spotify-cloudflare-worker" rel="noopener noreferrer"&gt;View the project on GitHub: esp32-spotify-cloudflare-worker&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  See It Live
&lt;/h2&gt;

&lt;p&gt;Go to &lt;strong&gt;&lt;a href="https://ateljevagabond.se" rel="noopener noreferrer"&gt;ateljevagabond.se&lt;/a&gt;&lt;/strong&gt; — if something is playing at the studio right now, you will see it in the Now Playing widget. Same Worker, same data, two different consumers.&lt;/p&gt;




&lt;h2&gt;
  
  
  About Ateljé Vagabond
&lt;/h2&gt;

&lt;p&gt;This project is an example of what we do: build focused, well-engineered systems where hardware and software work together cleanly. If you are working on something similar — edge computing, IoT + cloud integration, or custom Cloudflare Worker architectures — we would like to hear from you at &lt;a href="https://ateljevagabond.se" rel="noopener noreferrer"&gt;ateljevagabond.se&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with PlatformIO, TFT_eSPI, ArduinoJson, and Cloudflare Workers. Most of the engineering happened between Christmas and New Year.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>esp32</category>
      <category>cloudflare</category>
      <category>iot</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
