GEMINI LABJP
FLASH35 — Gemini 3.5 Flash is now GA and powers gemini-flash-latest, making everyday generation faster and more affordableAGENTS — Managed Agents launch in public preview in the Gemini API, running in secure, isolated Google-hosted Linux sandboxesMEDIA — Nano Banana 2 Lite and Gemini Omni Flash bring faster image and high-quality video generation across AI Studio and the APITTS — Streaming speech generation is now supported for gemini-3.1-flash-tts-preview via streamGenerateContentTRANSLATE — A new audio model detects 70+ languages for live speech-to-speech translation while preserving natural intonationSPENDCAP — Project-level spend caps for billing have been added in Google AI Studio to keep costs under controlFLASH35 — Gemini 3.5 Flash is now GA and powers gemini-flash-latest, making everyday generation faster and more affordableAGENTS — Managed Agents launch in public preview in the Gemini API, running in secure, isolated Google-hosted Linux sandboxesMEDIA — Nano Banana 2 Lite and Gemini Omni Flash bring faster image and high-quality video generation across AI Studio and the APITTS — Streaming speech generation is now supported for gemini-3.1-flash-tts-preview via streamGenerateContentTRANSLATE — A new audio model detects 70+ languages for live speech-to-speech translation while preserving natural intonationSPENDCAP — Project-level spend caps for billing have been added in Google AI Studio to keep costs under control
Articles/API / SDK
API / SDK/2026-07-14Advanced

When a Batch Job Sat in RUNNING for Half a Day: Field Notes on Catching Stalls Early with Per-State Dwell Budgets and Record Reconciliation

When a Gemini Batch job stalls quietly under the shadow of the 24-hour SLA, per-state dwell-time budgets and submitted-vs-completed record reconciliation let you name the stall early. Field notes with real operational numbers.

Gemini API183Batch API5SLO2Observability2Operations7

Premium Article

I opened the nightly batch dashboard first thing in the morning and the job was frozen in RUNNING. Submitted at 22:00 the night before. Eleven hours had passed. But the Batch API SLA promises completion "within 24 hours," and our monitoring was wired to that 24-hour threshold, so no alert had fired.

Which meant thirteen more hours of not knowing whether the job was alive or dead. Delivery was at noon. It was not going to make it.

The lesson that day was blunt: monitoring keyed to the SLA only tells you about a stall after it's too late to act. What I needed was a way to name an unusual dwell against the usual baseline, early. This note records the approach I landed on for the indie developer pipeline I run — measuring how long jobs sit in each state, drawing a budget from that, and pulling out the jobs that drift past it — along with the numbers from actually running it.

Why an SLA threshold is always too late

The 24-hour SLA is an upper bound: "we will finish within this." In practice, a healthy job finishes in minutes to tens of minutes. That gap is the trap.

If your population of healthy jobs finishes in 20 minutes and you set the threshold at 24 hours, you can only detect a stalled job "after it runs 70x slower than normal." A monitor that stays silent at 90 minutes — or three hours — when the baseline is 20 minutes is not really monitoring anything.

So reframe it. The SLA is a contract with the business, not an anomaly threshold. The anomaly threshold should come from your own measured distribution: how long this job normally sits in each state.

Record dwell time on every state transition

First, record how long a job stays in each state. Time spent in QUEUED, time spent in RUNNING — write it to a ledger on each transition. It piggybacks on polling you're already doing, so the added cost is negligible.

# What this records:
# On each poll, observe the state; the moment it changes,
# append "how many seconds it stayed in the previous state" to a ledger.
import time, json
from google import genai
 
client = genai.Client(api_key="YOUR_API_KEY")
 
TERMINAL = {"BATCH_STATE_SUCCEEDED", "BATCH_STATE_FAILED", "BATCH_STATE_CANCELLED"}
 
def track_dwell(name: str, ledger_path: str, poll: int = 30):
    prev_state = None
    entered_at = time.time()
    while True:
        job = client.batches.get(name=name)
        state = job.state.name
        now = time.time()
        if prev_state is None:
            prev_state, entered_at = state, now
        elif state != prev_state:
            record = {
                "name": name,
                "from_state": prev_state,
                "dwell_sec": round(now - entered_at, 1),
                "to_state": state,
                "ts": now,
            }
            with open(ledger_path, "a", encoding="utf-8") as f:
                f.write(json.dumps(record) + "\n")
            prev_state, entered_at = state, now
        if state in TERMINAL:
            return
        time.sleep(poll)

One thing that matters here: keep state-transition logs out of any sampling. They're low-volume, so if they get swept into generic sampling, the exact line you wanted drops and you're left with "no record." Put Batch logs in an unconditionally-retained category.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Why a 24-hour SLA threshold detects stalls far too late, and how per-state p95 dwell budgets surface them early
Code that reconciles submitted vs completed record counts to count rows that vanished silently
Measured lead-time improvement after moving from SLA thresholds to budget-based detection
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

API / SDK2026-06-12
Retiring the Midnight Polling Loop — Rebuilding My Gemini Batch Monitoring Around Webhooks
A working log of migrating Gemini Batch API completion monitoring from 60-second polling to event-driven webhooks: static vs dynamic, signature verification, and real numbers.
API / SDK2026-07-01
Keeping Unattended Jobs From Failing Silently: A Preflight Gate for Gemini's Platform Changes
Unrestricted API keys are now rejected, the old CLI reached end of life, and the Interactions API is becoming the default entry point. These 2026 platform shifts stop working automation without raising an error. Here is a preflight gate, with runnable code, that catches the failure before the batch runs.
API / SDK2026-06-28
The Morning a Managed Agent Stalled and Left No Trace — Building a Run-Observability Layer Outside the Sandbox
With Gemini Managed Agents, the sandbox lives on Google's side, so when a run stalls there is nothing left in your own logging stack. This is a working TypeScript design for an outside observability layer that taps stream events into a ledger, detects silent stalls, and folds runs into readable postmortems.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →