GEMINI LABJP
SUNSET — Three days until the image models shut down: Imagen 4 and the Gemini 3 Image family stop on August 17MIGRATE — gemini-3.1-flash-image is the recommended target; calls to generate_images() fail hard after the cutoff, so there is no grace periodFLASH — Gemini 3.6 Flash is generally available, with better token efficiency and stronger code and agentic planning at a lower price than 3.5 FlashLITE — Gemini 3.5 Flash-Lite also reached GA as a low-latency, cost-conscious subagent option aimed at high-volume automationPARAMS — The sampling parameters temperature, top_p, and top_k are now deprecated, so existing calls are worth revisitingROBOTICS — gemini-robotics-er-1.6-preview shuts down on August 31, so this is the window to plan the migrationSUNSET — Three days until the image models shut down: Imagen 4 and the Gemini 3 Image family stop on August 17MIGRATE — gemini-3.1-flash-image is the recommended target; calls to generate_images() fail hard after the cutoff, so there is no grace periodFLASH — Gemini 3.6 Flash is generally available, with better token efficiency and stronger code and agentic planning at a lower price than 3.5 FlashLITE — Gemini 3.5 Flash-Lite also reached GA as a low-latency, cost-conscious subagent option aimed at high-volume automationPARAMS — The sampling parameters temperature, top_p, and top_k are now deprecated, so existing calls are worth revisitingROBOTICS — gemini-robotics-er-1.6-preview shuts down on August 31, so this is the window to plan the migration
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 API209Batch API5SLO2Observability4Operations10

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-08-04
Retrying a Retired Model Never Showed Up in Success Latency
With the image generation models shutting down soon, I rebuilt a mock server to see what my retry layer actually does when it hits a retired model ID. The damage landed in wall time and queue wait, and never touched the metric I was watching.
API / SDK2026-07-29
A 17% Drop in Output Tokens Sounded Big. Then I Decomposed the Bill
Output pricing fell 16.67% and output tokens fell about 17%. Adding those numbers does not give you the savings. Here is an exact price/volume/cross-term decomposition, plus a loop measurement where the savings rate went down instead of up.
📚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 →