<?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: Guilherme Vassoller Daros</title>
    <description>The latest articles on DEV Community by Guilherme Vassoller Daros (@guilherme_daros).</description>
    <link>https://dev.to/guilherme_daros</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3641251%2Fe1c7b285-7086-45c0-a184-4450c5a95d08.jpg</url>
      <title>DEV Community: Guilherme Vassoller Daros</title>
      <link>https://dev.to/guilherme_daros</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/guilherme_daros"/>
    <language>en</language>
    <item>
      <title>Airflow Scheduling: Assets vs. Cron | Which One Should You Use?</title>
      <dc:creator>Guilherme Vassoller Daros</dc:creator>
      <pubDate>Sat, 29 Aug 2026 20:25:17 +0000</pubDate>
      <link>https://dev.to/guilherme_daros/airflow-scheduling-assets-vs-cron-which-one-should-you-use-2gjd</link>
      <guid>https://dev.to/guilherme_daros/airflow-scheduling-assets-vs-cron-which-one-should-you-use-2gjd</guid>
      <description>&lt;p&gt;Sometimes, a change that looks simple on the surface is not actually that simple.&lt;/p&gt;

&lt;p&gt;Imagine that you need to replace the source table feeding a refined or trusted table in a data pipeline. At first, it might look like a one-line change: update the table name, deploy the code, and move on.&lt;/p&gt;

&lt;p&gt;But in a real data platform, there is usually much more behind that change.&lt;/p&gt;

&lt;p&gt;There are dependencies, scheduling rules, upstream and downstream processes, resource consumption, concurrency, data lineage, and, sometimes, assumptions that were not immediately obvious when the pipeline was first created.&lt;/p&gt;

&lt;p&gt;I recently had to look into exactly this kind of situation in an Apache Airflow project, and one of the questions that came up was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Should this DAG be scheduled using a cron expression, or should it be triggered based on an Asset?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer, as usual in software engineering, is: &lt;strong&gt;it depends.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And understanding why it depends is much more important than simply knowing how to configure either option.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cron: the familiar way of scheduling a DAG
&lt;/h2&gt;

&lt;p&gt;Let's start with the simplest and most familiar option: a time-based schedule.&lt;/p&gt;

&lt;p&gt;With Airflow, we can define a DAG to run according to a cron expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my_pipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0 13 * * 0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;catchup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, the DAG is scheduled to run every Sunday at 1 PM.&lt;/p&gt;

&lt;p&gt;In a real environment, we might have different schedules for different environments.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;Schedule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Development&lt;/td&gt;
&lt;td&gt;Saturday at 1 PM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Homologation&lt;/td&gt;
&lt;td&gt;Sunday at 1 PM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;Monday–Friday at 1 PM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important characteristic here is that &lt;strong&gt;the schedule is based on time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If the DAG is configured to run at 1 PM every Sunday, Airflow will try to run it at that time, regardless of whether the data it depends on has actually changed.&lt;/p&gt;

&lt;p&gt;This is not necessarily a bad thing.&lt;/p&gt;

&lt;p&gt;In fact, sometimes this is exactly what we want.&lt;/p&gt;

&lt;p&gt;But there is another approach.&lt;/p&gt;




&lt;h1&gt;
  
  
  When data becomes part of the schedule
&lt;/h1&gt;

&lt;p&gt;Modern data pipelines often have dependencies that are better described in terms of data rather than time.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw table
    ↓
Trusted table
    ↓
Refined table
    ↓
Analytics / ML / BI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Run the refined pipeline every Sunday at 1 PM."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;we might want to say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Run the refined pipeline whenever the trusted data has been successfully updated."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is where Airflow's &lt;strong&gt;Assets&lt;/strong&gt; become interesting.&lt;/p&gt;

&lt;p&gt;Assets were introduced in Airflow 2.4 under the name &lt;strong&gt;Datasets&lt;/strong&gt;. In Airflow 3.0, the concept was renamed from Dataset to Asset, aligning the terminology with the broader data ecosystem.&lt;/p&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the availability or update of data can become a scheduling event.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Inlets, outlets, and data lineage
&lt;/h1&gt;

&lt;p&gt;To understand this concept, it helps to look at two terms: &lt;strong&gt;inlets&lt;/strong&gt; and &lt;strong&gt;outlets&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An inlet represents something a task consumes.&lt;/p&gt;

&lt;p&gt;An outlet represents something a task produces or updates.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             inlet
               ↓
        ┌─────────────┐
        │   Task      │
        │             │
        └─────────────┘
               ↓
             outlet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, imagine a task that reads a raw table and produces a trusted table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;raw.customers
      │
      │ inlet
      ▼
┌─────────────────────┐
│ transform_customers │
└─────────────────────┘
      │
      │ outlet
      ▼
trusted.customers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The upstream table is the input.&lt;/p&gt;

&lt;p&gt;The trusted table is the output.&lt;/p&gt;

&lt;p&gt;This relationship is useful not only for scheduling, but also for understanding &lt;strong&gt;data lineage&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Airflow describes this idea as a way to track where data comes from, what happens to it, and where it moves over time. This can support audit trails, data governance, and debugging of data flows.&lt;/p&gt;

&lt;p&gt;Airflow's documentation summarizes the idea nicely:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Airflow tracks data by means of inlets and outlets of the tasks."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words, the relationship between what a task consumes and what it produces becomes part of the workflow's metadata.&lt;/p&gt;

&lt;p&gt;And this is where things start getting interesting.&lt;/p&gt;




&lt;h1&gt;
  
  
  A simple Asset example
&lt;/h1&gt;

&lt;p&gt;Suppose we have a task that produces a table represented as an Asset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Asset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="n"&gt;trusted_customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://warehouse/trusted/customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outlets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;trusted_customers&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_trusted_customers&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Transform raw data
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now another DAG can depend on that Asset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Asset&lt;/span&gt;

&lt;span class="n"&gt;trusted_customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres://warehouse/trusted/customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refined_customers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;trusted_customers&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;catchup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conceptually, the dependency becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Producer DAG
     │
     │ updates
     ▼
trusted.customers
     │
     │ triggers
     ▼
Consumer DAG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consumer does not need to guess when the upstream pipeline will finish.&lt;/p&gt;

&lt;p&gt;It reacts to the data event.&lt;/p&gt;

&lt;p&gt;This is what makes Asset-based scheduling particularly interesting for data-driven architectures.&lt;/p&gt;




&lt;h1&gt;
  
  
  So, Asset or Cron?
&lt;/h1&gt;

&lt;p&gt;This was the part that made the problem more interesting for me.&lt;/p&gt;

&lt;p&gt;At first, it is tempting to think:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If Assets are more dynamic and data-aware, shouldn't we just use Assets everywhere?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I don't think so.&lt;/p&gt;

&lt;p&gt;There is no universally better option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cron and Assets solve different problems.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A useful way to think about it is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cron answers "when?"&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Assets answer "after what?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And depending on the pipeline, one question may be much more important than the other.&lt;/p&gt;




&lt;h1&gt;
  
  
  Cron is predictable
&lt;/h1&gt;

&lt;p&gt;Let's say we have a pipeline that should run every Sunday at 1 PM.&lt;/p&gt;

&lt;p&gt;With Cron:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0 13 * * 0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the intention is extremely clear.&lt;/p&gt;

&lt;p&gt;Every Sunday at 1 PM, the DAG should run.&lt;/p&gt;

&lt;p&gt;It doesn't matter whether the upstream table was updated once, twice, or ten times during the week.&lt;/p&gt;

&lt;p&gt;The schedule defines the cadence.&lt;/p&gt;

&lt;p&gt;This can be very useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the processing is inherently time-based;&lt;/li&gt;
&lt;li&gt;the source system follows a fixed delivery schedule;&lt;/li&gt;
&lt;li&gt;running more frequently would provide no benefit;&lt;/li&gt;
&lt;li&gt;you want strict control over execution frequency;&lt;/li&gt;
&lt;li&gt;the downstream processing is expensive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Predictability can be a feature.&lt;/p&gt;




&lt;h1&gt;
  
  
  Assets are data-aware
&lt;/h1&gt;

&lt;p&gt;Now imagine that our trusted table is updated whenever new raw data arrives.&lt;/p&gt;

&lt;p&gt;We could make the downstream DAG depend on that Asset.&lt;/p&gt;

&lt;p&gt;That sounds great.&lt;/p&gt;

&lt;p&gt;And often, it is.&lt;/p&gt;

&lt;p&gt;But there is an important detail:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How often is that Asset updated?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's say we have a large table that takes two hours to process.&lt;/p&gt;

&lt;p&gt;It consumes a significant amount of cluster resources.&lt;/p&gt;

&lt;p&gt;Previously, the pipeline ran twice a week, and that was enough.&lt;/p&gt;

&lt;p&gt;Now someone decides to "modernize" the architecture:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Let's make this task Asset-driven."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The upstream raw table, however, is updated three times a day.&lt;/p&gt;

&lt;p&gt;Now the dependency looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw table
   │
   ├── update #1 ──► Asset event ──► expensive pipeline
   │
   ├── update #2 ──► Asset event ──► expensive pipeline
   │
   └── update #3 ──► Asset event ──► expensive pipeline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The downstream pipeline can now run three times a day.&lt;/p&gt;

&lt;p&gt;That might be exactly what we want.&lt;/p&gt;

&lt;p&gt;Or it might be a disaster.&lt;/p&gt;

&lt;p&gt;If each execution takes two hours and heavily consumes the cluster, we may suddenly have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unnecessary executions;&lt;/li&gt;
&lt;li&gt;increased cluster utilization;&lt;/li&gt;
&lt;li&gt;task concurrency;&lt;/li&gt;
&lt;li&gt;longer queues;&lt;/li&gt;
&lt;li&gt;contention with other pipelines;&lt;/li&gt;
&lt;li&gt;higher infrastructure costs;&lt;/li&gt;
&lt;li&gt;potentially overlapping runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing is technically wrong with the Asset configuration.&lt;/p&gt;

&lt;p&gt;The problem is that &lt;strong&gt;the data frequency and the processing frequency are not necessarily the same thing.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Data frequency is not always processing frequency
&lt;/h1&gt;

&lt;p&gt;This is probably the most important lesson I took from this experience.&lt;/p&gt;

&lt;p&gt;Just because data changes does not mean that the downstream pipeline should process that change immediately.&lt;/p&gt;

&lt;p&gt;Consider this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source
  │
  ├── 08:00 update
  ├── 12:00 update
  └── 18:00 update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An Asset-driven DAG could potentially react to all three events.&lt;/p&gt;

&lt;p&gt;But perhaps the business only needs the transformation to happen once per day.&lt;/p&gt;

&lt;p&gt;In that case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source updates
  │
  ├── 08:00
  ├── 12:00
  └── 18:00
          │
          ▼
    Daily processing
          │
          ▼
      Final table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Cron schedule might actually be the better design.&lt;/p&gt;

&lt;p&gt;On the other hand, if every update needs to be available downstream as quickly as possible, an Asset-based approach could be much more appropriate.&lt;/p&gt;

&lt;p&gt;The point is not to choose the more modern feature.&lt;/p&gt;

&lt;p&gt;The point is to choose the scheduling model that matches the actual behavior of the system.&lt;/p&gt;




&lt;h1&gt;
  
  
  A simple decision framework
&lt;/h1&gt;

&lt;p&gt;When deciding between Cron and Assets, I like to think about a few questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Is time the real dependency?
&lt;/h3&gt;

&lt;p&gt;If the pipeline needs to run at a specific cadence regardless of upstream changes, Cron is probably a good fit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Every Sunday
     ↓
   Run DAG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Is data availability the real dependency?
&lt;/h3&gt;

&lt;p&gt;If the downstream pipeline should only run after upstream data has been successfully produced, Assets may be a better fit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Data updated
     ↓
Asset event
     ↓
Run DAG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. How frequently can the Asset be updated?
&lt;/h3&gt;

&lt;p&gt;This is easy to overlook.&lt;/p&gt;

&lt;p&gt;Before switching from Cron to Assets, ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How many times can this Asset emit an event?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A source that updates once a day is very different from a source that updates every few minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. How expensive is the downstream processing?
&lt;/h3&gt;

&lt;p&gt;A lightweight task running frequently may be perfectly fine.&lt;/p&gt;

&lt;p&gt;A two-hour transformation consuming a large cluster is a different story.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Do we actually need immediate processing?
&lt;/h3&gt;

&lt;p&gt;Sometimes the answer is no.&lt;/p&gt;

&lt;p&gt;A pipeline can be data-dependent without being required to react to every single data update.&lt;/p&gt;




&lt;h1&gt;
  
  
  Asset-driven does not automatically mean better
&lt;/h1&gt;

&lt;p&gt;There is a tendency in engineering to associate newer or more dynamic features with better architecture.&lt;/p&gt;

&lt;p&gt;But adding a dependency-aware scheduler to a pipeline that was perfectly suited to a fixed schedule can actually make the system more complex.&lt;/p&gt;

&lt;p&gt;The important question is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can we use Assets here?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Should this pipeline react to every upstream data event?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are very different questions.&lt;/p&gt;

&lt;p&gt;Assets can provide a much clearer representation of data dependencies and can make workflows more event-driven.&lt;/p&gt;

&lt;p&gt;But they also introduce a different execution model.&lt;/p&gt;

&lt;p&gt;And that model needs to be understood before changing an existing pipeline.&lt;/p&gt;




&lt;h1&gt;
  
  
  The bigger picture: scheduling is part of architecture
&lt;/h1&gt;

&lt;p&gt;What initially looks like a small scheduling configuration can actually be an architectural decision.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0 13 * * 0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;some_asset&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not simply changing syntax.&lt;/p&gt;

&lt;p&gt;You are changing &lt;strong&gt;what causes the pipeline to execute&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With Cron, the cause is time.&lt;/p&gt;

&lt;p&gt;With Assets, the cause is a data event.&lt;/p&gt;

&lt;p&gt;That difference can propagate through the entire platform.&lt;/p&gt;

&lt;p&gt;It can affect resource utilization, execution frequency, concurrency, downstream dependencies, observability, and even operational costs.&lt;/p&gt;

&lt;p&gt;This is why I think scheduling deserves more attention when designing data pipelines.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final thoughts
&lt;/h1&gt;

&lt;p&gt;After looking into this, my conclusion was surprisingly simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no "best" scheduling strategy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is only the strategy that best matches the behavior and requirements of a particular pipeline.&lt;/p&gt;

&lt;p&gt;Cron is not outdated just because Assets are more data-aware.&lt;/p&gt;

&lt;p&gt;Assets are not automatically better just because they provide dynamic dependencies.&lt;/p&gt;

&lt;p&gt;Sometimes you want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Time → Pipeline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And sometimes you want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Data → Pipeline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is understanding which one represents the real dependency.&lt;/p&gt;

&lt;p&gt;Before replacing a Cron schedule with an Asset, I would ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How often does the upstream data change?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;How often should the downstream process actually run?&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;And how much does each execution cost?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If those three answers don't align, blindly switching to Asset-based scheduling may create more problems than it solves.&lt;/p&gt;

&lt;p&gt;And perhaps that is the broader lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In data engineering, making a pipeline more dynamic does not necessarily make it better. Sometimes, being explicit and predictable is exactly what the system needs.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>architecture</category>
      <category>data</category>
      <category>devops</category>
    </item>
    <item>
      <title>How I Built a Intelligent AgTech Risk Monitoring System: Architecture, Technical Decisions, and Key Learnings</title>
      <dc:creator>Guilherme Vassoller Daros</dc:creator>
      <pubDate>Thu, 15 Jan 2026 01:04:34 +0000</pubDate>
      <link>https://dev.to/guilherme_daros/how-i-built-a-intelligent-agtech-risk-monitoring-system-architecture-technical-decisions-and-key-1n5a</link>
      <guid>https://dev.to/guilherme_daros/how-i-built-a-intelligent-agtech-risk-monitoring-system-architecture-technical-decisions-and-key-1n5a</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This project was developed as part of the Hardware Architecture course at my university. Our team set out to build a simple yet powerful system where we could experiment with different technologies, sensors, and hardware components. It was our first hands-on experience working with sensors and embedded systems, so we aimed to create a solution that was fast, scalable, and accessible.&lt;/p&gt;

&lt;p&gt;In this article, I walk through the project’s architecture, the technologies used, the challenges we faced, and the key lessons learned throughout the development process.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Main Features&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time monitoring: Continuously collects environmental data (temperature, humidity, and luminosity) from sensors connected to an Arduino.&lt;/li&gt;
&lt;li&gt;Asynchronous data pipeline: Uses a message queue (RabbitMQ) to reliably transmit sensor readings for analysis and storage.&lt;/li&gt;
&lt;li&gt;Risk analysis engine: Processes sensor data to compute risk levels for pest outbreaks, with multi-tier alert levels.&lt;/li&gt;
&lt;li&gt;Dashboard interface: Interactive web dashboard built with Next.js that displays real-time and historical visualizations.&lt;/li&gt;
&lt;li&gt;Scalable architecture: Designed with distributed components that can scale independently and adapt to multiple crop types.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/guiDaros/project_agtech_hdwach" rel="noopener noreferrer"&gt;https://github.com/guiDaros/project_agtech_hdwach&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hardware&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Arduino Uno with environmental sensors (DHT11, HW080, LDR)&lt;/li&gt;
&lt;li&gt;Raspberry Pi (for backend services)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Backend&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.8+ with the Flask framework&lt;/li&gt;
&lt;li&gt;RabbitMQ (CloudAMQP) for asynchronous messaging&lt;/li&gt;
&lt;li&gt;Redis (Upstash) for near real-time data caching&lt;/li&gt;
&lt;li&gt;SQLite for historical data storage&lt;/li&gt;
&lt;li&gt;PySerial to communicate with the Arduino&lt;/li&gt;
&lt;li&gt;Pandas for data analysis&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js (React framework) with TypeScript&lt;/li&gt;
&lt;li&gt;Tailwind CSS and shadcn/ui for components&lt;/li&gt;
&lt;li&gt;Recharts for interactive data visualizations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tools&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git for version control&lt;/li&gt;
&lt;li&gt;VSCode as the main editor&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  System Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  High-level system flow
&lt;/h3&gt;

&lt;p&gt;Sensors → Arduino → Message Queue → Backend → Database → Frontend&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%2Frqr8rkw69rjz6anpyl5g.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.amazonaws.com%2Fuploads%2Farticles%2Frqr8rkw69rjz6anpyl5g.png" alt="entire sys image" width="799" height="391"&gt;&lt;/a&gt;&lt;br&gt;
Development setup during testing and validation.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why this architecture was chosen
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Asynchronous Messaging&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RabbitMQ is used to decouple producers and consumers through asynchronous, message-driven communication. This allows system components to operate independently, remain resilient to failures, and support multiple processing paths such as real-time streaming, historical storage, and analytics. The same structure also enables future extensions, including machine learning services, without changing the ingestion layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separation of System Layers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The system is organized into hardware, backend, and frontend layers, responsible for data acquisition, processing and storage, and visualization. This separation of concerns improves maintainability, simplifies testing, and allows components to evolve and scale independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why RabbitMQ Instead of Direct HTTP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RabbitMQ was chosen over direct HTTP to handle the realities of a distributed agricultural environment, including network latency, intermittent connectivity, and partial failures. By providing buffering, reliable delivery, and asynchronous processing, the message broker ensures that sensor data is not lost and can be consumed at different rates by downstream services.&lt;/p&gt;




&lt;h3&gt;
  
  
  How data moves through the system
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The system follows an event-driven, ELT-oriented data pipeline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sensor readings are collected by Arduino-connected devices and sent to the backend via a serial connection, where they are published to RabbitMQ as raw events. The data remains unprocessed at this stage to preserve its original form and avoid coupling ingestion with transformation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;RabbitMQ acts as the ingestion layer, routing messages to dedicated queues for different consumers. Raw data is then loaded into storage, with SQLite used for historical persistence and Redis for fast access to recent readings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Downstream analysis services consume messages from the queues, process sensor data, and compute derived metrics such as environmental risk indicators for pest and fungus development. These refined results are then delivered to a React and Next.js frontend, which renders real-time and historical dashboards.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Challenges and How We Addressed Them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Ensuring Reliable Sensor Data Ingestion
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;One of the main challenges was designing a reliable ingestion mechanism for sensor data in a distributed environment. Sensor readings are generated continuously and originate from hardware components that are inherently prone to noise, temporary failures, and unstable communication.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To address this, the system was designed around asynchronous messaging. Instead of tightly coupling data producers and consumers through direct communication, sensor readings are published as events to RabbitMQ. This approach allows data to be buffered, retried, and processed independently of ingestion, increasing fault tolerance and system resilience.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;By decoupling hardware data acquisition from downstream processing, the system remains operational even when individual services become temporarily unavailable.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Balancing Real-Time Processing and Analytical Flexibility
&lt;/h3&gt;

&lt;p&gt;Another challenge was designing a solution that could handle both immediate real-time visualization and future analytical needs without major changes.&lt;/p&gt;

&lt;p&gt;By preserving raw data and separating different types of consumption, the system ensures flexibility. Dashboards, historical analysis, or predictive models can be added or improved independently, without disrupting existing processes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The system successfully integrated hardware sensors, asynchronous data ingestion, backend processing, and a web-based dashboard into a single working solution.&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.amazonaws.com%2Fuploads%2Farticles%2Ffcx3sdanzekh79u9gfoz.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.amazonaws.com%2Fuploads%2Farticles%2Ffcx3sdanzekh79u9gfoz.png" alt=" " width="800" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Sensor readings were collected, transmitted, processed, and visualized in near real time, enabling continuous monitoring of environmental conditions. Historical data was also stored and accessed for retrospective analysis.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The project was demonstrated in a live presentation setting, where the complete data flow—from sensor acquisition to dashboard visualization—was executed in real time. This validation confirmed the correctness of the system integration and the architectural decisions made during development.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Future Work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Future iterations of this project will focus on extending analytical capabilities and improving data processing quality. One of the next planned steps is the development of an initial predictive model to analyze historical sensor data and estimate the likelihood of pest or disease outbreaks. This first model will serve as an experimental foundation for more advanced predictive approaches.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In parallel, ongoing work is being done to improve the data pipeline itself, with a focus on increasing processing capacity, filtering noisy sensor readings, and producing more reliable inputs for analysis and visualization. These improvements aim to enhance the quality of results without altering the overall system architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Additional enhancements may include integrating new sensors, refining alert mechanisms, and adapting the system for larger-scale or more distributed deployments.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This project provided practical experience in designing and implementing a distributed, event-driven system that integrates hardware, backend services, and a modern web interface.&lt;/p&gt;

&lt;p&gt;Beyond the technical implementation, the project reinforced the importance of architectural decisions such as decoupling, data flow design, and system modularity. Working with real sensor data highlighted the challenges of handling data at the boundary between hardware and software.&lt;/p&gt;

&lt;p&gt;Overall, the project served as a valuable learning experience in applied system design, bridging concepts from hardware architecture, data engineering, and web development.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>beginners</category>
      <category>iot</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
