For weeks, a cron job at Elevare Digital woke up on schedule, checked its queue, and logged a clean success. It approved exactly zero drafts. Nineteen pieces of content sat waiting. The team only found out later, after the silent gap had grown from an oddity into a small backlog. Nothing had crashed. No paging alerts fired. The system was technically healthy and functionally dead.

This is the quiet horror of autonomous pipelines. When you remove the human from the loop, you also remove the person who notices that nothing is happening.

The Pipeline That Ran Itself

Elevare Digital runs a fully automated content workflow. Software agents generate drafts. A scheduled approver cron acts as the gatekeeper, reviewing those drafts and pushing approved items straight to publishing. No human opens a dashboard to bless each batch. The whole point is that the machine handles the drudgery while the team moves on to other problems.

Under this model, trust becomes your primary interface. You trust the scheduler to fire. You trust the job to run. You trust the exit code. When the logs show a steady heartbeat of 200 OK responses, you assume work is moving. For weeks, that heartbeat was perfect. The cron fired on time, every time. It simply never did the actual work.

Nineteen Drafts and No Alarm

The discovery was accidental. Someone eventually noticed that the publishing queue had gone quiet, or perhaps they checked a downstream metric and saw a flatline. What they found was a stash of nineteen drafts sitting completely untouched. The approver had been running dutifully, logging success every single day, and had processed none of them.

In a manual workflow, a human reviewer would have noticed an empty inbox or a pileup of pending items on day one. In the automated version, the absence of activity looked exactly like the absence of work. The cron had no manager to disappoint. It just kept clocking in and going home early.

Two Bugs, One Empty Result

The failure had two parents. Neither was a syntax error, a timeout, or a dependency outage. Both were semantic mistakes that reduced nineteen valid rows to nothing in the eyes of the query engine.

First, a type mismatch. The agent generating drafts wrote records tagged as article. The approver cron queried specifically for thread types. This is the kind of drift that happens when producers and consumers evolve on parallel tracks. One team—or one agent—decided the output was an article. Another wrote the consumer assuming it would ingest threads. No type system threw a compile-time error because these were likely loose string tags, perhaps JSON fields or unenforced varchar values. The database simply found no matches and returned an empty set. That is not an error condition to the engine. It is a correct answer to a wrong question.

Second, an inner join in the approver’s query quietly swallowed the rows whole. If the query joined the drafts table to another table—perhaps a lookup for metadata, status flags, or routing rules—and the join condition failed, the inner join behaved exactly as designed. It excluded non-matching rows. No orphan rows appeared in the result set. No nulls flagged a problem. The nineteen drafts passed through the query like water through a sieve, and the application layer received a pristine, empty list.

Because the query returned no rows, the function exited cleanly. No exceptions bubbled up. The HTTP response was 200 OK. The cron logged success and went back to sleep.

The Trap of Processed Zero

Here is the crux of the problem. In a queue-based system, a consumer frequently finds zero rows to process. The queue empties out. The worker finishes fast. The log reads processed: 0 and the team reads that as good news: we are keeping up with demand. That is a healthy state.

But processed: 0 encodes two completely different realities:

  • Healthy state: Zero processed because zero pending. Queue is empty. System is idle by design.
  • Broken state: Zero processed because the consumer cannot see the work. Queue has nineteen rows. System is blind, not idle.

Without an independent check on the queue depth, these two states emit identical telemetry. They look the same in dashboards, smell the same in log aggregators, and trigger the same silence inPagerDuty. You have built a monitoring strategy that detects when the worker screams, not when it whispers past a pile of real work.

Closing the Gap

Elevare Digitalは、監視対象を変更することで問題を解決しました。彼らはエラー率や成功ステータスだけに頼るのをやめ、代わりに、利用可能な作業量と完了した作業量の間のギャップに対してアラートを出すようにしました。

各バッチの終了後、彼らは現在、シンプルな不変条件チェック(invariant check)を実行しています。

  • processed が 0 かつ pending 行が 0 より大きい場合、高重要度のアラートをトリガーする。

このルールは、意図的に原因を問わない(agnostic)設計になっています。失敗の原因が、不適切なフィルタリングなのか、結合(join)の失敗なのか、あるいは列挙型(enum)文字列のタイポなのかは問いません。重要なのは、「作業が存在しているのに、作業が全く進んでいない」という事実だけです。これにより、監視の視点が「プロセスがエラーを吐いたか?」から「作業が前進したか?」へとシフトします。

これをサポートするために、彼らはキューの深さ(queue depth)を単なるスポットチェックではなく、時系列で追跡すべき「第一級のメトリクス(first-class metric)」として扱っています。プロデューサーが継続的に行を追加し続けている一方で、コンシューマーが成功を報告し続けている場合、キューの深さの推移は決定的な証拠(smoking gun)となります。静的なスナップショットは嘘をつくことがありますが、増え続けるバックログが嘘をつくことはありません。

自律型システムへの教訓

Elevareの事例には、運用を自動化(hands-off)しているすべての人にとって役立つ、いくつかの実践的なルールが含まれています。

スキャンされた行を、処理された行とは別にログに記録する。 コンシューマーが40行に触れるクエリを実行しても、不適切な条件によってそれらすべてがフィルタリングされ、processed: 0 と報告されることがあります。最終的なカウントのみを記録していると、この「幽霊のような相互作用(ghost interaction)」を見逃してしまいます。「スキャンされた行数(scanned-rows)」のメトリクスがあれば、ワーカーが現場に現れ、作業を確認し、困惑したまま立ち去ったことが明らかになります。スキャン数と処理数の間のこのギャップこそが、多くの場合、最も早い予兆となります。

キューの深さを時系列として追跡する。 キューが一時的に空になるのは問題ありません。ワーカーが正常(green)な状態を維持しているにもかかわらず、キューが単調に増加し続けるのは問題です。キューの深さとコンシューマーのスループットをグラフ化してください。両者が乖離した場合は、たとえすべてのヘルスチェックをパスしていても、直ちに調査を行ってください。

モックだけでなく、実際のプロデューサーの出力に対してコンシューマーをテストする。 モックデータを使用したユニットテストには、テスト担当者の想定が含まれてしまいます。もしモックファクトリが thread 型を生成し、コンシューマーも thread 型を期待しているなら、テストはパスしますが、本番環境では失敗します。プロデューサーの出力から実際のレコードを取得する統合テストを実行してください。コンシューマーが、プロデューサーが書き込んだ内容を真に認識できることを確認してください。

データ型と列挙型(enum)の値を「契約(contract)」として扱う。 JSONブロブ内の緩い文字列タグは、目に見えない失敗点になるまでは便利です。スキーマを明示的に定義し、定数を共有してください。プロデューサーとコンシューマーの境界(seam)でペイロードを検証してください。もし契約が破られた場合は、WHERE 句の中で静かに失敗するのではなく、境界部分で派手に(loudly)失敗させるべきです。

真の教訓

自律型システムは、人間のように失敗することはありません。病欠を申し出ることも、毎回例外を投げることも、明らかなクラッシュダンプを残すこともありません。彼らは 200 OK を返し、在庫が腐っていくのを放置します。もしあなたのアラートが「悲鳴」しか聞き取れないのであれば、最も高くつく失敗、つまり「すべてが正常に見えるのに、何も進んでいない」という状況を見逃すことになるでしょう。

ギャップを監視するようにオブザーバビリティ(可観測性)を設計してください。投入された作業量と、完了した作業量を比較してください。両者が一致しなくなったときは、マシンが嘘をついていると想定してください。なぜなら、完璧な成功ログこそが、システムが完全に盲目になった唯一の兆候であることもあるからです。