# Streamlining AI Agent Deployments: From Frequent Restarts to Efficiency

Author: Maksym Tytarenko | Date: 2026-08-18 | Category: AI & ML | Tags: ai agents, deployment, efficiency, automation, docker, gcp, code review, merging
Canonical: https://www.tytarenkoagency.com/blog/streamlining-ai-agent-deployments-efficiency

> My agent fleet logged 10 deploys and 12 restarts in one day, and restarts kept killing in-flight reviews. A drain flag, a quiet probe, and batched merges cut that to 2 or 3 restarts a day without touching the agent loop.

## Executive Summary

I built an autonomous AI agent fleet, and one of the most expensive operational mistakes was also the most ordinary: too many restarts. My daemon on a GCP VM polls a GitHub Projects board and runs each task as headless Claude Code inside a Docker container, with Claude as the executor and first-line reviewer, an optional OpenAI GPT model via the Codex CLI as the final adversarial reviewer, and my own Telegram button as the merge gate. In that setup, 10 deploys and 12 restarts in a single day did not just create noise. They interrupted work mid-flight and destroyed review passes that were already paid for in time and model calls.

![An infographic comparing operational efficiency before and after implementing a drain-first restart strategy, highlighting costs associated with frequent restarts.](https://media.tytarenkoagency.com/ai-generated/uploads/20260818_154821_3a4e6e87.png)


The fix required no new model and no bigger machine, only a deployment-process change: drain the daemon before restart, and batch merges so a single restart covers several completed changes. After the change, the fleet went from 12 restarts a day to 2 or 3, and in-flight reviews stopped disappearing. The core agent loop was not touched at all.

This matters for CTOs, founders, and senior engineers because AI agent systems are stateful even when they look “stateless” from the outside. Once an agent reads a task board, writes code, runs a reviewer loop, and waits on human approval, a restart is no longer a routine deploy event. It is a workflow interruption and a token sink.

## Why Restarts Are Expensive in an Agent Fleet

Traditional service deployments assume a request is short-lived and replayable. An autonomous coding agent is different. It may spend many minutes planning, editing, reviewing, retrying, and preparing a pull request. Restart the process in the middle and the system abandons work that was already paid for in attention and tokens, because the expensive part of code generation is the reasoning and revision loop, not the final file write.

The daemon holds at least three kinds of live state: the current task pulled from the board, the current execution or review phase, and the pending handoff to the next gate, such as human merge approval. A concrete failure mode looks like this. The executor produces a patch, the reviewer starts a fresh skeptical pass, and the daemon restarts before the reviewer finishes. The next poll cycle sees the same board item again, but the review state is gone. The system has not merely delayed delivery; it has invalidated partially completed work.

My day of 10 deploys and 12 restarts was a signal that deploys and restarts were being treated as separate mechanical events instead of one control surface. Some restarts were deploys landing, others were manual pokes at a daemon that looked stuck while it was actually working. Each one could cut off an executor, reset a reviewer context, or interrupt a merge that was already waiting for my approval. The architectural implication is simple: when the fleet has in-flight reviews or merges, the restart boundary must move from “any time” to “safe checkpoint only.”

![A diagram illustrating the architecture of an autonomous coding agent system, detailing the workflow and states involved in task execution.](https://media.tytarenkoagency.com/ai-generated/uploads/20260818_154700_e3242ea8.png)


## The Pipeline, End to End

The workflow is short, but the ordering matters:

- A daemon on a GCP VM polls a GitHub Projects board
- It launches each task as headless Claude Code inside a Docker container
- The Claude executor writes the code
- A fresh Claude reviewer session reviews it and can trigger automatic revise cycles
- An optional GPT model, run through the Codex CLI as `gpt-5.3-codex`, tries to refute the pull request
- I merge via Telegram buttons

Each stage has a different failure mode. The executor is optimized for progress. The reviewer is optimized for skepticism. The final adversarial reviewer is optimized for breaking the solution before I approve it, and it never writes code in this system; refutation is its entire job. If the daemon restarts between those stages, it erases exactly the state that gives the next stage meaning. A deployment process for this kind of system must preserve stage continuity, not just process uptime.

## Drain Before Restart: How It Actually Works

The whole mechanism fits in a flag file and a bounded wait.

When a deploy starts, the workflow writes a drain flag onto the VM before doing anything else. While that flag is fresh, the daemon refuses to claim new tasks from the board, and the Telegram bot refuses button actions that would start new work. Whatever is already running keeps running. Stopping intake and stopping the process are two different actions, and conflating them was the original mistake.

The deploy then waits for the fleet to go quiet. Quiet has a precise, observable definition: no per-task containers are running, and no fresh busy lock is held by an in-flight handler. Both signals live on disk or in the container runtime, never in anyone's process memory. The busy lock is a plain file whose freshness stands for liveness: a handler that dies leaves a lock that goes stale within a couple of minutes and gets reclaimed, so a crash cannot wedge the deploy pipeline. Draining never terminates a running container either; it only refuses to start new ones and lets the existing ones run to their natural exit.

The wait polls every few seconds and is bounded by a ceiling derived from the longest task the fleet runs, roughly half an hour plus headroom. When the fleet probes quiet, the daemon is restarted and the flag is cleared by the same deploy run that wrote it. If the ceiling expires first, the deploy proceeds anyway and says so loudly in the logs, because a crashed lock holder must never block deploys forever.

There are two clocks here, and they answer different questions. The quiet-wait ceiling bounds how long a single deploy will wait for the fleet, so it is sized to the longest normal task. The flag TTL bounds how long the daemon will honor a drain nobody cleaned up, so it is sized to the whole worst-case deploy including its preparation steps, which lands a little over an hour. The TTL must outlive any healthy deploy that wrote the flag, and nothing more; past it, a leftover flag is ignored and reported as an error rather than obeyed.

Tasks paused on my approval interact with draining gracefully. A task waiting for a Telegram button press holds no container and no busy lock, so it does not delay quiet; its pending state lives on disk and simply survives the restart. And because the bot declines button actions while the flag is fresh, an approval cannot launch a merge into a daemon that is about to go down.

Two design rules carry the safety of the whole scheme:

- An unreadable state is busy, never quiet. If the container probe or the lock file cannot be read, the deploy treats the fleet as busy and keeps waiting. A probe whose blind state means “go ahead” is not a guard.
- Every wait is bounded. The drain flag carries a TTL, so a deploy that dies without cleaning up cannot leave the fleet drained forever; a stale flag is ignored and logged as an error. Fail-closed behavior is only safe to adopt because every fail-closed answer has a deadline.

Notice what is absent: nothing here checkpoints model context across a restart. Draining works precisely because it avoids the need for persistence. The process is only replaced at a boundary where nothing valuable lives in memory.

## Batching Merges Into One Controlled Restart

The second change was to batch merges. Instead of restarting after every small change, I group completed work so one restart covers several merges.

The temptation is to merge each task the moment it looks ready, because each completion feels like progress. But when every merge triggers a deploy and every deploy triggers a restart, the fleet spends its time switching contexts instead of finishing queued work cleanly.

The tradeoff is latency versus stability. Batching slightly delays the moment a completed change reaches production. In exchange, restarts happen a few times a day instead of a dozen, and a nearly finished review is far less likely to be cut off by the next deploy wave. For a fleet already gated on human approval through Telegram, the added delay is negligible.

One constraint keeps batching honest: batches must not grow so large that rollback becomes ambiguous. Logs and review history need clear boundaries, so any future investigation can still map a restart to the exact set of changes it covered.

## Before and After

Before the change, the pattern was noisy: 10 deploys and 12 restarts in one day, restarts landing while reviews were in flight, and review passes silently lost. Nothing crashed, which is exactly why the cost stayed invisible for so long. The way I finally saw it was mundane: reading the daemon log for that day and counting how many tasks had started a review more than once, each duplicate meaning an earlier pass a restart had thrown away. The alert system had nothing to say about any of it, because from the outside every restart looked healthy.

After drain-first restarts and merge batching, the fleet settled at 2 or 3 restarts a day, and the repeated-review log signature disappeared with them. Reviews now finish, or reach a safe boundary, before the process carrying them is replaced.

## A Pattern You Can Copy

Here is the critical path, reduced to pseudocode that mirrors the real implementation:

```text
# deploy side, runs before the restart
write_drain_flag(run_id)          # daemon stops claiming new work
deadline = now() + MAX_WAIT       # ceiling: longest task plus headroom
while now() < deadline:
    if no_task_containers() and not busy_lock_held():
        break                     # fleet is quiet, safe to replace
    sleep(POLL_SECONDS)           # unreadable probe counts as busy
restart_daemon()
clear_drain_flag(run_id)          # only the run that wrote it may clear it

# daemon side, every poll cycle
if drain_flag_fresh():            # stale flags are ignored, loudly
    skip_claiming_new_tasks()     # in-flight work keeps running
else:
    claim_next_task_from_board()
```

The key rule the daemon must encode is that “no new tasks” and “process exit” are different states. Stopping intake is a control action. Terminating the daemon is a lifecycle action. The drain flag separates them, and the quiet probe decides when the second may safely follow the first.

## Constraints and Tradeoffs

The first constraint is deploy latency. A drain-first policy makes a deploy take as long as the current task needs to reach a boundary, up to the ceiling. If the stability of in-flight work matters more than the immediacy of deploys, that trade is correct, but the operator has to make it consciously rather than inherit it.

Second, what happens when a task never reaches a safe boundary? The bounded wait answers that: after the ceiling, the restart proceeds and the loss is logged. The task itself is not lost, only the pass in flight. Its board item is still marked in progress, so the freshly started daemon re-claims it on the next poll and the review simply begins again from the top; no human has to notice or intervene. Draining reduces interruptions; it does not promise zero. The difference from before is that a repeated pass is now a rare, explicitly logged exception instead of routine background damage.

Third, why draining rather than something fancier? I considered checkpointing agent state so a review could resume after a restart, and a queue-based design where tasks are re-claimed with their progress attached. Both fail the same test: the expensive state is the model session itself, and a half-finished reasoning pass cannot be serialized and resumed across a process boundary at any reasonable cost. The board already serves as the durable queue. Draining wins because it protects the unserializable state the cheapest way possible, by letting it finish.

Fourth, auditability. The executor and the reviewer are separate sessions, and the adversarial step is a separate vendor. Each stage needs enough traceability that a lost review can be attributed to model behavior or to infrastructure churn. Restarts should be logged with what was running at the time, and drain waits with how long they took and why they ended.

## What to Track

The useful metrics are operational rather than vanity numbers:

- Deploys per day and restarts per day, as separate counters
- Tasks interrupted mid-review, and review passes lost to a restart
- Time spent in the draining state before each restart
- How often the drain wait hits its ceiling instead of reaching quiet
- Time from task pickup to merge approval

These measures tell you whether the fleet acts like a controlled workflow system or like a brittle process that happens to call models. If the policy works, interruptions fall before any broader throughput gain shows up. That is the right order of evidence: first preserve work, then optimize speed. In my fleet, the restart count itself was the first number to move, from 12 a day down to 2 or 3.

## Actionable Takeaways

- Audit every place your agent process can be restarted and classify it as safe, unsafe, or conditional.
- Separate “stop taking work” from “stop the process”; a drain flag on disk is enough to encode the difference.
- Define quiet in observable terms, such as running task containers plus a busy lock, and treat an unreadable probe as busy.
- Bound every wait: a drain flag needs a TTL, a quiet wait needs a ceiling, and both need loud logs when they expire.
- Batch merges so one controlled restart covers several completed changes, and keep batch boundaries visible for rollback.
- Track restarts, interrupted reviews, and drain time before you optimize raw deploy speed.

## FAQ

### Why should AI agent deployments be drained before restart?

Because agent work is stateful even when it runs in containers. A single agent task in my fleet can hold up to half an hour of accumulated planning and review context, and a restart in the middle of it throws that reasoning away while the tokens it cost stay on the bill.

### What counts as a safe boundary for restarting an agent daemon?

A moment when nothing valuable lives only in process memory. In my fleet that reduces to two on-disk checks a deploy script can run: the container runtime lists no per-task containers, and the busy lock file is absent or already past its staleness window of about two minutes. If either check cannot be read, the answer is busy, not quiet.

### What happens if a task never finishes draining?

The wait is bounded at roughly 35 minutes, sized to the longest normal task plus headroom, after which the deploy restarts anyway and logs that the drain did not complete. The drain flag itself expires on a TTL a little over an hour, so even a deploy that dies mid-run cannot leave the fleet refusing work into the next day.

### What did batching merges change in practice?

Restarts dropped from 12 a day to 2 or 3, because one restart now covers several completed changes. The cost is a small delay before a finished change reaches production, which a fleet gated on human approval barely notices.

## Related reading

- [Four Merge Presses, One Merge: Fixing the Approval Layer of My AI Agent Fleet](https://www.tytarenkoagency.com/blog/four-merge-presses-one-merge-approval-layer)
- [Enhancing AI Review Processes: From Effort Allocation to Test Evidence](https://www.tytarenkoagency.com/blog/enhancing-ai-review-processes)
- [The Surface Writes the Prompt: Codex vs Claude Code vs Claude.ai, Side by Side](https://www.tytarenkoagency.com/blog/surface-dictates-prompt-codex-claude)

