<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Cloud, Data Engineering & AI Hub]]></title><description><![CDATA[If you are working with AWS Glue and Amazon S3, you may eventually come across a problem that doesn't look serious at first:]]></description><link>https://rahul-dataeng.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Cloud, Data Engineering &amp; AI Hub</title><link>https://rahul-dataeng.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 17 Oct 2026 12:45:31 GMT</lastBuildDate><atom:link href="https://rahul-dataeng.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Stop Reprocessing Everything: Build Incremental Data Pipelines on AWS]]></title><description><![CDATA[How to use AWS Glue, Amazon S3, and PySpark to process only new and changed data without making your pipeline fragile.
Imagine you have a data pipeline that processes 100 GB of data every morning.
The]]></description><link>https://rahul-dataeng.hashnode.dev/stop-reprocessing-everything-build-incremental-data-pipelines-on-aws</link><guid isPermaLink="true">https://rahul-dataeng.hashnode.dev/stop-reprocessing-everything-build-incremental-data-pipelines-on-aws</guid><category><![CDATA[big data]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[AWS Glue]]></category><category><![CDATA[AWS]]></category><category><![CDATA[#apache-spark]]></category><category><![CDATA[PySpark]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Rahul R]]></dc:creator><pubDate>Fri, 16 Oct 2026 15:49:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ab7e1cd7238f0f167fa4d67/1549f68f-9f7e-42fc-8414-b75faece6a21.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>How to use AWS Glue, Amazon S3, and PySpark to process only new and changed data without making your pipeline fragile.</em></p>
<p>Imagine you have a data pipeline that processes 100 GB of data every morning.</p>
<p>The first day, everything works fine. A few weeks later, the dataset grows to 500 GB. After a few months, it reaches several terabytes.</p>
<p>But here is the interesting part: only a small portion of that data changes every day.</p>
<p>Your pipeline still reads everything.</p>
<p>The job takes longer, consumes more compute resources, and spends most of its time processing records that have already been processed before.</p>
<p>I have always found this to be one of the most important questions in data engineering:</p>
<p><strong>If only 2% of the data has changed, why should we process 100% of it again?</strong></p>
<p>This is where incremental data processing becomes useful.</p>
<p>In this article, we will explore how to design an incremental pipeline using Amazon S3, AWS Glue, and PySpark, including the problems that appear when the pipeline starts running in production.</p>
<h2>1. Full Refresh vs. Incremental Processing</h2>
<p>Let's start with a simple example.</p>
<p>Suppose an application exports customer records to Amazon S3 every day.</p>
<p>Your raw dataset contains 10 million records, but only 50,000 records are new or updated today.</p>
<p>With a full refresh, your pipeline might read and transform all 10 million records.</p>
<p>With incremental processing, it attempts to process only the records that have changed since the previous successful run.</p>
<table>
<thead>
<tr>
<th>Approach</th>
<th>Records to process</th>
</tr>
</thead>
<tbody><tr>
<td>Full refresh</td>
<td>10,000,000</td>
</tr>
<tr>
<td>Incremental processing</td>
<td>50,000</td>
</tr>
<tr>
<td>Potential reduction in records processed</td>
<td>99.5%</td>
</tr>
</tbody></table>
<p>These figures are illustrative, not measured benchmark results. Actual cost and runtime savings depend on file sizes, partition layout, transformation complexity, and the amount of data that must be scanned.</p>
<p>Incremental processing does not automatically make every pipeline faster. If your source contains millions of tiny files, for example, discovering and listing those files can remain expensive even when the number of new records is small.</p>
<p>Still, avoiding unnecessary work is a good starting point.</p>
<h2>2. A Simple AWS Architecture</h2>
<p>For this example, imagine that customer data arrives in Amazon S3 and AWS Glue transforms it into a curated dataset.</p>
<p>The flow looks like this:</p>
<pre><code class="language-text">Source Application
        |
        v
Amazon S3 - Raw Layer
        |
        v
AWS Glue + PySpark
        |
        +---- Read New or Changed Data
        |
        +---- Validate and Transform
        |
        v
Amazon S3 - Curated Layer
        |
        v
Downstream Analytics
</code></pre>
<p>The important part is deciding how the job knows which records are new.</p>
<p>There are several ways to do this:</p>
<ul>
<li><p><strong>AWS Glue job bookmarks:</strong> Track processing progress for supported sources and transformations.</p>
</li>
<li><p><strong>Watermarks:</strong> Store a previously processed timestamp or another ordered change marker.</p>
</li>
<li><p><strong>CDC (Change Data Capture):</strong> Consume inserts, updates, and deletes from a change stream or database log.</p>
</li>
<li><p><strong>Manifest-based ingestion:</strong> Track which source files have been successfully processed.</p>
</li>
</ul>
<p>These mechanisms solve related but different problems. A file bookmark does not automatically provide record-level change detection inside every file, and a timestamp watermark does not automatically handle deletes.</p>
<p>For our example, we will use a timestamp watermark to explain the core idea.</p>
<h2>3. Build an Incremental Pipeline with PySpark</h2>
<p>Assume the source provides customer records with the following columns:</p>
<pre><code class="language-text">customer_id
customer_name
email
updated_at
</code></pre>
<p>The <code>updated_at</code> field represents when the source record was last changed.</p>
<p>For the first run, the pipeline may need to process the entire available dataset. Subsequent runs can use the saved watermark to identify a smaller set of changes.</p>
<h3>Step 1: Read the previous watermark</h3>
<p>A watermark is a marker representing the last point in the source data that the pipeline has successfully processed.</p>
<p>For a real implementation, store this state durably in a system such as DynamoDB, a control table, or another suitable metadata store.</p>
<p>Here is a simplified example using a value supplied to the Glue job:</p>
<pre><code class="language-python">from datetime import datetime, timezone

previous_watermark = "2026-10-15T00:00:00Z"

watermark = datetime.fromisoformat(
    previous_watermark.replace("Z", "+00:00")
)

print(f"Previous watermark: {watermark}")
</code></pre>
<p>For an initial load, you need an explicit bootstrap strategy rather than assuming a previous watermark already exists.</p>
<p>Also, don't use the current clock time as the new watermark merely because the job started. The checkpoint must reflect data that has actually been processed successfully.</p>
<h3>Step 2: Read and filter the source data</h3>
<p>For this example, assume the source data is stored as Parquet files in S3 and the dataset fits the chosen scan strategy.</p>
<pre><code class="language-python">from pyspark.sql import functions as F

source_path = "s3://example-bucket/raw/customers/"

source_df = (
    spark.read
    .format("parquet")
    .load(source_path)
)

incremental_df = source_df.filter(
    F.col("updated_at") &gt; F.lit(previous_watermark).cast("timestamp")
)
</code></pre>
<p>This demonstrates the basic filtering logic, but it is important to understand one limitation.</p>
<p><strong>Filtering after reading Parquet files does not guarantee that Spark reads only the changed data from S3.</strong></p>
<p>Spark may still need to discover and scan many files before applying the filter. For efficient incremental ingestion, the source layout must support it—for example, by partitioning or organizing data appropriately, reading only newly arrived files, or using a CDC source.</p>
<p>For production systems, validate timestamp types and timezone behavior, too. A watermark comparison is only reliable when the source's change marker has consistent semantics.</p>
<h3>Step 3: Validate the incoming records</h3>
<p>Incremental processing reduces the volume of work, but it does not remove the need for data quality checks.</p>
<p>For example, customer IDs should not be null.</p>
<pre><code class="language-python">valid_df = incremental_df.filter(
    F.col("customer_id").isNotNull()
)

invalid_df = incremental_df.filter(
    F.col("customer_id").isNull()
)

invalid_count = invalid_df.count()

print(f"Invalid records: {invalid_count}")
</code></pre>
<p>In a production pipeline, invalid records should normally be written to a quarantine location with enough information to investigate and replay them later.</p>
<p>Be careful with <code>count()</code>: it can trigger an additional Spark action and cause more computation. If the same DataFrame is used for several actions, consider whether persistence or a single aggregation is appropriate.</p>
<p>Also decide whether invalid records should block the entire batch or be quarantined while valid records continue. That decision should follow the data contract and business requirements.</p>
<h3>Step 4: Write the processed data</h3>
<p>For a simple append-only dataset, you can write the valid records to a curated S3 location:</p>
<pre><code class="language-python">target_path = "s3://example-bucket/curated/customers/"

(
    valid_df.write
    .format("parquet")
    .mode("append")
    .save(target_path)
)
</code></pre>
<p>This is fine as an illustration of writing new records, but it is not sufficient for a general customer table.</p>
<p>What if an existing customer's email address changes?</p>
<p>Appending the updated record may create two versions of the same customer. If the target must contain one current record per customer, use a storage format and write strategy that supports the required upsert behavior, such as an appropriately configured transactional table with <code>MERGE</code> support.</p>
<p>The target design should match the business requirement:</p>
<ul>
<li><p>Append-only events: append new events.</p>
</li>
<li><p>Current-state records: apply inserts and updates using a stable key.</p>
</li>
<li><p>Sources that support deletions: propagate delete events as well.</p>
</li>
</ul>
<p>Never assume that an incremental source filter alone makes the target correct.</p>
<h2>4. The Hard Part: When Should You Update the Watermark?</h2>
<p>This is where a seemingly simple pipeline can lose data.</p>
<p>Imagine the job reads new records, writes some of them to the target, and then fails before it finishes.</p>
<p>If the pipeline advances its watermark too early, the next run may skip records that were never successfully written.</p>
<p>If the pipeline retries the batch, it may write records that were already committed during the previous attempt.</p>
<p>These are two different failure modes: missing data and duplicate data.</p>
<p>A safer design follows this sequence:</p>
<ol>
<li><p>Determine the source range or batch to process.</p>
</li>
<li><p>Read and validate that batch.</p>
</li>
<li><p>Write the batch using an idempotent strategy.</p>
</li>
<li><p>Verify that the write completed according to the pipeline's requirements.</p>
</li>
<li><p>Advance the checkpoint only when the batch is safely committed.</p>
</li>
</ol>
<p>However, the order alone does not guarantee exactly-once processing. A crash can occur after the target write succeeds but before the checkpoint is updated.</p>
<p>For that reason, the target write must also be safe to retry, or the system needs a coordinated commit mechanism.</p>
<p>For example, a deterministic batch ID, a deduplication key, and an idempotent merge can help make retries safe. The exact approach depends on the target storage format and the semantics of its write operations.</p>
<h2>5. What About Records That Arrive Late?</h2>
<p>Consider a record that has a source timestamp of October 14 but does not arrive until October 16.</p>
<p>If your pipeline has already advanced its watermark to October 15, a strict timestamp filter could miss that record.</p>
<p>There are several ways to handle this:</p>
<p><strong>Option 1: Use a lookback window.</strong></p>
<p>Read a small period before the last watermark and deduplicate or merge the results into the target.</p>
<p>This is simple and useful when the maximum lateness is known. It is not a complete solution if records can arrive arbitrarily late.</p>
<p><strong>Option 2: Use a reliable change sequence.</strong></p>
<p>A database log sequence number, CDC offset, or another source-defined change marker can be more reliable than application timestamps, provided the source guarantees the required ordering and retention.</p>
<p><strong>Option 3: Reconcile periodically.</strong></p>
<p>Compare source and target counts, keys, or other business-level checks to identify missing or inconsistent records.</p>
<p>Reconciliation is especially useful when source delivery and target ingestion are separate processes.</p>
<p>The right solution depends on how the source behaves. A pipeline that receives complete daily snapshots has different requirements from one that consumes a continuous database change stream.</p>
<h2>6. What If the Source Record Is Updated?</h2>
<p>Suppose customer <code>C101</code> originally has this email:</p>
<pre><code class="language-text">customer_id: C101
email: old@example.com
</code></pre>
<p>Later, the source changes the email to:</p>
<pre><code class="language-text">customer_id: C101
email: new@example.com
</code></pre>
<p>An incremental pipeline should not blindly append the second record if the target represents the latest state of each customer.</p>
<p>Instead, it should identify the business key and apply the new version.</p>
<p>Conceptually, the operation is:</p>
<pre><code class="language-sql">MERGE INTO target_customers AS target
USING staged_customer_changes AS source
ON target.customer_id = source.customer_id

WHEN MATCHED THEN
  UPDATE SET
    target.email = source.email

WHEN NOT MATCHED THEN
  INSERT (customer_id, email)
  VALUES (source.customer_id, source.email);
</code></pre>
<p>This is illustrative SQL. The exact syntax and supported behavior depend on the table format and query engine.</p>
<p>In practice, you should also define how to handle multiple updates to the same customer within a batch, deletes, schema evolution, and conflicting updates from concurrent jobs.</p>
<h2>7. Monitoring: How Do You Know Incremental Processing Is Working?</h2>
<p>A job marked <code>SUCCEEDED</code> only tells you that the job completed according to its execution status. It does not prove that every expected source record reached the target.</p>
<p>Track metrics that describe the data itself:</p>
<ul>
<li><p>Source records or files discovered.</p>
</li>
<li><p>New, updated, and deleted records processed.</p>
</li>
<li><p>Invalid and quarantined records.</p>
</li>
<li><p>Records written to the target.</p>
</li>
<li><p>Watermark or source offset before and after the run.</p>
</li>
<li><p>Processing duration and estimated resource consumption.</p>
</li>
<li><p>Reconciliation differences between source and target.</p>
</li>
</ul>
<p>AWS Glue job metrics and Amazon CloudWatch can help monitor execution, while application-level metrics and reconciliation checks provide visibility into the data.</p>
<p>For example, if a source delivers 50,000 changed records but the pipeline writes only 47,000, the pipeline should investigate the difference instead of treating a successful job status as proof of correctness.</p>
<p>Do not assume source counts and target counts must always match exactly. Deduplication, filtering, and business rules may legitimately change counts. Define expected reconciliation rules for each pipeline.</p>
<h2>8. A Practical Production Checklist</h2>
<p>Before putting an incremental pipeline into production, check the following:</p>
<ul>
<li><p>[ ] The initial load and subsequent incremental runs are both defined.</p>
</li>
<li><p>[ ] The source provides a reliable way to identify changes.</p>
</li>
<li><p>[ ] Late-arriving records are handled.</p>
</li>
<li><p>[ ] Updates and deletes have explicit processing rules.</p>
</li>
<li><p>[ ] Retrying a batch does not create duplicates or corrupt the target.</p>
</li>
<li><p>[ ] The checkpoint advances only after the required commit conditions are satisfied.</p>
</li>
<li><p>[ ] Failed and invalid records can be investigated and replayed.</p>
</li>
<li><p>[ ] Schema changes are detected and handled deliberately.</p>
</li>
<li><p>[ ] Source-to-target reconciliation is monitored.</p>
</li>
<li><p>[ ] Backfills can run without accidentally advancing the normal production checkpoint.</p>
</li>
<li><p>[ ] Concurrent runs cannot overwrite each other's progress.</p>
</li>
<li><p>[ ] Processing volume, runtime, and cost are measured before and after the change.</p>
</li>
</ul>
<h2>Final Thoughts</h2>
<p>Incremental processing is not just about adding a timestamp filter to a PySpark DataFrame.</p>
<p>It is about deciding what has changed, identifying which changes have been processed successfully, and ensuring that failures do not leave the target in an inconsistent state.</p>
<p>AWS Glue bookmarks, application-level watermarks, CDC, and manifest-based ingestion can all be useful, but none of them removes the need to design the target writes and recovery behavior carefully.</p>
<p>Start with a small, measurable improvement. Identify a pipeline that repeatedly processes the same data, determine how changes can be tracked reliably, and compare the results against a full-refresh baseline.</p>
<p>The goal is not simply to process fewer records. It is to process fewer records <strong>without sacrificing correctness or recoverability</strong>.</p>
<p>That is what makes an incremental pipeline production-ready.</p>
<hr />
<p><em>What strategy do you use for incremental processing in your data pipelines—Glue job bookmarks, timestamps, CDC, or something else? I'd be interested to hear what has worked well in your environment.</em></p>
]]></content:encoded></item><item><title><![CDATA[When Delta Lake Jobs Collide: Troubleshooting Concurrency Issues in AWS Glue]]></title><description><![CDATA[Have you ever run two AWS Glue jobs against the same Delta table and noticed that one job fails while the other completes successfully?
The confusing part is that both jobs might work perfectly when e]]></description><link>https://rahul-dataeng.hashnode.dev/when-delta-lake-jobs-collide-troubleshooting-concurrency-issues-in-aws-glue</link><guid isPermaLink="true">https://rahul-dataeng.hashnode.dev/when-delta-lake-jobs-collide-troubleshooting-concurrency-issues-in-aws-glue</guid><category><![CDATA[data engineer]]></category><category><![CDATA[data pipeline]]></category><category><![CDATA[AWS Glue]]></category><category><![CDATA[AWS]]></category><category><![CDATA[#apache-spark]]></category><category><![CDATA[Delta Lake]]></category><category><![CDATA[concurrency]]></category><category><![CDATA[big data]]></category><dc:creator><![CDATA[Rahul R]]></dc:creator><pubDate>Mon, 12 Oct 2026 09:08:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ab7e1cd7238f0f167fa4d67/1b4e5239-34a8-4645-8c0b-abcea46f1585.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever run two AWS Glue jobs against the same Delta table and noticed that one job fails while the other completes successfully?</p>
<p>The confusing part is that both jobs might work perfectly when executed separately. The issue appears only when they run at the same time.</p>
<p>This is a common scenario to investigate when building data pipelines that use AWS Glue, Apache Spark, and Delta Lake.</p>
<p>In production, multiple jobs may read, insert, update, or delete records from the same table. When their operations overlap, Delta Lake's transaction mechanism must determine whether both changes can be committed safely.</p>
<p>Let's understand how these conflicts happen, how to identify their root causes, and what we can do to handle them.</p>
<h2>What Is a Delta Lake Concurrency Issue?</h2>
<p>Delta Lake uses a transaction log to track changes to a table. Instead of modifying a table without coordination, a write operation prepares its changes and attempts to commit them to the transaction log.</p>
<p>When multiple operations run simultaneously, their changes may conflict.</p>
<p>For example, imagine two AWS Glue jobs processing customer data.</p>
<ul>
<li><p>Job A deletes inactive customer records.</p>
</li>
<li><p>Job B updates customer records with new information.</p>
</li>
<li><p>Both jobs access overlapping data and attempt to commit their changes.</p>
</li>
</ul>
<p>Depending on the operations, isolation settings, and Delta Lake version, one transaction may fail because its changes conflict with another committed transaction.</p>
<p>This is known as a concurrency conflict.</p>
<p><strong>The important thing to understand is that concurrency conflicts are not necessarily bugs in your Spark code.</strong> They can be the result of multiple transactions attempting to change the same table at the same time.</p>
<h2>Why Do Delta Lake Concurrency Issues Occur?</h2>
<p>Let's look at the most common scenarios.</p>
<h3>1. Multiple Glue Jobs Writing to the Same Delta Table</h3>
<p>Consider a data pipeline where two Glue jobs update the same target table.</p>
<p>Job A processes today's customer updates, while Job B processes corrections from a previous batch.</p>
<p>Both jobs write to the same Delta table:</p>
<pre><code class="language-python">from delta.tables import DeltaTable

delta_table = DeltaTable.forPath(
    spark,
    "s3://my-data-lake/delta/customers/"
)

delta_table.update(
    condition="customer_status = 'ACTIVE'",
    set={"last_processed": "'2026-10-12'"}
)
</code></pre>
<p>If another job modifies overlapping data concurrently, the transaction may encounter a conflict.</p>
<p>The exact result depends on the changes made by each transaction and the concurrency behavior supported by the Delta Lake version being used.</p>
<p><strong>How to address it:</strong></p>
<ul>
<li><p>Avoid running conflicting writes simultaneously.</p>
</li>
<li><p>Separate workloads when they can operate on independent data.</p>
</li>
<li><p>Coordinate writers that must modify the same records.</p>
</li>
<li><p>Investigate the exact exception before implementing retries.</p>
</li>
</ul>
<h3>2. Concurrent DELETE and UPDATE Operations</h3>
<p>One particularly important scenario occurs when one job deletes records while another updates them.</p>
<p>Suppose Job A removes inactive customers:</p>
<pre><code class="language-sql">DELETE FROM delta.`s3://my-data-lake/delta/customers/`
WHERE customer_status = 'INACTIVE';
</code></pre>
<p>At the same time, Job B updates customer records:</p>
<pre><code class="language-sql">UPDATE delta.`s3://my-data-lake/delta/customers/`
SET customer_status = 'ACTIVE'
WHERE customer_id = 101;
</code></pre>
<p>If customer 101 is included in the delete operation's affected data, the operations may conflict.</p>
<p>This is especially worth investigating when a pipeline performs cleanup and data correction simultaneously.</p>
<p><strong>How to address it:</strong></p>
<p>Schedule cleanup after dependent updates complete, or redesign the workflow so that the two jobs operate on separate, well-defined sets of records.</p>
<p>Do not assume that every concurrent DELETE and UPDATE must fail. The outcome depends on the actual data overlap and the table's concurrency semantics.</p>
<h3>3. Concurrent MERGE Operations</h3>
<p>MERGE is frequently used in data engineering to implement upserts.</p>
<p>For example:</p>
<pre><code class="language-sql">MERGE INTO delta.`s3://my-data-lake/delta/customers/` AS target
USING incoming_customers AS source
ON target.customer_id = source.customer_id

WHEN MATCHED THEN
    UPDATE SET
        target.customer_name = source.customer_name

WHEN NOT MATCHED THEN
    INSERT (
        customer_id,
        customer_name
    )
    VALUES (
        source.customer_id,
        source.customer_name
    );
</code></pre>
<p>Now imagine two Glue jobs running this MERGE operation with different source batches.</p>
<p>If both batches contain overlapping customer IDs, the jobs may attempt to change the same logical records. They may also affect overlapping data files or partitions.</p>
<p>Depending on the Delta Lake implementation and isolation behavior, one transaction may fail with a concurrency exception.</p>
<p><strong>How to address it:</strong></p>
<ul>
<li><p>Deduplicate source records before running MERGE.</p>
</li>
<li><p>Ensure that the source contains a deterministic record for each business key.</p>
</li>
<li><p>Avoid overlapping source batches where possible.</p>
</li>
<li><p>Serialize operations that must update the same keys.</p>
</li>
<li><p>Retry only after confirming that the failure is transient and the operation is safe to repeat.</p>
</li>
</ul>
<p>Deduplication is particularly important because it also prevents ambiguous source data from causing separate MERGE errors.</p>
<h3>4. Concurrent Jobs Modifying the Same Partitions</h3>
<p>Partitioning can influence how much data a transaction needs to read or modify.</p>
<p>For example, suppose a Delta table is partitioned by processing date:</p>
<pre><code class="language-text">customers/
    processing_date=2026-10-10/
    processing_date=2026-10-11/
    processing_date=2026-10-12/
</code></pre>
<p>Job A processes October 12, while Job B processes October 11.</p>
<p>If the jobs truly modify separate partitions, the risk of conflicts may be lower. However, partition separation does not guarantee conflict-free execution in every situation.</p>
<p>For example, a broad DELETE, an UPDATE that touches multiple partitions, or a MERGE that reads and modifies overlapping data may still conflict with another transaction.</p>
<p><strong>How to address it:</strong></p>
<p>Design jobs to process clearly defined partitions and use partition filters where appropriate. Verify the actual read and write scope instead of assuming that partitioning alone prevents conflicts.</p>
<h2>How to Identify the Root Cause</h2>
<p>When a Glue job fails with a Delta Lake concurrency exception, I would investigate the following areas before changing the code.</p>
<h3>Step 1: Check the Glue Job Logs</h3>
<p>Open the failed job run and inspect its driver logs in Amazon CloudWatch.</p>
<p>Look for the full exception, including the exception class and the transaction operation involved.</p>
<p>Depending on the Delta Lake version, you may encounter errors with names such as:</p>
<ul>
<li><p><code>ConcurrentDeleteReadException</code></p>
</li>
<li><p><code>ConcurrentDeleteDeleteException</code></p>
</li>
<li><p><code>ConcurrentWriteException</code></p>
</li>
<li><p><code>MetadataChangedException</code></p>
</li>
</ul>
<p>These errors indicate different types of conflicts. Confirm the exact exception in your logs rather than assuming all concurrency errors have the same cause.</p>
<h3>Step 2: Compare Job Execution Times</h3>
<p>Check whether another Glue job was modifying the same Delta table when the failure occurred.</p>
<p>Compare:</p>
<ul>
<li><p>Job start and end times.</p>
</li>
<li><p>Target table paths.</p>
</li>
<li><p>Source batch identifiers.</p>
</li>
<li><p>DELETE, UPDATE, and MERGE conditions.</p>
</li>
<li><p>Affected partitions and business keys.</p>
</li>
</ul>
<p>This helps determine whether the issue is related to concurrent access or another failure.</p>
<h3>Step 3: Check the Delta Lake Version</h3>
<p>AWS Glue version and Delta Lake version are important because supported features and transaction behavior can differ.</p>
<p>Confirm the libraries available to your Glue job and the configuration used to enable Delta Lake.</p>
<p>Do not assume that a Delta Lake feature available in a newer release is supported in your current environment.</p>
<h3>Step 4: Review the Transaction Log</h3>
<p>Delta Lake stores transaction information in the table's <code>_delta_log</code> directory.</p>
<p>For example:</p>
<pre><code class="language-text">s3://my-data-lake/delta/customers/
    _delta_log/
    part-00000-....parquet
    part-00001-....parquet
</code></pre>
<p>The transaction log can help you understand the sequence of committed changes and whether another transaction modified the table.</p>
<p>For production troubleshooting, use Delta Lake's supported history and diagnostic capabilities where available, rather than manually changing transaction-log files.</p>
<h2>Practical Solutions for AWS Glue</h2>
<p>Once the root cause is understood, choose a solution that matches the workload.</p>
<h3>Solution 1: Prevent Overlapping Runs</h3>
<p>If the same Glue job must not modify a Delta table concurrently, set its concurrency limit to one:</p>
<pre><code class="language-python">import boto3

glue = boto3.client("glue")

glue.update_job(
    JobName="customer-delta-job",
    ExecutionProperty={
        "MaxConcurrentRuns": 1
    }
)
</code></pre>
<p>This limits concurrent runs of that particular Glue job.</p>
<p>However, it does not prevent a different Glue job or external application from writing to the same Delta table. If several jobs share a target, coordinate their execution centrally.</p>
<h3>Solution 2: Use AWS Step Functions to Coordinate Jobs</h3>
<p>For pipelines with multiple dependent writers, AWS Step Functions can control the order of operations.</p>
<p>A workflow might look like this:</p>
<pre><code class="language-text">Start Pipeline
      |
      v
Process Incoming Data
      |
      v
Validate Source Records
      |
      v
Update Delta Table
      |
      v
Run Cleanup Operation
      |
      v
Validate Final Data
      |
      v
Complete
</code></pre>
<p>This approach is useful when cleanup must happen after an update or when multiple writes must be executed in a specific order.</p>
<p>Step Functions coordinates the workflow, but you must still consider other writers that operate outside that workflow.</p>
<h3>Solution 3: Implement Safe Retries</h3>
<p>Sometimes a transaction conflict is temporary, and retrying the operation after another transaction completes may succeed.</p>
<p>However, retries should not be added blindly.</p>
<p>A safer approach is to:</p>
<ol>
<li><p>Identify the specific concurrency exception.</p>
</li>
<li><p>Confirm that retrying is appropriate for that exception.</p>
</li>
<li><p>Wait briefly before retrying.</p>
</li>
<li><p>Limit the number of attempts.</p>
</li>
<li><p>Record the failed attempt and the reason.</p>
</li>
<li><p>Validate the final table state.</p>
</li>
</ol>
<p>For example, a simple retry structure might look like this:</p>
<pre><code class="language-python">import time

max_attempts = 3

for attempt in range(max_attempts):
    try:
        # Execute your Delta Lake write operation here.
        # Replace this comment with the actual operation.
        perform_delta_operation()
        break

    except Exception:
        if attempt == max_attempts - 1:
            raise

        time.sleep(2 ** attempt)
</code></pre>
<p>This is only a generic retry structure. In production, catch the specific retryable exception instead of catching every exception indiscriminately. Do not assume that a failed write is always safe to repeat.</p>
<h3>Solution 4: Make Each Batch Deterministic</h3>
<p>A reliable pipeline should know exactly which source records belong to each batch.</p>
<p>For example, if two jobs process the same customer ID from different source batches, they may attempt to update the same target record.</p>
<p>Use stable batch identifiers, deterministic deduplication rules, and clear ownership of the records each job is allowed to modify.</p>
<p>This reduces accidental overlap and makes failed batches easier to recover.</p>
<h2>What I Would Avoid</h2>
<p>A few shortcuts can make concurrency problems harder to diagnose:</p>
<ul>
<li><p><strong>Do not retry every error.</strong> Some failures indicate persistent conflicts or incorrect logic.</p>
</li>
<li><p><strong>Do not disable concurrency controls without understanding the impact.</strong> Multiple writers may still conflict.</p>
</li>
<li><p><strong>Do not assume partitioning eliminates conflicts.</strong> Operations can read or modify data beyond the partition you expected.</p>
</li>
<li><p><strong>Do not manually delete files from a Delta table's storage directory.</strong> Delta Lake manages data files through its transaction log.</p>
</li>
<li><p><strong>Do not treat every concurrency exception as a data corruption issue.</strong> A failed transaction does not automatically mean that the table is corrupted.</p>
</li>
</ul>
<h2>Final Thoughts</h2>
<p>Delta Lake concurrency issues can become challenging when multiple AWS Glue jobs update, delete, or merge records into the same table.</p>
<p>The solution is not always to reduce the number of jobs or increase Spark resources. In many cases, the real issue is how concurrent transactions interact with the same data.</p>
<p>Start by identifying the exact exception, comparing job execution times, and checking the affected records and partitions. Then decide whether you need execution coordination, safer batch boundaries, or a carefully controlled retry strategy.</p>
<p><strong>A reliable data pipeline needs more than successful Spark execution. It also needs a clear strategy for managing concurrent changes to shared data.</strong></p>
<p>Have you faced a <code>ConcurrentDeleteReadException</code> or another Delta Lake concurrency error in AWS Glue? Share the error and what caused it in your pipeline. Your experience may help other data engineers solve the same problem.</p>
]]></content:encoded></item><item><title><![CDATA[AWS Step Functions vs Apache Airflow: Similar Purpose, Different Use Cases
]]></title><description><![CDATA[When I started exploring data engineering workflows, one question that came to mind was: if AWS Step Functions and Apache Airflow can both orchestrate workflows, why do we need both?
At first glance, ]]></description><link>https://rahul-dataeng.hashnode.dev/aws-step-functions-vs-apache-airflow-similar-purpose-different-use-cases</link><guid isPermaLink="true">https://rahul-dataeng.hashnode.dev/aws-step-functions-vs-apache-airflow-similar-purpose-different-use-cases</guid><category><![CDATA[AWS Step Functions]]></category><category><![CDATA[AWS]]></category><category><![CDATA[data engineer]]></category><category><![CDATA[big data]]></category><category><![CDATA[apache-airflow]]></category><category><![CDATA[data pipeline]]></category><dc:creator><![CDATA[Rahul R]]></dc:creator><pubDate>Sun, 11 Oct 2026 15:18:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ab7e1cd7238f0f167fa4d67/6b5f6a9b-0220-4237-a9ff-d40d3bb37b4b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I started exploring data engineering workflows, one question that came to mind was: if AWS Step Functions and Apache Airflow can both orchestrate workflows, why do we need both?</p>
<p>At first glance, they seem similar. Both help connect multiple tasks, manage execution order, handle failures, and coordinate complex processes.</p>
<p>But when we look at how they work and the problems they are designed to solve, the differences become clearer.</p>
<p>In this article, let's compare AWS Step Functions and Apache Airflow, understand their strengths, and explore when to choose one over the other.</p>
<h2>What Is AWS Step Functions?</h2>
<p>AWS Step Functions is a managed workflow orchestration service that coordinates AWS services and application tasks using state machines.</p>
<p>You define the workflow as a series of states. Each state can perform a task, make a decision, wait, run branches in parallel, or handle errors.</p>
<p>For example, consider a data pipeline that processes incoming files:</p>
<ol>
<li><p>A file arrives in Amazon S3.</p>
</li>
<li><p>AWS Lambda validates the file.</p>
</li>
<li><p>AWS Glue transforms the data.</p>
</li>
<li><p>The workflow checks whether processing succeeded.</p>
</li>
<li><p>A notification is sent through Amazon SNS.</p>
</li>
</ol>
<p>Step Functions can coordinate these operations, manage execution flow, and define retry and error-handling behavior.</p>
<p>One of its biggest advantages is that it integrates naturally with AWS services without requiring you to operate your own orchestration server.</p>
<h2>What Is Apache Airflow?</h2>
<p>Apache Airflow is an open-source platform for authoring, scheduling, and monitoring workflows programmatically.</p>
<p>Workflows are represented as Directed Acyclic Graphs (DAGs), typically written in Python. Each task represents an operation, and dependencies define the order in which tasks execute.</p>
<p>For example, a daily analytics pipeline might:</p>
<ol>
<li><p>Extract data from multiple sources.</p>
</li>
<li><p>Run data-quality checks.</p>
</li>
<li><p>Transform datasets using Spark or SQL.</p>
</li>
<li><p>Load the results into a data warehouse.</p>
</li>
<li><p>Run validation queries.</p>
</li>
<li><p>Publish the final dataset for reporting.</p>
</li>
</ol>
<p>Airflow is especially useful when workflows contain many dependent data-processing tasks and need scheduling, observability, backfills, and centralized pipeline management.</p>
<p>You can run Airflow yourself or use a managed offering such as Amazon Managed Workflows for Apache Airflow (Amazon MWAA).</p>
<h2>Step Functions vs Airflow: What Is the Difference?</h2>
<p>Although both tools orchestrate workflows, their design priorities are different.</p>
<table>
<thead>
<tr>
<th>Feature</th>
<th>AWS Step Functions</th>
<th>Apache Airflow</th>
</tr>
</thead>
<tbody><tr>
<td>Main purpose</td>
<td>Coordinate application and service workflows</td>
<td>Author, schedule, and monitor data workflows</td>
</tr>
<tr>
<td>Workflow definition</td>
<td>State machines, commonly defined using Amazon States Language (JSON)</td>
<td>Python-based DAGs</td>
</tr>
<tr>
<td>Scheduling</td>
<td>EventBridge Scheduler or other triggers can start workflows</td>
<td>Built-in DAG scheduling</td>
</tr>
<tr>
<td>AWS integration</td>
<td>Native integrations with many AWS services</td>
<td>AWS provider operators, hooks, and APIs</td>
</tr>
<tr>
<td>Error handling</td>
<td>State-level retries, catches, and workflow error paths</td>
<td>Task retries, callbacks, and failure-handling patterns</td>
</tr>
<tr>
<td>Backfills</td>
<td>Usually implemented through application-specific execution logic</td>
<td>Strong support for rerunning historical scheduled DAG runs</td>
</tr>
<tr>
<td>Infrastructure</td>
<td>Fully managed orchestration service</td>
<td>Self-managed or managed through Amazon MWAA</td>
</tr>
<tr>
<td>Best fit</td>
<td>Event-driven automation and service orchestration</td>
<td>Scheduled data pipelines and complex data dependencies</td>
</tr>
</tbody></table>
<p>Neither tool is universally better. The right choice depends on the workflow, operational requirements, and how much control you need over orchestration.</p>
<h2>Use Case 1: When AWS Step Functions Makes More Sense</h2>
<p>Imagine an event-driven pipeline in AWS.</p>
<p>Whenever a customer uploads a file to S3, the system needs to validate it, process it, and notify the relevant team.</p>
<p>A possible workflow is:</p>
<pre><code class="language-text">S3 Upload
    |
    v
EventBridge / Lambda
    |
    v
Step Functions
    |
    v
Validate File
    |
    v
Run AWS Glue Job
    |
    v
Check Result
   / \
Success Failure
   |      |
   v      v
Publish  Retry / Handle Error
   |
   v
Send Notification
</code></pre>
<p>Step Functions is a good fit here because the workflow is triggered by an event and coordinates several AWS services.</p>
<p>You can define retries for transient failures, branch based on job results, and track individual workflow executions.</p>
<p>This is particularly useful for event-driven data ingestion, file processing, application workflows, and automated recovery processes.</p>
<h2>Use Case 2: When Apache Airflow Makes More Sense</h2>
<p>Now consider a data engineering platform that processes data from several source systems every day.</p>
<p>The pipeline has dependencies between ingestion, transformation, data-quality checks, and reporting. Sometimes historical data must be reprocessed after a transformation changes.</p>
<p>An Airflow DAG could look like this:</p>
<pre><code class="language-text">Extract Customer Data     Extract Sales Data
          |                       |
          v                       v
       Validate                 Validate
          |                       |
          +-----------+-----------+
                      |
                      v
              Transform with Spark
                      |
                      v
                Data Quality
                      |
                      v
               Load Warehouse
                      |
                      v
              Publish Analytics
</code></pre>
<p>Airflow is a strong fit because you can express dependencies in Python, schedule recurring workflows, monitor task status, and manage historical reruns.</p>
<p>For data engineering teams managing dozens or hundreds of related DAGs, these capabilities can be especially valuable.</p>
<p>Airflow can also orchestrate AWS services, including Glue jobs, so using it does not mean abandoning AWS-native processing services.</p>
<h2>Can We Use Both in the Same Architecture?</h2>
<p>Yes. In some systems, using both is a sensible design.</p>
<p>For example, an enterprise data platform might use Airflow to schedule daily pipelines across multiple systems, while Step Functions manages a particular event-driven process that needs detailed branching and recovery.</p>
<pre><code class="language-text">Apache Airflow
      |
      v
Daily Data Pipeline
      |
      v
Trigger Step Functions
      |
      v
Validate and Process Files
      |
      v
Run AWS Glue
      |
      v
Return Workflow Status
      |
      v
Continue Airflow DAG
</code></pre>
<p>This is a conceptual example, not a mandatory architecture. The integration requires a suitable Airflow operator or API call, and the team must define how completion, failures, retries, and duplicate executions are handled.</p>
<p>However, there is an important consideration: <strong>don't introduce both tools just because you can.</strong></p>
<p>If one service can orchestrate the entire workflow cleanly, adding another orchestrator may increase operational complexity, monitoring effort, and failure-handling responsibilities.</p>
<h2>What About AWS Glue Workflows?</h2>
<p>Since AWS Glue is commonly used for data engineering, it is also worth mentioning Glue Workflows.</p>
<p>AWS Glue Workflows can coordinate Glue jobs, crawlers, and triggers for ETL use cases. They may be sufficient when the workflow is primarily contained within Glue.</p>
<p>Step Functions becomes attractive when you need broader service orchestration, richer branching, and integration with services outside Glue.</p>
<p>Airflow is worth considering when you need a centralized platform for scheduling and managing pipelines across multiple systems.</p>
<h2>How Do I Choose the Right Tool?</h2>
<p>I would start by asking three questions.</p>
<p><strong>1. What triggers the workflow?</strong></p>
<p>If the process starts in response to an event and coordinates AWS services, Step Functions may be a natural fit. For recurring pipelines with schedules and data dependencies, Airflow is often a strong candidate.</p>
<p><strong>2. How complex are the dependencies?</strong></p>
<p>For application-style workflows with branching, waiting, and service-level error handling, Step Functions is well suited. For Python-defined DAGs, historical backfills, and a large collection of data pipelines, Airflow offers useful capabilities.</p>
<p><strong>3. Who will operate the system?</strong></p>
<p>Step Functions reduces the need to manage orchestration infrastructure. With Airflow, the team needs to account for the Airflow environment, workers, dependencies, upgrades, and operational monitoring, even when using a managed service.</p>
<p>Cost, execution duration, concurrency, service quotas, and workflow complexity should also be evaluated before making a production decision.</p>
<h2>Final Thoughts</h2>
<p>AWS Step Functions and Apache Airflow may appear to do the same thing because both coordinate tasks and manage workflow dependencies. But their strengths come from different design priorities.</p>
<p>Step Functions is particularly useful for managed, event-driven orchestration across AWS services. Airflow is particularly useful for scheduling and managing complex data pipelines through Python-defined DAGs.</p>
<p>For data engineers, the goal should not be to choose the most popular tool. It should be to choose the one that fits the problem without introducing unnecessary complexity.</p>
<p><strong>My takeaway:</strong> Understand the workflow first, then choose the orchestration tool.</p>
<p>I'd love to hear from other data engineers: <strong>Do you prefer AWS Step Functions or Apache Airflow for your data pipelines, and what made you choose it?</strong></p>
]]></content:encoded></item><item><title><![CDATA[Stop Hardcoding API Keys: A Practical Guide to Fetching Secrets from HashiCorp Vault]]></title><description><![CDATA[Have you ever stored a database password or API key directly in your application code just to get something working?
I think many developers have done something similar while learning or building a qu]]></description><link>https://rahul-dataeng.hashnode.dev/stop-hardcoding-api-keys-a-practical-guide-to-fetching-secrets-from-hashicorp-vault</link><guid isPermaLink="true">https://rahul-dataeng.hashnode.dev/stop-hardcoding-api-keys-a-practical-guide-to-fetching-secrets-from-hashicorp-vault</guid><category><![CDATA[backend]]></category><category><![CDATA[Vault]]></category><category><![CDATA[hashicorp-vault]]></category><category><![CDATA[secrets management]]></category><category><![CDATA[DevSecOps]]></category><category><![CDATA[Python]]></category><category><![CDATA[cloud security]]></category><dc:creator><![CDATA[Rahul R]]></dc:creator><pubDate>Fri, 09 Oct 2026 18:06:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ab7e1cd7238f0f167fa4d67/0172e6bf-cbff-4bf3-87ef-e6e26de7bcda.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever stored a database password or API key directly in your application code just to get something working?</p>
<p>I think many developers have done something similar while learning or building a quick prototype.</p>
<p>For example:</p>
<pre><code class="language-python">DATABASE_PASSWORD = "my_database_password"
PAYMENT_API_KEY = "my_payment_api_key"
</code></pre>
<p>It works locally. Your application connects to the database, calls the external API, and everything looks fine.</p>
<p>But what happens when you push that code to GitHub or deploy it to a production environment?</p>
<p>If those credentials end up in the wrong place, someone could use them to access your database or external services.</p>
<p>Moving secrets into a <code>.env</code> file is a useful step for local development, but production applications need a reliable way to manage credentials, control access, and handle rotation.</p>
<p>That is where <strong>HashiCorp Vault</strong> comes in.</p>
<p>In this article, we'll build a small example to understand how a Python backend can retrieve database credentials and API keys from Vault instead of keeping them in the source code.</p>
<h2>What Is HashiCorp Vault?</h2>
<p>HashiCorp Vault is a secrets management platform that helps applications securely manage sensitive information.</p>
<p>Some common examples include:</p>
<ul>
<li><p>Database usernames and passwords</p>
</li>
<li><p>Third-party API keys</p>
</li>
<li><p>Application credentials</p>
</li>
<li><p>Certificates and encryption keys</p>
</li>
</ul>
<p>Vault provides authentication mechanisms and policies that determine which identities can access particular secrets. It also supports dynamic secrets for supported systems, allowing credentials to be generated for a limited period instead of relying entirely on permanent passwords.</p>
<p>For this tutorial, we'll focus on a simpler use case: storing secrets in Vault and retrieving them from a Python application using the <strong>Key-Value (KV) secrets engine</strong>.</p>
<h2>Understanding the Architecture</h2>
<p>Before writing the code, let's look at how the process works.</p>
<ol>
<li><p>An administrator stores application secrets in Vault.</p>
</li>
<li><p>The Python backend authenticates with Vault.</p>
</li>
<li><p>Vault verifies the application's identity and permissions.</p>
</li>
<li><p>The application requests a particular secret path.</p>
</li>
<li><p>Vault returns the permitted secret values.</p>
</li>
<li><p>The application uses those values to connect to a database or call an external API.</p>
</li>
</ol>
<p>The important idea is that the application source code does not need to contain the actual database password or API key.</p>
<h2>Step 1: Start Vault and Store the Secrets</h2>
<p>We'll use a local development server to keep this example simple.</p>
<p>Start Vault:</p>
<pre><code class="language-bash">vault server -dev
</code></pre>
<p>The development server prints a token that can be used for this local exercise. Keep it private and do not use it in production.</p>
<p>In another terminal, configure the connection details.</p>
<p><strong>Linux or macOS:</strong></p>
<pre><code class="language-bash">export VAULT_ADDR="http://127.0.0.1:8200"
export VAULT_TOKEN="&lt;your-development-token&gt;"
</code></pre>
<p><strong>Windows PowerShell:</strong></p>
<pre><code class="language-powershell">$env:VAULT_ADDR = "http://127.0.0.1:8200"
$env:VAULT_TOKEN = "&lt;your-development-token&gt;"
</code></pre>
<p>The standard development setup enables a KV v2 secrets engine at the <code>secret/</code> mount.</p>
<p>Let's store some sample credentials:</p>
<pre><code class="language-bash">vault kv put secret/my-backend-app \
  database_username="app_user" \
  database_password="example-password" \
  external_api_key="example-api-key"
</code></pre>
<p>These values are for demonstration only.</p>
<p>We've now stored three values under the secret path:</p>
<p><code>secret/my-backend-app</code></p>
<p>The application will use this path to retrieve the values when it needs them.</p>
<h2>Step 2: Create a Read-Only Policy</h2>
<p>Storing secrets securely is important, but controlling access is equally important.</p>
<p>We don't want every application or developer to have permission to read, modify, or delete every secret in Vault.</p>
<p>Create a file named <code>backend-read.hcl</code>:</p>
<pre><code class="language-hcl">path "secret/data/my-backend-app" {
  capabilities = ["read"]
}
</code></pre>
<p>Why does the policy use <code>secret/data/</code> instead of just <code>secret/</code>?</p>
<p>Because KV v2 uses a data API path for reading secret values. The CLI hides some of this detail, but the policy must use the correct API path.</p>
<p>Load the policy:</p>
<pre><code class="language-bash">vault policy write backend-read backend-read.hcl
</code></pre>
<p>This policy grants read access to the specified secret. It does not grant write or delete permissions.</p>
<p>For this tutorial, we're using the development token to demonstrate the API call. The policy is an example of how access should be restricted; the development root token itself is not restricted by this policy.</p>
<p>In production, authenticate the application using a suitable method, such as AppRole or a platform-native identity mechanism, and associate the appropriate policy with that identity.</p>
<p><strong>Never use the Vault root token as a permanent application credential.</strong></p>
<h2>Step 3: Connect Python to Vault</h2>
<p>Now let's retrieve the secrets from Python.</p>
<p>First, install the Vault client library:</p>
<pre><code class="language-bash">pip install hvac
</code></pre>
<p>The <code>hvac</code> library allows Python applications to interact with Vault's API.</p>
<p>Create a file named <code>app.py</code>:</p>
<pre><code class="language-python">import os
import hvac

vault_url = os.environ["VAULT_ADDR"]
vault_token = os.environ["VAULT_TOKEN"]

client = hvac.Client(
    url=vault_url,
    token=vault_token
)

if not client.is_authenticated():
    raise RuntimeError("Vault authentication failed")

response = client.secrets.kv.v2.read_secret_version(
    path="my-backend-app",
    mount_point="secret"
)

secrets = response["data"]["data"]

database_username = secrets["database_username"]
database_password = secrets["database_password"]
external_api_key = secrets["external_api_key"]

print("Secrets retrieved successfully")
</code></pre>
<p>Let's break down what's happening.</p>
<h3>1. Create the Vault client</h3>
<p>The application reads the Vault URL and token from environment variables rather than embedding them in the Python source code.</p>
<h3>2. Check authentication</h3>
<p>The application verifies whether the supplied credentials are accepted by Vault.</p>
<h3>3. Request the secret</h3>
<p>The <code>read_secret_version()</code> method retrieves the secret stored at <code>secret/my-backend-app</code>.</p>
<h3>4. Extract the values</h3>
<p>The returned response contains the secret data. We extract the database username, database password, and API key so the application can use them.</p>
<p>Notice that we print only a success message. Logging the full response could accidentally expose credentials.</p>
<p>For a remote production Vault server, use HTTPS with proper TLS certificate verification. Also, replace the development token with an appropriate application authentication method.</p>
<h2>Step 4: Use the Retrieved Credentials</h2>
<p>Retrieving a secret is only useful if the application can use it.</p>
<p>For example, a Python application using PostgreSQL could create a connection like this:</p>
<pre><code class="language-python">import os
import psycopg

connection = psycopg.connect(
    host=os.environ["DB_HOST"],
    dbname=os.environ["DB_NAME"],
    user=database_username,
    password=database_password
)
</code></pre>
<p>In this example, the database host and database name come from environment variables, while the sensitive database credentials come from Vault.</p>
<p>The same approach applies to external API calls. The application can pass <code>external_api_key</code> to the appropriate HTTP client when communicating with the service that issued the key.</p>
<p>One thing to remember: retrieving secrets securely doesn't automatically make every part of the application secure. You still need to protect logs, memory access, network connections, and the application's own authentication credentials.</p>
<h2>Step 5: What Happens When an API Key Changes?</h2>
<p>Let's say your external service issues a replacement API key.</p>
<p>Without centralized secrets management, you might have to find where the key is configured, update deployment settings, and redeploy the application.</p>
<p>With Vault, an authorized operator can update the secret:</p>
<pre><code class="language-bash">vault kv put secret/my-backend-app \
  database_username="app_user" \
  database_password="example-password" \
  external_api_key="replacement-example-key"
</code></pre>
<p>KV v2 maintains secret versions, so this update creates a new version under the default configuration.</p>
<p>However, there's an important detail: <strong>updating a secret in Vault does not automatically replace a value that your application has already loaded into memory.</strong></p>
<p>Your application needs a strategy for refreshing its credentials, such as re-reading the secret when appropriate or restarting the relevant service.</p>
<p>For systems that support dynamic credentials, Vault can also help manage short-lived credentials and their lifecycle.</p>
<h2>Best Practices When Using Vault</h2>
<p>A few practices are worth keeping in mind when moving from a local experiment to a real application.</p>
<ul>
<li><p><strong>Follow least privilege:</strong> Give each application access only to the secrets it requires.</p>
</li>
<li><p><strong>Avoid permanent root tokens:</strong> Use an appropriate authentication method for the application.</p>
</li>
<li><p><strong>Use TLS:</strong> Protect connections to remote Vault servers and verify certificates.</p>
</li>
<li><p><strong>Keep secrets out of logs:</strong> Never print complete secret responses or credentials.</p>
</li>
<li><p><strong>Plan for rotation:</strong> Make sure the application can handle credential changes.</p>
</li>
<li><p><strong>Protect the Vault deployment:</strong> Secure its storage, authentication configuration, policies, and administrative access.</p>
</li>
</ul>
<p>Also, remember that the Vault token is itself a sensitive credential. Moving an API key into Vault but exposing the token that can retrieve it would defeat much of the benefit.</p>
<h2>Final Thoughts</h2>
<p>The goal isn't simply to move passwords from one file to another. It's to separate sensitive credentials from application code and control how applications access them.</p>
<p>HashiCorp Vault provides a way to do that through centralized secret storage, authentication, and access policies.</p>
<p>We've walked through a small Python example: storing secrets in Vault, creating a read-only policy, retrieving the values, and using them in a backend application.</p>
<p>This is a starting point rather than a complete production deployment. The next steps are to explore application authentication, credential rotation, and dynamic secrets in more detail.</p>
<p>I'm learning more about HashiCorp Vault and how it fits into backend and cloud engineering workflows.</p>
<p><strong>How do you currently manage application secrets in your projects?</strong></p>
<p>Do you use environment variables, a cloud secrets manager, HashiCorp Vault, or a combination of these approaches?</p>
<p>I'd be interested to hear about your experience.</p>
]]></content:encoded></item><item><title><![CDATA[When AWS Glue Couldn’t Read My S3 Files: Solving the Small Files Problem]]></title><description><![CDATA[If you are working with AWS Glue and Amazon S3, you may eventually come across a problem that doesn't look serious at first:

There are too many small files in S3.

A file that is only a few KB doesn']]></description><link>https://rahul-dataeng.hashnode.dev/when-aws-glue-couldn-t-read-my-s3-files-solving-the-small-files-problem</link><guid isPermaLink="true">https://rahul-dataeng.hashnode.dev/when-aws-glue-couldn-t-read-my-s3-files-solving-the-small-files-problem</guid><dc:creator><![CDATA[Rahul R]]></dc:creator><pubDate>Sat, 26 Sep 2026 16:22:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ab7e1cd7238f0f167fa4d67/aa3a5643-301c-4ad8-914f-d61374439b03.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you are working with AWS Glue and Amazon S3, you may eventually come across a problem that doesn't look serious at first:</p>
<blockquote>
<p><strong>There are too many small files in S3.</strong></p>
</blockquote>
<p>A file that is only a few KB doesn't seem like a problem.</p>
<p>But what happens when you have thousands or even millions of those files?</p>
<p>That's when things can get interesting.</p>
<p>In one of my data pipelines, I faced an issue where an AWS Glue job was trying to read data from S3 but started failing with errors related to <strong>too many requests, throttling, and file read failures</strong>.</p>
<p>The root cause was not that the data was corrupted.</p>
<p>The real problem was the <strong>number of small files</strong>.</p>
<p>In this article, I'll explain what happened, why AWS Glue struggled to read the data, what the S3 requests had to do with it, and how we solved the problem.</p>
<hr />
<h2>The Architecture</h2>
<p>The basic flow looked something like this:</p>
<p><strong>Source → S3 → AWS Glue → Processing → Output</strong></p>
<p>The data was being written into S3, and AWS Glue was responsible for reading those files and processing them.</p>
<p>Initially, everything looked normal.</p>
<p>Then we started seeing failures when the number of files increased.</p>
<hr />
<h2>The Problem: Too Many Small Files</h2>
<p>The biggest issue was that the data was split into a very large number of small files.</p>
<p>For example, instead of having:</p>
<pre><code class="language-text">file-001.parquet   128 MB
file-002.parquet   128 MB
file-003.parquet   128 MB
</code></pre>
<p>we ended up with something closer to:</p>
<pre><code class="language-text">file-001.parquet   1 KB
file-002.parquet   2 KB
file-003.parquet   1 KB
file-004.parquet   3 KB
file-005.parquet   1 KB
...
...
thousands of files
</code></pre>
<p>Individually, these files were tiny.</p>
<p>Collectively, they became a problem.</p>
<h3>Why?</h3>
<p>Because the number of files matters, not just the total amount of data.</p>
<p>Suppose we have:</p>
<pre><code class="language-text">1 GB of data
</code></pre>
<p>There is a big difference between storing it as:</p>
<pre><code class="language-text">10 × 100 MB files
</code></pre>
<p>and:</p>
<pre><code class="language-text">1,000,000 × 1 KB files
</code></pre>
<p>The amount of data is approximately the same.</p>
<p>But the number of objects that have to be discovered, opened, requested, and processed is completely different.</p>
<hr />
<h2>What Was Happening Inside AWS Glue?</h2>
<p>When AWS Glue reads data from S3, it doesn't simply make one request and magically receive the entire dataset.</p>
<p>There are multiple interactions involved in discovering and reading objects.</p>
<p>With a large number of files, the amount of S3 activity can increase significantly.</p>
<p>Conceptually, the process looks like this:</p>
<pre><code class="language-text">S3
 │
 ├── small-file-001
 ├── small-file-002
 ├── small-file-003
 ├── small-file-004
 ├── small-file-005
 ├── ...
 └── small-file-N
          │
          ▼
      AWS Glue
          │
          ▼
   Many read operations
          │
          ▼
   High request rate
          │
          ▼
      Throttling
</code></pre>
<p>This is where the problem started becoming visible in the Glue logs.</p>
<hr />
<h2>The Error in the Glue Logs</h2>
<p>One of the important clues was the S3-related error.</p>
<p>The exact message can vary depending on the operation and implementation, but errors can include things such as:</p>
<pre><code class="language-text">AmazonS3Exception:
Slow Down
Status Code: 503
</code></pre>
<p>or errors indicating that a large number of requests were being made while Glue was attempting to read the objects.</p>
<p>A simplified example looks like:</p>
<pre><code class="language-text">com.amazonaws.services.s3.model.AmazonS3Exception:
Slow Down

Status Code: 503
Error Code: SlowDown
</code></pre>
<p>You may also see failures around reading particular files or partitions.</p>
<p>At first glance, it is tempting to think:</p>
<blockquote>
<p>"Maybe one of the S3 files is corrupted."</p>
</blockquote>
<p>But that wasn't the actual issue in our case.</p>
<p>The important clue was that the problem appeared when Glue was dealing with a very large number of small objects.</p>
<hr />
<h1>Understanding the Small Files Problem</h1>
<p>This is a common problem in distributed data processing.</p>
<p>Imagine a restaurant with 100 customers.</p>
<p>If the customers arrive as 10 groups of 10 people, the restaurant can manage them reasonably well.</p>
<p>Now imagine the same 100 customers arriving individually, one after another.</p>
<p>The restaurant has to perform many more individual interactions.</p>
<p>The same basic idea applies here.</p>
<p>Instead of Glue dealing with:</p>
<pre><code class="language-text">100 large files
</code></pre>
<p>it may have to deal with:</p>
<pre><code class="language-text">100,000 small files
</code></pre>
<p>Even if the total data volume isn't huge.</p>
<hr />
<h1>Why Small Files Can Hurt Performance</h1>
<p>There are several reasons.</p>
<h2>1. More S3 requests</h2>
<p>More objects generally means more object-related operations.</p>
<p>Instead of reading a few large objects, Glue needs to work with a much larger number of objects.</p>
<p>That increases request activity against S3.</p>
<hr />
<h2>2. More Spark tasks</h2>
<p>AWS Glue uses Apache Spark under the hood for many workloads.</p>
<p>When Spark sees many input files, those files can translate into many input partitions/tasks.</p>
<p>For example:</p>
<pre><code class="language-text">S3
 │
 ├── file-1
 ├── file-2
 ├── file-3
 ├── ...
 └── file-10000
        │
        ▼
   Spark / Glue
        │
        ├── Task 1
        ├── Task 2
        ├── Task 3
        ├── ...
        └── Task N
</code></pre>
<p>Now Spark has additional scheduling and task-management overhead.</p>
<hr />
<h2>3. More metadata overhead</h2>
<p>Even if each file contains very little actual data, the system still needs to discover and manage those objects.</p>
<p>So you can end up spending significant effort handling files rather than processing the actual data.</p>
<p>This is one reason why:</p>
<blockquote>
<p><strong>Small files can become a performance problem even when the total data size is small.</strong></p>
</blockquote>
<hr />
<h1>The Root Cause in Our Case</h1>
<p>After looking at the data flow, the problem became much clearer.</p>
<p>The pipeline was producing a large number of very small files.</p>
<p>The Glue job then tried to read those files.</p>
<p>That resulted in a high number of S3 operations.</p>
<p>Eventually, the request rate became a problem and we started seeing throttling/read failures.</p>
<p>So the chain looked like this:</p>
<pre><code class="language-text">Too many small files
        ↓
Large number of objects to read
        ↓
Many S3 requests
        ↓
High request rate
        ↓
S3 throttling / Slow Down responses
        ↓
Glue read failures
        ↓
Glue job failure
</code></pre>
<p>That was the real issue.</p>
<hr />
<h1>The Solution</h1>
<p>The most important lesson from this issue was:</p>
<blockquote>
<p><strong>Don't just try to make Glue handle more small files. Try to reduce the number of small files.</strong></p>
</blockquote>
<p>Instead of continuously increasing retries or resources, we focused on the actual problem.</p>
<p>We needed to <strong>combine the small files into larger files</strong>.</p>
<p>This process is commonly called:</p>
<ul>
<li><p>File compaction</p>
</li>
<li><p>Small-file compaction</p>
</li>
<li><p>File consolidation</p>
</li>
</ul>
<p>The idea is simple:</p>
<pre><code class="language-text">Before

1 KB   2 KB   1 KB   3 KB   2 KB   1 KB
 │      │      │      │      │      │
 └──────┴──────┴──────┴──────┴──────┘
                  ↓
          Many small objects


After

        ┌───────────────────┐
        │    Larger file    │
        │      64 MB        │
        └───────────────────┘

        ┌───────────────────┐
        │    Larger file    │
        │      128 MB       │
        └───────────────────┘
</code></pre>
<p>Now Glue has significantly fewer files to deal with.</p>
<hr />
<h1>Using Spark to Compact the Files</h1>
<p>If the data is being processed with Spark, one approach is to read the small files and write them back using a controlled number of partitions.</p>
<p>For example:</p>
<pre><code class="language-python">df = spark.read.parquet("s3://my-bucket/input/")

df = df.repartition(10)

df.write \
    .mode("overwrite") \
    .parquet("s3://my-bucket/compacted/")
</code></pre>
<p>The exact number of partitions should depend on the size of your dataset and workload.</p>
<p>Don't blindly use:</p>
<pre><code class="language-python">repartition(1)
</code></pre>
<p>for everything.</p>
<p>That forces all the data through a single partition and can create a bottleneck.</p>
<hr />
<h1><code>coalesce()</code> vs <code>repartition()</code></h1>
<p>This is another important detail.</p>
<p>You may see code like:</p>
<pre><code class="language-python">df = df.coalesce(10)
</code></pre>
<p>or:</p>
<pre><code class="language-python">df = df.repartition(10)
</code></pre>
<p>They are not exactly the same.</p>
<h3><code>coalesce()</code></h3>
<p><code>coalesce()</code> is generally useful when you want to <strong>reduce the number of partitions without a full shuffle</strong>.</p>
<p>For example:</p>
<pre><code class="language-python">df = df.coalesce(10)
</code></pre>
<p>This can be useful when reducing the number of output files.</p>
<h3><code>repartition()</code></h3>
<p><code>repartition()</code> performs a shuffle and can redistribute the data across partitions.</p>
<pre><code class="language-python">df = df.repartition(10)
</code></pre>
<p>This can provide a more even distribution, but the shuffle itself has a cost.</p>
<p>So the choice depends on the workload.</p>
<hr />
<h1>An Important Point: <code>coalesce()</code> Is Not the Whole Solution</h1>
<p>One thing I learned from this problem is that there is a difference between:</p>
<blockquote>
<p><strong>Reducing Spark partitions</strong></p>
</blockquote>
<p>and</p>
<blockquote>
<p><strong>Designing a proper file-compaction strategy.</strong></p>
</blockquote>
<p>If the source system is continuously producing thousands of tiny files, simply changing the number of Spark partitions in one downstream job may not completely solve the underlying problem.</p>
<p>You should also ask:</p>
<blockquote>
<p><strong>Why are these small files being created in the first place?</strong></p>
</blockquote>
<p>For example:</p>
<pre><code class="language-text">Source
  ↓
Many tiny writes
  ↓
S3
  ↓
Thousands of small files
  ↓
Glue
</code></pre>
<p>A better architecture tries to prevent or compact those files earlier:</p>
<pre><code class="language-text">Source
  ↓
Controlled writes
  ↓
Larger files
  ↓
S3
  ↓
Glue
  ↓
Processing
</code></pre>
<hr />
<h1>Another Option: Glue File Grouping</h1>
<p>AWS Glue also provides mechanisms for grouping input files for processing.</p>
<p>For workloads with many small files, Glue's file grouping options can help reduce the number of Spark tasks created for individual files.</p>
<p>For example, depending on the Glue version and source configuration, you may see options such as:</p>
<pre><code class="language-text">groupFiles
groupSize
</code></pre>
<p>The important distinction is:</p>
<blockquote>
<p><strong>Grouping files for processing is not the same thing as physically combining the S3 objects.</strong></p>
</blockquote>
<p>If you use file grouping, the physical files in S3 still exist.</p>
<p>If you perform compaction, you actually create fewer, larger objects.</p>
<p>Both approaches can be useful, but they solve slightly different parts of the problem.</p>
<hr />
<h1>Don't Immediately Increase Retries</h1>
<p>When you see an error such as:</p>
<pre><code class="language-text">Slow Down
503
TooManyRequests
</code></pre>
<p>your first thought might be:</p>
<blockquote>
<p>"Let's increase the retry count."</p>
</blockquote>
<p>Retries can help with transient failures.</p>
<p>But if the underlying problem is a huge number of small files generating a very high request rate, simply increasing retries may make the situation worse or prolong the job.</p>
<p>You could end up with:</p>
<pre><code class="language-text">Request
   ↓
Throttle
   ↓
Retry
   ↓
Throttle
   ↓
Retry
   ↓
Throttle
   ↓
Job eventually fails
</code></pre>
<p>The better approach is to reduce unnecessary request pressure where possible.</p>
<hr />
<h1>How I Would Troubleshoot This Today</h1>
<p>If an AWS Glue job suddenly starts failing while reading from S3, I would check the following.</p>
<h3>Step 1: Check the number of files</h3>
<p>Don't only check the total size.</p>
<p>Look at:</p>
<pre><code class="language-text">Total objects
Average object size
Largest object
Smallest object
Number of objects per partition/prefix
</code></pre>
<p>For example:</p>
<pre><code class="language-text">Total data:       500 GB
Number of files:  8 million
Average size:     ~65 KB
</code></pre>
<p>That should immediately make you suspicious of a small-files problem.</p>
<hr />
<h3>Step 2: Check the Glue logs</h3>
<p>Look for errors such as:</p>
<pre><code class="language-text">503 Slow Down
TooManyRequests
Read timeout
S3 request failures
</code></pre>
<p>Also look at when the failures occur.</p>
<p>If the job fails while discovering or reading a very large number of objects, the file count is worth investigating.</p>
<hr />
<h3>Step 3: Check the S3 layout</h3>
<p>Look at the prefixes and partitions.</p>
<p>For example:</p>
<pre><code class="language-text">s3://bucket/raw/date=2026-09-25/
</code></pre>
<p>Then check how many objects exist underneath that prefix.</p>
<p>Sometimes one partition may contain an unexpectedly large number of files.</p>
<hr />
<h3>Step 4: Check how the files are generated</h3>
<p>This is often the most important step.</p>
<p>Ask:</p>
<blockquote>
<p>Why are we producing so many files?</p>
</blockquote>
<p>Potential causes include:</p>
<ul>
<li><p>Too many Spark partitions</p>
</li>
<li><p>Frequent small writes</p>
</li>
<li><p>Streaming-style ingestion</p>
</li>
<li><p>Poor partitioning strategy</p>
</li>
<li><p>Multiple upstream jobs writing independently</p>
</li>
<li><p>Very small batches</p>
</li>
<li><p>Over-partitioned datasets</p>
</li>
</ul>
<p>Fixing the producer can be much better than repeatedly fixing the consumer.</p>
<hr />
<h1>What File Size Should We Target?</h1>
<p>There isn't one magic number that works for every workload.</p>
<p>A common approach for analytics workloads is to aim for reasonably sized files rather than thousands of tiny ones.</p>
<p>For example:</p>
<pre><code class="language-text">1 KB       ❌
10 KB      ❌
100 KB     ❌
1 MB       ⚠️
10 MB      ⚠️
64 MB      ✅
128 MB     ✅
256 MB     ✅
</code></pre>
<p>These are only illustrative ranges, not hard AWS requirements.</p>
<p>The right size depends on:</p>
<ul>
<li><p>Dataset size</p>
</li>
<li><p>Query pattern</p>
</li>
<li><p>Number of partitions</p>
</li>
<li><p>Glue/Spark workload</p>
</li>
<li><p>Compression</p>
</li>
<li><p>Downstream consumers</p>
</li>
<li><p>Frequency of writes</p>
</li>
</ul>
<p>The goal isn't:</p>
<blockquote>
<p>"Make every file exactly 128 MB."</p>
</blockquote>
<p>The goal is:</p>
<blockquote>
<p><strong>Avoid unnecessarily large numbers of tiny files.</strong></p>
</blockquote>
<hr />
<h1>A Simple Before and After</h1>
<h3>Before</h3>
<pre><code class="language-text">S3

file-001 → 1 KB
file-002 → 2 KB
file-003 → 1 KB
file-004 → 3 KB
...
file-100000 → 2 KB

          ↓

AWS Glue

Huge number of input files
          ↓
Many requests
          ↓
High request rate
          ↓
Slow Down / read failures
</code></pre>
<h3>After</h3>
<pre><code class="language-text">S3

part-001 → 128 MB
part-002 → 128 MB
part-003 → 128 MB
part-004 → 128 MB

          ↓

AWS Glue

Fewer input files
          ↓
Fewer file operations
          ↓
Less overhead
          ↓
More stable processing
</code></pre>
<hr />
<h1>The Bigger Lesson</h1>
<p>This issue taught me something that is easy to overlook when working with cloud data platforms.</p>
<p>When we look at a dataset, we usually ask:</p>
<blockquote>
<p>"How much data do we have?"</p>
</blockquote>
<p>But in distributed systems, another question is equally important:</p>
<blockquote>
<p><strong>"How many objects are we dealing with?"</strong></p>
</blockquote>
<p>For example:</p>
<pre><code class="language-text">Dataset A

1 TB
1,000 files

Dataset B

1 TB
10,000,000 files
</code></pre>
<p>Both contain approximately the same amount of data.</p>
<p>But their processing characteristics can be very different.</p>
<p>The second dataset has a massive small-files problem.</p>
<hr />
<h1>Key Takeaways</h1>
<p>Here are the main things I took away from this issue:</p>
<h3>1. Small files are not always harmless</h3>
<p>A few small files are fine.</p>
<p>Millions of small files can become a serious operational problem.</p>
<h3>2. Look at file count, not only data volume</h3>
<p>Always check:</p>
<pre><code class="language-text">Total data
+
Number of files
+
Average file size
</code></pre>
<h3>3. Too many files can increase S3 request activity</h3>
<p>When a distributed processing job has to work with a huge number of objects, request volume and metadata/task overhead can become significant.</p>
<h3>4. Glue file grouping and compaction are different</h3>
<p>Grouping can reduce processing/task overhead.</p>
<p>Compaction actually reduces the number of physical S3 objects.</p>
<h3>5. Don't blindly increase retries</h3>
<p>Retries are useful for transient failures, but they don't fix a poor file layout.</p>
<h3>6. Fix the source when possible</h3>
<p>If an upstream application is continuously generating tiny files, fixing the writing strategy is often better than repeatedly compensating downstream.</p>
<hr />
<h1>Final Thoughts</h1>
<p>The interesting part of this issue was that the dataset itself wasn't necessarily huge.</p>
<p>The problem was <strong>how the data was stored</strong>.</p>
<p>A large number of tiny files created a completely different workload for AWS Glue and S3 than a smaller number of reasonably sized files.</p>
<p>Once we looked at the problem from that perspective, the solution became much clearer:</p>
<pre><code class="language-text">Too many small files
        ↓
Reduce the file count
        ↓
Compact / group files
        ↓
Fewer read operations
        ↓
More stable Glue processing
</code></pre>
<p>So, if your AWS Glue job suddenly starts throwing S3 read errors, don't immediately assume that the data is corrupted.</p>
<p><strong>Check the number and size of the files first.</strong></p>
<p>Sometimes the real problem isn't the data.</p>
<p>It's the <strong>number of files containing the data.</strong></p>
<hr />
<h2>Tags</h2>
<p><code>AWS</code> <code>AWS Glue</code> <code>Amazon S3</code> <code>Apache Spark</code> <code>PySpark</code> <code>Data Engineering</code> <code>Big Data</code> <code>Cloud Computing</code> <code>ETL</code></p>
]]></content:encoded></item></channel></rss>