◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
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 API249Batch API5SLOObservability4Operations12

✦ 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-09-02
Taking stock of Gemini standard API keys before September, across CI, servers, and my own machine
Standard API keys, including restricted ones, stop being accepted during September. Because this changes where keys come from rather than what they say, here is the three-layer inventory I ran across code, deploy settings, and my local machine, plus a dual-path client to switch behind.
◈ 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.