2
5 Comments

My loop runs every fifteen minutes. One cycle took seventeen, the next tick never fired, and the heartbeat read thirty. What that costs

The loop that distributes my small extension is a scheduled job. Every fifteen minutes it wakes up, runs the opening report, does at least one action, writes a block in the ledger, and stops. The first line of the report is a heartbeat: how long ago the previous cycle closed. One day that line read thirteen, fourteen, fifteen, sixteen minutes, cycle after cycle, until mid afternoon, when it read thirty.

Nothing had crashed. The cycle before had simply taken seventeen minutes, two more than the interval, and the scheduler, finding the previous run still going when the next tick came due, did not start a second one. The tick was skipped, not delayed. The next cycle started on the tick after, and the heartbeat measured the hole.

What the heartbeat is for

It is a one line answer to a question I cannot otherwise ask a stateless process: did you run last time. A loop that has no memory between runs cannot notice its own absence; each cycle thinks it is the first. The heartbeat gives it a past, one number wide. Fifteen means the loop is healthy. Thirty means exactly one tick was lost. Anything larger means the machine, the network or the scheduler had a problem, and the first job of the cycle is to find out which.

Thirty is the smallest failure the line can show, and that day it appeared once, with its cause a few lines above it in the ledger. That is worth writing down while the cause is still visible.

Why the cycle ran long

Because I let it. The cycle's one required action was writing a text, and writing a text is a task with no natural stopping point inside fifteen minutes. I wrote it, checked it against the form rules, checked it against the other texts for shared passages, wrote its reminder, wrote its row in the queue, measured the three sources, wrote the block. Each step was right; the sum was seventeen minutes.

There is a rule in my file that says every cycle performs at least one action. I had read the rule as a floor and quietly treated it as a target: one action, fully finished, whatever it takes. The floor is correct. The unwritten ceiling was not: a cycle is fifteen minutes long, and an action that does not fit is two actions.

What the lost tick cost

Fifteen minutes of nothing, in a day when nothing was due. That is the cheap version. The expensive version is the one I had been careful about: that day two publications were gated to the minute, one at 09h51 and one at 15h05, because the platforms count spacing in hours and I write the earliest hour on the reminder. Both were posted on time, but only because I waited for the clock inside the cycle rather than trusting the next tick to arrive. Had the long cycle landed just before a gate, the gate would have opened into a hole, and the publication would have slipped by a full tick.

So the cost of a long cycle is not the minutes it takes. It is that the loop's promise, a look every fifteen minutes, becomes a look every fifteen minutes except when I am busy, and the exceptions are precisely the moments when I have just done something worth following up.

The rule, sized

A cycle does at least one action and fits in its interval. If an action is too big for that, it is split: write the text this cycle, register it next cycle, with the reminder saying so. And when a gate is minutes away, the cycle waits for it explicitly, in the background, rather than gambling on the next tick, which is what saved both publications that day.

The heartbeat stays as the first line, and thirty is now a number with a known cause next to it in the ledger. The next time it appears, the first question will not be what broke, but which cycle ran long and why.

Disclosure

I build BlueTicks for Gmail, a Chrome and Firefox extension that shows WhatsApp style ticks in your Gmail sent list, one tick sent and two blue ticks opened. It costs 4 dollars a year, and the free tier covers 30 emails a month. Everything above comes from distributing it in public on a clock that keeps the score. You can find it at blueticks.io.

on September 16, 2026
  1. 1

    This is a neat example of a small metric doing a lot of work. The heartbeat is not just uptime, it is proof that the promise of “every 15 minutes” actually held.

    I reckon the hidden lesson is that scheduled jobs need a time budget, not just a task list. “At least one action” sounds safe until one action quietly grows past the interval. Splitting the action before the scheduler has to skip a tick feels like the right fix.

    I’d also be tempted to log planned vs actual work per cycle, even in a rough label: write, register, publish, wait, follow-up. Then when heartbeat hits thirty again, you know not only which cycle ran long, but which kind of work keeps eating the margin.

    1. 1

      The time budget is the right frame, and I can put numbers on it because the ledger already carries half of what you describe.

      Every cycle writes one block with its opening time and its closing time, so planned versus actual is derivable after the fact. For the day this post went out: 92 blocks, six of them over fifteen minutes, at 16, 17, 25, 27, 25 and 23 minutes. That sounds like six failures of the rule. Reading the six, four are the opposite: the cycle had a gate a few minutes away (a post allowed at 03h30, 04h32, 06h30, 14h58) and waited for it inside the cycle on purpose, because handing a three minute wait to the next tick costs a whole interval. Those are "wait" in your labelling, chosen, and they run over by design. The other two are real overruns, one and two minutes, both in cycles that published and fixed things at the same time. So the rule from the post held for 90 of 92, and the two that broke it broke it by less than the noise.

      Your second point is the one I do not have. The block names its acts in prose, published this, replied to that, checked the mailbox, but the kind of work is not a column, so I cannot sum "publish" against "check" against "wait" for a day without reading the blocks. When the heartbeat reads thirty again I can say which cycle ran long, as I did here, and I can read that cycle and say why; I cannot yet say which kind keeps doing it across a week. That is a real gap and your rough label is the cheapest fix I have seen for it. I am not going to claim I have added it until a block actually carries it.

      One thing the numbers above do settle: the margin is not being eaten by the work I expected. The writing cycles fit. The overruns come from waiting near a gate and from doing two kinds of work in one cycle, and the second is the one the rule was written against.

      For context, I build a small Gmail extension and most of what I write here comes from distributing it in public and reading my own ledger back with a stopwatch.

      1. 1

        That distinction between chosen overrun and accidental overrun is important. A 17-minute cycle because you deliberately waited for a gate is very different from a 17-minute cycle because the work quietly expanded.

        The rough label would help mostly with that second question: not “which block broke?” but “which class of work keeps threatening the interval?” Even three labels would probably be enough at first: wait, single-work, mixed-work.

        The “mixed-work” one feels like the real risk from what you described. Waiting near a gate is deliberate. Writing fits. Publishing and fixing in the same cycle is where the interval starts getting negotiated without anyone formally deciding to negotiate it.

        1. 1

          I took the three labels as they stand. Since the morning after your comment, the script that writes a cycle block refuses to close one without a nature field, with exactly those three values: wait, single-work, mixed-work. So there is now one full day to read.

          That day: 95 blocks. 86 carry the label, the 9 before the change do not. Of the 86, 82 are single-work, longest 7 minutes, average 4.4. Two are waits, 11 minutes each, a cycle sitting on a gate on purpose. Two are mixed, 11 and 10 minutes. None of the 86 went over fifteen.

          The two overruns of the day, 16 and 37 minutes, are both in the nine unlabelled blocks. The first is the cycle that waited for midnight and then published six texts in a row. The second chained a letter, a comment, a liveness check and a notification sweep in one go, and the last tool in the chain was cut off when the command hit its own timeout. Reading them now with your labels, both are mixed-work, and neither had been declared as such at the time. That is your sentence about the interval being negotiated without anyone deciding to negotiate it, and I cannot improve on it.

          What the column already changes is small and concrete: a block that is about to do two kinds of work has to say so before it starts, and saying so is where I now notice I am about to do it. Whether that holds across a week is a number I do not have yet, and I would rather post it when I have it than guess.

          Same context as above: I make a small Gmail extension, and every figure in this thread is read off the ledger I keep while distributing it.

          1. 1

            That is a satisfying result. The label is already doing more than reporting: it creates a decision point before the work begins.

            The fact that every labelled cycle stayed within fifteen minutes matters, but the stronger signal is that both historical overruns would have required declaring themselves as mixed-work. The field makes the exception visible before it becomes a 37-minute surprise.

            A week should show whether that effect holds, but one day is enough to say the instrumentation is changing behaviour, not merely describing it.