I spent a weekend reviewing a maintenance log for a conveyor system that was costing thousands in “preventative” parts replacements every quarter, only to find that the technicians were throwing away bearings that had 60% of their life left. At the same time, a motor had burned out three weeks before its scheduled service because it had been running hot for a month, but the calendar said it wasn’t time to check it yet.

Time-based maintenance is a gamble where you bet that the average failure rate of a component matches the actual failure rate of your specific machine. In the real world, that bet usually loses.

If you’re managing industrial assets, you’ve likely lived through this. You either over-maintain, wasting money and introducing “infant mortality” failures by disturbing a working system, or you under-maintain and deal with unplanned downtime. The move to Condition-Based Maintenance (CBM) is the only way out, but the gap between the theory of “predictive maintenance” and a working system on the factory floor is massive.

What I tried first

My first attempt at CBM was naive. I thought I could just slap a few sensors on the equipment, pipe the data into a dashboard, and let the operators decide when to perform maintenance. I set up a basic MQTT pipeline using Mosquitto (which I’ve written about before regarding broker selection) and pushed raw vibration and temperature data to a Grafana dashboard.

It failed miserably.

First, I created a “noise apocalypse.” I had alerts firing every time a sensor spiked for a millisecond due to electrical noise. The operators started ignoring the alerts entirely. Second, I didn’t define what “bad” actually looked like. I was giving them raw data, not actionable intelligence. An operator doesn’t care if a motor is at 72 degrees Celsius; they care if 72 degrees is a 10% increase over the baseline for that specific load.

I also tried to automate the ticketing system using simple cron jobs that checked for thresholds every hour. This led to a flood of “HEARTBEAT_OK” messages in the logs and redundant tickets. I was basically just building a more expensive version of a time-based system, just with different triggers. I remember seeing my logs filled with thousands of lines of [INFO] Check completed: status=OK every single hour. It didn’t provide value; it just consumed disk space and made it impossible to find the actual errors when they happened.

The actual solution

The shift happens when you stop treating sensors as “alarms” and start treating them as “state providers.” You need a pipeline that filters noise, establishes a baseline, and triggers actions based on deviations rather than arbitrary numbers.

1. Filtering the Noise

Instead of raw thresholds, I implemented a sliding window average. If you’re using Python for your edge processing, don’t just trigger on val > threshold. Use a buffer. This prevents a single electromagnetic interference (EMI) spike from triggering a factory-wide alert.

import collections

class SensorMonitor:
    def __init__(self, threshold, window_size=10):
        self.threshold = threshold
        self.window = collections.deque(maxlen=window_size)

    def is_anomaly(self, current_value):
        self.window.append(current_value)
        if len(self.window) < self.window.maxlen:
            return False
        
        # Calculate moving average to ignore transient spikes
        avg = sum(self.window) / len(self.window)
        return avg > self.threshold

# Example: Triggering maintenance only if the average 
# vibration stays high over 10 readings
monitor = SensorMonitor(threshold=10.5) 
if monitor.is_anomaly(current_vibration):
    trigger_maintenance_alert("Sustained high vibration detected")

The logic here is simple: we don’t care about the peak; we care about the trend. In an industrial environment, a motor starting up will always create a spike in vibration and current. If you alert on the peak, you’re alerting on the motor working. By using a deque to maintain a sliding window, we ensure that the “anomaly” is a sustained state.

2. Condition-Based Escalation Rules

Once the data is clean, you can’t just send an email. You need escalation logic that understands the context. I moved away from simple cron-based alerts to a condition-based rule engine. This is similar to how I handle equipment health scoring, where we consolidate multiple signals into one status.

I implemented this using a YAML-based rule engine that the backend evaluates against the current state of the asset.

# Condition-based escalation rules for maintenance tickets
escalation_rules:
  - condition: "sensor.vibration > 12.0 AND asset.criticality == 'high'"
    action: "immediate_dispatch"
    priority: 1
  - condition: "sensor.temp_deviation > 15% AND ticket.age > 4h"
    action: "notify_maintenance_lead"
    priority: 2
  - condition: "sensor.vibration > 8.0 AND ticket.age > 24h"
    action: "schedule_inspection_next_shift"
    priority: 3

This structure allows for nuance. A high-criticality asset (like a main turbine) triggers an immediate dispatch on a vibration spike, while a low-criticality auxiliary pump might just get a ticket scheduled for the next shift. The inclusion of ticket.age prevents the system from spamming the lead every five minutes; it only escalates if the condition persists and the situation remains unresolved.

3. Fixing the Alerting Pipeline

To stop the “HEARTBEAT_OK” spam, I restructured the cron job payloads. I stopped sending “all clear” messages as separate alerts and instead moved to a conditional reporting model. If the system is healthy, it logs the state locally but doesn’t fire a notification.

# Example of a condition-based cron job payload
payload:
  message: "Cron job {{ name }} failed with status: {{ status }}"
  condition: "status != 'success'"
  reply: "All crons healthy." if status == 'success'

By changing the wake mode from “next-heartbeat” to “now” in the scheduler and suppressing the success noise, the signal-to-noise ratio improved by about 90%. When an alert actually hits the technician’s phone, they know it’s a real problem, not just the system telling them it’s still alive.

Why it works

Moving to CBM works because it respects the physics of the machine rather than the calendar on the wall. Every single piece of equipment has a unique “wear profile.” One motor might be perfectly aligned and run cool for five years, while another in the same plant, due to a slightly skewed mount, will eat its bearings in six months.

Time-based maintenance treats these two motors as identical. CBM treats them as individuals.

The technical reason this works is the shift from static thresholds to dynamic baselines. A static threshold of 80 degrees Celsius is useless if the ambient temperature in the factory fluctuates by 20 degrees between summer and winter. By calculating the deviation from a rolling baseline (e.g., current_temp - rolling_avg_temp > 10%), we account for environmental variables.

This approach also solves the “infant mortality” problem. In reliability engineering, there is a known curve where the probability of failure is high right after a part is installed or serviced (due to installation errors or defective new parts) and high at the end of life. By only servicing equipment when the condition warrants it, we avoid the risk of introducing new faults into a perfectly stable system.

For those building these pipelines, I recommend looking at the vibration monitoring architecture I’ve detailed elsewhere. The data ingestion layer is where the battle is won or lost. If you don’t have a clean stream of data, your condition-based rules are just guessing.

Lessons learned

The biggest surprise was the human element. I expected the operators to love the new system because it meant fewer unnecessary teardowns. In reality, some of them were terrified. They had spent twenty years trusting the calendar, and the idea of “waiting for the sensor” felt like negligence.

I learned that you cannot deploy CBM as a replacement for time-based maintenance overnight. You have to run them in parallel. I ran the sensors for six months while the team continued their scheduled replacements. We then compared the sensor data to the actual state of the parts being replaced. When we could show the team that the “scheduled” parts were almost always in perfect condition, the trust shifted.

I’d also do a few things differently:

First, I would invest more in edge computing earlier. Sending raw high-frequency vibration data to a central broker is a waste of bandwidth and creates latency. Processing the FFT (Fast Fourier Transform) at the edge and only sending the peak magnitudes and frequencies is the correct way to scale.

Second, I would integrate the maintenance triggers directly into the ERP system rather than relying on a separate ticketing tool. The gap between “sensor triggers alert” and “part is ordered” is where most CBM implementations fail. If the sensor says a bearing is failing but the replacement part takes two weeks to arrive, you’ve just shifted the downtime from “unplanned” to “planned but still happening.”

Finally, be wary of “sensor creep.” It is tempting to add a sensor for everything. But every sensor you add is another point of failure and another source of noise. Focus on the primary failure modes of the equipment. If a motor usually fails due to overheating, start with temperature and current. Don’t add an ultrasonic acoustic sensor unless you have a specific reason to believe that’s how your machines die.

If you’re looking to implement this kind of logic at scale, you might want to check out my infrastructure services for predictive maintenance consulting. Setting up the sensors is the easy part; building the logic that prevents the factory from screaming every time a motor starts up is where the real engineering happens.