cn|
WorkWritingAboutConnect

cn

building things that matter

  • Work
  • Writing
  • About
  • Connect

© 2026 Cristian Najera

  1. Home
  2. /Work
  3. /Production Data Engineering Pipeline

Data Engineering

Near-real-time analytics at 50M events/day near-real-time

The analytics team was flying blind — business metrics were always 8 hours old because the pipeline ran once a day. We rebuilt it around Kinesis and AWS Glue, landing 50M+ events/day into Redshift Serverless with sub-4-minute end-to-end latency.

Client

Enterprise (B2B SaaS)

Role

Data Engineer

Year

2024

Duration

4 months

Project previewComing soon
Events/day

50M+

Pipeline Latency

<4 min

Cost Reduction

67%

Uptime

99.7%

When the analytics team asks "how many users activated this week?" and the answer is eight hours old, every decision downstream is a guess. The company's clickstream pipeline ran as a nightly batch — a cron job pulling the previous day's events from S3, transforming them in a monolithic PySpark script, and loading them into a provisioned Redshift cluster. One missed window meant waiting another 24 hours.

The pipeline processed 50M+ events per day at peak. The data wasn't the problem; the architecture was. We needed the same data, fresher, without blowing up the infrastructure budget.

Pipeline architecture

The replacement pipeline followed a five-stage flow: ingest, land, transform, serve, orchestrate.

Kinesis Data Streams captured events from the application SDK with two shards delivering 1 MB/s per shard. A Lambda consumer batched records and wrote them to S3 in Hive-partitioned prefixes (dt=YYYY-MM-DD/hr=HH/), giving us partition pruning for free downstream.

An AWS Glue PySpark job ran every five minutes, reading only the latest partitions. The transform layer handled deduplication, timestamp normalization, null filtering, and session ID hashing — all in a single pass.

from awsglue.context import GlueContext
from pyspark.context import SparkContext
from pyspark.sql import functions as F
 
sc = SparkContext()
glue_context = GlueContext(sc)
 
raw = glue_context.create_dynamic_frame.from_catalog(
    database="raw_events",
    table_name="clickstream",
    push_down_predicate="partition_dt >= '2024-01-01'",
)
 
cleaned = (
    raw.toDF()
    .filter(F.col("event_type").isNotNull())
    .withColumn("event_ts", F.to_timestamp("event_ts", "yyyy-MM-dd'T'HH:mm:ss"))
    .withColumn("session_id", F.sha2(F.col("user_id").cast("string"), 256))
    .dropDuplicates(["event_id"])
)

Cleaned data landed in Redshift Serverless at 128 RPU base capacity. dbt incremental models handled the business logic layer — session aggregation, funnel metrics, cohort definitions — and ran as part of the same Airflow DAG that triggered the Glue job.

MWAA Airflow orchestrated the entire flow on a five-minute cadence: trigger Glue job, wait for completion, run dbt models, publish metrics. The DAG was the single source of truth for pipeline state.

Observability

We instrumented the pipeline at two layers. Infrastructure metrics — Kinesis iterator age, Glue DPU utilization, Redshift query queue depth — fed into CloudWatch dashboards that the on-call engineer monitored. Business SLOs were tracked via custom CloudWatch metrics published directly from the Glue job at the end of each run.

import boto3
 
cloudwatch = boto3.client("cloudwatch", region_name="us-east-1")
 
 
def publish_pipeline_metrics(
    rows_processed: int,
    dedup_count: int,
    job_run_id: str,
) -> None:
    """Publish per-run metrics to CloudWatch for SLO tracking."""
    cloudwatch.put_metric_data(
        Namespace="DataPipeline/Clickstream",
        MetricData=[
            {
                "MetricName": "RowsProcessed",
                "Value": rows_processed,
                "Unit": "Count",
                "Dimensions": [{"Name": "JobRunId", "Value": job_run_id}],
            },
            {
                "MetricName": "DedupRate",
                "Value": dedup_count / max(rows_processed, 1) * 100,
                "Unit": "Percent",
                "Dimensions": [{"Name": "JobRunId", "Value": job_run_id}],
            },
        ],
    )

A dead-letter queue captured events that failed parsing. A CloudWatch alarm on queue depth greater than zero triggered a PagerDuty page immediately — no more silent data loss.

Lessons learned

Partition pruning pays for itself. Hive-partitioned S3 prefixes reduced Glue scan cost by roughly 80%. The job reads only the current and previous hour's partitions on each run, which kept the five-minute cadence feasible without scaling DPU capacity.

Dead-letter queues prevent silent failures. The original batch pipeline silently skipped malformed events. Adding a Kinesis DLQ surfaced data quality issues that had gone undetected for months — broken SDK integrations, timezone mismatches, and truncated payloads.

Redshift Serverless auto-pause needs a cache layer. Without materialized views, auto-pause resume latency of 8–12 seconds was visible to dashboard users. A scheduled dbt model that pre-warmed the most queried aggregations eliminated cold-start pain while keeping costs low during off-hours.

Data Freshness
8 hours (daily batch)→<4 minutes
Measured from event emission to Redshift availability
Compute Cost
~$18K/mo (fixed Redshift cluster)→~$6K/mo (Redshift Serverless + auto-pause)
67% reduction by eliminating always-on provisioned capacity
On-call Incidents
12/month (pipeline failures, stale data alerts)→2/month
Dead-letter queue + replay eliminated silent failures
AWS KinesisAWS GlueRedshift ServerlessdbtAirflow
  1. May 2024

    Architecture design & Kinesis stream provisioning

  2. Jun 2024

    Glue ETL jobs + S3 raw landing zone

  3. Jul 2024

    Redshift Serverless + dbt models + staging validation

  4. Aug 2024

    Production cutover & observability hardening

All projectsGet in touch →