vegman.dev
All posts

Silence Is the Correct Output

2026-05-28article

At six in the morning the phone buzzes: "Queue empty, no new material." At six thirty - the same message. At seven - again. By the end of the day there are forty-eight of them, and you do the only sensible thing: you turn off notifications for that task. A week later the queue actually jams - and you hear about it from a person who was waiting on the result.

The scheduler (the service that runs tasks on a timetable) did exactly what it was asked to do. The mistake came earlier: the task was designed as "every half hour, tell me what you see" rather than "stay quiet while everything is fine."

What follows: how I sort recurring tasks into types, why for most of them the correct output is empty, who is allowed to wake a human at all, and in what order a task should mark work as done so that data doesn't get lost.

Three kinds of recurring tasks

In my systems, recurring tasks come in three kinds, and their requirements differ.

  • Data collection. The task reaches outward - to a source, to a mailbox, to somebody else's API - and pulls in what's new. The main risk here isn't noise, it's loss: anything pulled in but not processed disappears without a trace.
  • State checking. The task compares what exists now with what was known last time. The main risk is noise: this kind of task runs often and normally finds nothing.
  • Delivery to a human. The only kind that is allowed to write to a messenger at all. The main risk is duplicates: the same fact delivered twice devalues the channel.

Mixing them into one task is tempting and almost always bad. A mixed task inherits the worst requirements of all three at once: it must not lose, must not make noise, and must not duplicate - and when it fails, there's no telling which part of it managed to run.

Silence as the normal state

The main rule: a task that usually finds nothing should produce empty output. Not "nothing yet," not "check complete, 0 records," but literally empty output. Empty output is silence.

The arithmetic here is simple and merciless. A task that checks the queue every half hour and honestly reports "nothing yet" will wake a person 48 times a day. A task like that gets turned off within a day - and along with the noise, you turn off the rare messages the whole thing was built for.

A second consequence of the same rule: the task needs state, otherwise it will report the same event as many times as it runs. If an object moved into a problem state and stayed there, that's one event, not one event per run. A simple file mapping "object - last known state" is almost always enough for me: the task compares the current value with the recorded one, reports only on a mismatch, and updates the record. You don't need a database for this.

How to tell silence from death

Silence has an unpleasant property: a broken task looks exactly like a working one. Both are quiet.

So for every recurring task, three things must be visible: time of last run, status, and text of the last error. Without these you can't tell "all quiet" from "the task has been failing on line two for a month."

Related practical detail, about intervals. A scheduler usually distinguishes between "in 30 minutes, once" and "every 30 minutes." A task created as one-shot will quietly run exactly once and never again. No error, no crash - just permanent silence, indistinguishable from normal. I check this not by looking at the creation code but by the time of the last run: if it isn't moving, the task isn't "calm," it's dead.

flowchart LR
  subgraph before["Было: проверка через модель"]
    A[Пустая очередь] --> B[Вызов модели на форматирование]
    B --> C[1007 вызовов]
    C --> D[45 процентов всех расходов на модели]
  end
  subgraph after["Стало: проверка скриптом"]
    E[Пустая очередь] --> F[Скрипт сравнивает условие]
    F --> G[Ноль вызовов модели]
    G --> H[Пустой вывод, то есть молчание]
  end
Один и тот же результат при пустой очереди: проверка через модель и проверка скриптом

Who has the right to wake a human

The second rule: a deterministic check is a script, not an agent with a model. Deterministic means that for the same input the result is always the same: compare two values, count records, check a date. There's nothing to interpret here; there's a condition.

I once had a recurring task with a model assigned to formatting eat 45 percent of my entire model spend. It made 1007 calls - with an empty queue. Which is to say, the money went entirely toward neatly formatting "nothing found."

Hence a separation of duties that I hold to strictly:

  • The script decides whether there's a reason. That's a condition: cheap, predictable, verifiable by eye.
  • The model is called only once there is a reason, and only for the part that needs interpretation - for example, pulling scattered records into coherent text.
  • Only delivery wakes a human, and only with a fact they haven't seen yet.

Using a model to check "is it time yet" means paying for a question whose answer is computed by a comparison.

flowchart TD
  A[Прогон сборщика] --> B[Читаем непомеченные источники]
  B --> C[Обработка и вызов модели]
  C -->|Успех| D[Помечаем просмотренными]
  C -->|Ошибка| E[Состояние не меняем]
  D --> F[Обновляем файл состояния]
  E --> G[Сигналы уедут на следующем прогоне]
Пометка «просмотрено» - последний шаг прогона, а не первый

Order of operations: when to mark something done

The third rule follows from the first kind of task - collection. I had a signal collector that first marked sources as seen and then called the model. The logic is understandable: "took it into work, marked it so I don't take it again." As long as the call didn't fail, it all looked right.

When the call failed, the signals were already marked. A repeat run didn't see them - as far as it was concerned, they'd been seen. Data was lost permanently and silently: the task ended with an error, the error got fixed, and whatever fell through the gap between the marking and the call could no longer be recovered from anywhere.

The correct order is the reverse: mark only after successful processing. Read, process, confirm the result came back, and only then update state.

The fifth rule is the same thought from another angle: a task failure must not change state. If publishing didn't work, the material stays in the queue and goes out on the next run. That's boring, quiet behavior - and it's exactly what lets you fix the task calmly instead of in data-rescue mode.

The side effect is pleasant: the task becomes safe to rerun. A run that crashed broke nothing. A run that succeeded won't send the same thing a second time, because state has already been updated.

Put it to work

What to check in practice

  • Take each of your recurring tasks and name its kind: collection, checking, or delivery. If one word doesn't do it - the task is doing three jobs and should be split.
  • Run the task on deliberately empty data and look at the output. If there's even one character there, that's future spam.
  • Find every model call inside the task and ask of each one: is this interpretation or a condition? Move conditions into the script.
  • Count the model calls per day and compare with the number of real finds. A discrepancy of hundreds of times over is a bill for the phrase "nothing here."
  • Find the place in the code where the "processed" mark is set, and make sure it comes after successful processing, not before.
  • Break the task on purpose - cut off the call, feed it an unreachable source - and check that after the crash the data is still in the queue.
  • Open whatever creates the schedule and make sure it says "every N minutes," not "in N minutes, once."
  • Look at the last-run time for all your tasks. Anything that hasn't moved for longer than its own interval, treat as broken until proven otherwise.

How to know it worked

There's one criterion, and it's awkwardly simple: over a day of normal operation the system wrote you nothing - and you're calm about it, because in a minute you can open the task list and see that they all ran on time and finished without errors.

If the system is silent and you're not calm, you're short on observability. If the system writes and you've stopped reading, you're short on silence. It worked when you have both: quiet as the norm, and a message at exactly the moment something changed that you hadn't seen yet.