From Factory Sensors to Failure Warnings: An IoT Pipeline on AWS and Databricks

Quick answer: Sensor data becomes useful for predictive maintenance once it reaches one platform in one schema. Route MQTT messages through AWS IoT Core rules that normalise each manufacturer's payload, partition Kinesis by production line, land Parquet on S3, and load it into Delta tables on Databricks with Auto Loader. Expect the network approvals to take longer than the code. For a steel manufacturer with 847 sensors on 12 lines, models built on this data now give 4 to 72 hours of warning before failures.

Last updated: October 2026

Many plants already collect a great deal of sensor data. It sits in local historian servers, and nobody analyses it, so a failing bearing on a rolling mill is noticed when the line stops.


This article describes the pipeline we built for a steel manufacturer, covered in our manufacturing IoT case study. The plant had 847 sensors across 12 production lines, generating roughly 2 TB of data a day, and each unplanned stoppage cost an average of $180,000 in lost production, scrap and emergency repairs. Maintenance was about 90% reactive. Project details come from that engagement. The rule SQL and code are simplified illustrations written for this article.


Start with the network, not the code


The hardest problem was not technical. The operational technology (OT) team controlled the production network and did not trust IT, or us, to touch it, and the IT team had no access to that network. Sensor data was only approved to leave the production network once we had set up an isolated DMZ with a unidirectional data diode.


On a manufacturing project, we recommend planning three to four weeks of security review and network architecture work before writing any pipeline code.


Normalise payloads in IoT Core rules


All 847 sensors connect to AWS IoT Core over MQTT and publish every five seconds: vibration amplitude, temperature, pressure, current draw and RPM. The payloads were not standardised. Six sensor manufacturers each had their own format and timestamp precision; some sent epoch seconds, others ISO 8601 strings with time zone offsets, and two sent local time with no time zone at all.


We built a normalisation layer in IoT Core rules that turns every payload into one common schema before it reaches Kinesis. A rule is a SQL statement over an MQTT topic filter, with + and # as wildcards, and Kinesis actions can take the partition key from a substitution template. One rule per manufacturer keeps each mapping small and testable.


One normalisation rule per manufacturer (IoT Core SQL, version 2016-03-23)
-- Topic layout: plant/sensors/<line_id>/<sensor_id>/vendor_a
SELECT
    topic(3)          AS line_id,
    topic(4)          AS sensor_id,
    'vendor_a'        AS vendor,
    vib_amp           AS vibration_amplitude,
    temp_c            AS temperature_c,
    pressure_kpa      AS pressure_kpa,
    ts * 1000         AS event_epoch_ms,      -- vendor A sends epoch seconds
    timestamp()       AS received_epoch_ms
FROM 'plant/sensors/+/+/vendor_a'

-- Kinesis action on this rule:
--   streamName:   sensor-readings
--   partitionKey: ${topic(3)}-${topic(4)}

Time zones need the most care. IoT Core's time_to_epoch() function treats a timestamp string with no time zone as UTC, so a local time parsed in the rule would be silently shifted by the plant's UTC offset. For devices that send local time without a zone, we recommend passing the original string through unchanged, along with the plant's time zone, and converting it in Databricks, where the conversion can be tested. A rule's error action receives any message whose action fails, with the original payload, which is worth routing to a separate stream for inspection.


Partition Kinesis so no shard runs hot


From IoT Core, messages go to Kinesis Data Streams. In provisioned mode each shard accepts up to 1 MB per second or 1,000 records per second of writes, and a single partition key always maps to one shard. That second fact decides the key. Partitioning by production line alone caps each line at one shard's throughput; adding the sensor ID to the key, as in the rule above, spreads one line across shards when it needs more.


In the project, Kinesis was partitioned by production line, and each of the 12 lines had its own group of shards, which avoided hot partitions. On-demand mode is an alternative that scales automatically, but AWS notes that it does not isolate hot hash keys, so provisioned mode remains the better fit when keys are very uneven.


Parquet on S3, then Delta with Auto Loader


A Firehose delivery stream reads from Kinesis and writes compressed Parquet files to S3, partitioned by line, date and hour. Firehose converts JSON to Parquet using a schema held in the AWS Glue Data Catalog. On top of S3 we built a Databricks lakehouse on Delta Lake, whose ACID transactions matter when dashboards read the same tables that streaming ingestion is writing. Delta uses optimistic concurrency without locks, and two concurrent INSERTs cannot conflict under its default isolation level.


Auto Loader is the simplest way to pick up new Parquet files as they land. Its default mode lists the directory; Databricks now recommends file notification mode with file events for most workloads, which needs Databricks Runtime 14.3 LTS or later on S3.


Load new Parquet files into a bronze Delta table (PySpark, illustrative)
(spark.readStream.format("cloudFiles")
    .option("cloudFiles.format", "parquet")
    .option("cloudFiles.useManagedFileEvents", "true")
    .option("cloudFiles.schemaLocation", "/Volumes/iot/raw/schemas/sensor_readings")
    .load("s3://<bucket>/sensors/")
  .writeStream
    .option("checkpointLocation", "/Volumes/iot/raw/checkpoints/sensor_readings")
    .trigger(processingTime="1 minute")
    .toTable("iot.bronze.sensor_readings"))

Auto Loader records which files it has processed in the checkpoint, so each file is loaded exactly once even if the stream restarts.


Converting local timestamps in Databricks


For readings that arrive in local time, the silver layer converts the local value to UTC using the plant's zone. If you also want to keep the original local value as a typed column, Databricks' TIMESTAMP_NTZ type, available from Databricks Runtime 13.3 LTS, stores a timestamp without any time zone, which matches a value that came without one.


Normalise every reading to UTC (Databricks SQL, illustrative)
SELECT
    line_id,
    sensor_id,
    vendor,
    CASE
        WHEN event_epoch_ms IS NOT NULL
            THEN timestamp_millis(event_epoch_ms)
        ELSE to_utc_timestamp(local_ts_string, plant_time_zone)   -- e.g. 'America/Chicago'
    END AS event_ts_utc,
    vibration_amplitude,
    temperature_c,
    pressure_kpa
FROM iot.bronze.sensor_readings;

Local times without a zone are ambiguous for one hour each year where daylight saving time applies, when clocks go back and the same local hour occurs twice. If the plant observes daylight saving, flag readings from that hour and use the receipt time from the IoT rule to place them.


From history to failure warnings


The value came from the models. We trained anomaly detection models in Databricks on 18 months of historical sensor data, correlated with maintenance logs, and they flag three failure precursors:


PrecursorTypical warning before failure
Shifts in bearing vibration patterns24 to 72 hours
Temperature drift on hydraulic systems4 to 12 hours
Pressure anomalies in pneumatic actuators8 to 48 hours

Alerts go to a dashboard on large monitors on the shop floor and, at the same time, to the maintenance team's pagers through Amazon SNS. Reactive maintenance fell from about 90% of the workload to 35%, and the plant now saves $3.1 million a year in unplanned downtime. The whiteboard in the break room is still there, but it now counts the days since the last unplanned stoppage.


Pranay Vatsal, Founder & CEO

Pranay Vatsal is the Founder & CEO of CelestInfo with deep expertise in Snowflake, data architecture, and building production-grade data systems for global enterprises.

Related Articles

Frequently Asked Questions

Q: How should I choose a Kinesis partition key for IoT data?

A partition key always maps to one shard, and in provisioned mode a shard accepts up to 1 MB per second or 1,000 records per second. Use a key with enough distinct values, such as line ID plus sensor ID, so no single key needs more than one shard's throughput.

Q: How does AWS IoT Core handle timestamps without a time zone?

IoT Core's time_to_epoch() function treats a timestamp string with no time zone as UTC. If devices send local time without a zone, pass the string through and convert it with the plant's time zone in a later step, where the conversion can be tested.

Q: What is the recommended Auto Loader mode on Databricks?

Directory listing is the default, but Databricks recommends file notification mode with file events for most workloads. On S3, file events require Databricks Runtime 14.3 LTS or later.

Q: Can streaming writes and dashboards use the same Delta table?

Yes. Delta Lake uses optimistic concurrency with snapshot isolation for reads, so dashboards see consistent snapshots while streams append. Two concurrent INSERTs cannot conflict under the default isolation level.

Q: How long does it take to get sensor data out of a production network?

In our experience, plan three to four weeks of security review and network architecture work before writing pipeline code. In the project described here, data was approved to leave the production network only after an isolated DMZ with a unidirectional data diode was in place.