Smart Manufacturing

Cycle Time Monitoring with IoT: From PLC Signals to Bottleneck Analysis

Measure every single cycle instead of sampling with a stopwatch. Capture cycle signals with an ESP32, publish them over MQTT and compute rolling statistics that reveal the real bottleneck of a line.

Cycle time monitoring chart comparing station cycle times against takt time with the bottleneck highlighted, fed by MQTT data from the line
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

Cycle time monitoring replaces the stopwatch study with a measurement of every cycle, on every station, all day. In this tutorial you will capture cycle signals from a PLC output or a sensor with an ESP32, publish each cycle over MQTT, and run a small analysis service that computes rolling statistics per station and identifies the bottleneck of the line against takt time.

Cycle Time, Takt Time and Why the Difference Matters

Three terms are often mixed up:

Term Definition Source
Takt time Available production time divided by customer demand; the pace the line must achieve Planning
Ideal (design) cycle time Fastest possible time for one cycle under ideal conditions Machine specification, process engineering
Actual cycle time Measured time between consecutive cycle completions at a station Measurement

When a station's actual cycle time is longer than takt, the line cannot meet demand without overtime. When it is longer than the ideal cycle time, the gap is a performance loss in OEE terms. A stopwatch study gives a snapshot of a few dozen cycles; continuous monitoring shows the full distribution, including the slow cycles that happen at shift start, after tool changes or with certain operators and materials.

What Signal to Use

Pick a signal that marks the same point of every cycle:

  1. PLC "cycle complete" output or a bit you can map to a spare output. The most accurate option.
  2. Part-present sensor at the station exit, such as a photoelectric or inductive sensor.
  3. Clamp or door signal for manually loaded machines, if one open/close sequence equals one cycle.
  4. Current signature of a spindle or press, as a last resort when no discrete signal exists.

All 24 V signals must reach the microcontroller through an optocoupler or a proper level-shifting input module, never directly.

Topic Design

One message per cycle keeps the raw data intact and lets any consumer compute its own statistics:

connectmq/plant1/assembly/line1/st-10/cycle       {"seq":1532,"ct_ms":41250,"ts":1760600000}
connectmq/plant1/assembly/line1/st-20/cycle       ...
connectmq/plant1/assembly/line1/cycle-stats       retained summary for all stations

The sequence number lets consumers detect lost messages, and ct_ms is computed on the device from its own clock, so network latency does not distort the measurement.

Capturing Cycles on an ESP32

The sketch below uses a hardware interrupt to timestamp each rising edge with millis(), computes the time since the previous edge, and publishes one JSON message per cycle with PubSubClient. Install PubSubClient from the Arduino Library Manager. If you are new to the board, the ESP32 MQTT tutorial covers the toolchain.

#include <WiFi.h>
#include <PubSubClient.h>

const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* BROKER    = "broker.connectmq.com";
const char* STATION   = "st-10";
const char* TOPIC     = "connectmq/demo/ctm-3k8p/line1/st-10/cycle";

const int CYCLE_PIN = 4;                   // optocoupler output, active LOW
const unsigned long MIN_CYCLE_MS = 2000;   // ignore bounces and double pulses

WiFiClient net;
PubSubClient mqtt(net);

volatile unsigned long lastEdge = 0;
volatile unsigned long pendingCt = 0;
volatile bool havePending = false;
unsigned long seq = 0;

void IRAM_ATTR onCycle() {
  unsigned long now = millis();
  unsigned long ct = now - lastEdge;
  if (ct < MIN_CYCLE_MS) return;
  if (lastEdge != 0) {
    pendingCt = ct;
    havePending = true;
  }
  lastEdge = now;
}

void connectMqtt() {
  while (!mqtt.connected()) {
    String id = String("ctm-3k8p-") + STATION;
    if (mqtt.connect(id.c_str())) {
      Serial.println("MQTT connected");
    } else {
      Serial.printf("MQTT failed, rc=%d\n", mqtt.state());
      delay(2000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(CYCLE_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(CYCLE_PIN), onCycle, FALLING);

  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(250);
  mqtt.setServer(BROKER, 1883);
}

void loop() {
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();

  if (havePending) {
    noInterrupts();
    unsigned long ct = pendingCt;
    havePending = false;
    interrupts();

    char payload[96];
    snprintf(payload, sizeof(payload), "{\"seq\":%lu,\"ct_ms\":%lu,\"up_ms\":%lu}",
             ++seq, ct, millis());
    mqtt.publish(TOPIC, payload);
    Serial.println(payload);
  }
}

The MIN_CYCLE_MS guard filters contact bounce and double pulses; set it to roughly half the fastest realistic cycle. The first edge after boot only starts the clock, so no fake long cycle is reported. PubSubClient publishes at QoS 0; if you need QoS 1 delivery for every cycle, AsyncMqttClient supports it, or you can buffer cycles and publish them in batches. For production, switch to WiFiClientSecure, port 8883 and the credentials issued by your broker.

Analysing Cycle Time Monitoring Data

The analysis service subscribes to all stations, keeps the last 200 cycles per station in memory, and publishes a retained summary every 30 seconds. Using the median and the 90th percentile instead of the mean keeps occasional very long cycles (which are really short stops) from distorting the picture.

#!/usr/bin/env python3
"""Rolling cycle time statistics and bottleneck detection."""
import json
import statistics
import threading
import time
from collections import defaultdict, deque

import paho.mqtt.client as mqtt

PREFIX = "connectmq/demo/ctm-3k8p/line1"
TAKT_MS = 45_000
STOP_THRESHOLD_MS = 3 * TAKT_MS        # longer gaps are short stops, not cycles

cycles = defaultdict(lambda: deque(maxlen=200))
lock = threading.Lock()


def on_connect(client, userdata, flags, reason_code, properties):
    client.subscribe(f"{PREFIX}/+/cycle", qos=1)


def on_message(client, userdata, msg):
    station = msg.topic.split("/")[-2]
    ct = json.loads(msg.payload)["ct_ms"]
    if ct <= STOP_THRESHOLD_MS:
        with lock:
            cycles[station].append(ct)


def p90(values):
    return statistics.quantiles(values, n=10)[-1] if len(values) >= 10 else max(values)


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="ctm-stats-3k8p")
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.loop_start()

while True:
    time.sleep(30)
    with lock:
        stats = {
            st: {"n": len(v), "median_ms": int(statistics.median(v)), "p90_ms": int(p90(v))}
            for st, v in cycles.items() if v
        }
    if not stats:
        continue
    bottleneck = max(stats, key=lambda s: stats[s]["median_ms"])
    summary = {
        "ts": int(time.time()),
        "takt_ms": TAKT_MS,
        "bottleneck": bottleneck,
        "over_takt": [s for s, v in stats.items() if v["median_ms"] > TAKT_MS],
        "stations": stats,
    }
    client.publish(f"{PREFIX}/cycle-stats", json.dumps(summary), qos=1, retain=True)
    print(json.dumps(summary, indent=2))

You can test it without hardware by publishing synthetic cycles:

for i in $(seq 1 20); do
  mosquitto_pub -h broker.connectmq.com -p 1883 -i ctm-sim-$RANDOM \
    -t connectmq/demo/ctm-3k8p/line1/st-20/cycle \
    -m "{\"seq\":$i,\"ct_ms\":$((44000 + RANDOM % 6000))}"
  sleep 1
done

Note that the public broker allows 10 messages per second per client, which is far more than any real station needs. Everything published there is publicly readable, so use it only for tests and synthetic data.

Reading the Results

Observation Likely meaning Next step
Median above takt at one station True bottleneck Rebalance work content, add capacity, reduce setup
Median below takt but p90 far above Instability, not capacity Look for material, operator or quality causes of slow cycles
All stations close to takt, line still misses output Short stops or blocking/starving Combine with machine states and buffer levels
Cycle time drifts upward over weeks Tool wear, degrading equipment Correlate with maintenance data
Large difference between shifts Method or staffing differences Standardise work, train

The bottleneck station is where improvement pays off for the whole line; improving any other station only builds inventory. Watch for the bottleneck moving after changes, which is a sign of a balanced line.

Rolling Out Cycle Time Monitoring Across a Line

Start with the station everyone suspects is the bottleneck and one station on either side of it. Three stations are enough to see whether the slow cycles are caused by the station itself or by starving and blocking from its neighbours. Once the signal choice and the debounce settings are proven, copy the same sketch to the remaining stations and change only the station name and topic.

A few practical rules make the rollout smoother:

  1. Name stations exactly as the line layout does. Operators and engineers must recognise st-20 on the dashboard without a lookup table.
  2. Agree on the cycle point per station. If one station counts at part entry and the next at part exit, the numbers are not comparable.
  3. Record the takt time with the data. Takt changes when demand changes; storing it in the summary keeps old reports interpretable.
  4. Check the device clocks. ct_ms comes from the device's own timer and is reliable, but any timestamps you add for correlation need NTP-synchronised gateways.
  5. Show the data at the line. A screen with the live median per station against takt starts more improvement conversations than a weekly report.

In the ConnectMQ portal, the live topic explorer is a quick way to confirm every station is publishing during commissioning, and a threshold alert rule on a station's median_ms records each time it drifts above takt.

Connecting to Other Metrics

Cycle data is the performance leg of OEE. Combined with running time from machine status monitoring and good-part counts from production counting with IoT, you have everything needed to calculate OEE. Keep all of it on one broker and one topic hierarchy, and every new KPI becomes a subscriber rather than a new integration project. If you want to understand what an MQTT broker does as the hub of this design, start there; the smart manufacturing guide puts it into the wider roadmap.

Get Started with ConnectMQ

A line with dozens of stations publishing every cycle needs a broker that is always reachable. ConnectMQ is a cloud MQTT broker with TLS, MQTT 5, retained messages, a live topic explorer and threshold alerts on fields such as median_ms. Create a free Serverless broker, no credit card required, and connect your first station.

Frequently Asked Questions

What is the difference between cycle time and takt time?

Takt time is the pace required by customer demand, calculated from available time and demand. Cycle time is how long a station actually takes per unit. A station whose cycle time exceeds takt limits the line's output.

Should I use the average or the median cycle time?

Use the median for the typical cycle and a high percentile such as p90 for variability. The average is easily distorted by a few long cycles that are really short stops.

Can I measure cycle time without access to the PLC?

Yes. A photoelectric sensor at the station exit, a door or clamp signal, or the current signature of the main drive can mark each cycle. PLC signals are the most accurate when available.

How do I handle very long gaps between cycles?

Treat gaps much longer than takt as stops rather than cycles. Exclude them from cycle statistics and record them as downtime using machine state data.

Related articles

OEE vs TEEP vs OOE: Which Metric Should You Use?

OEE, OOE and TEEP share the same performance and quality factors but measure against different time bases. This comparison works through one example and lists the MQTT signals each metric needs.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.