JavaEye

One Google Search Console error code covered both 'quota exhausted' and 'already indexed', and my stop rule believed both

This is a debugging note from November 2026, on a self-hosted Google Search Console request-indexing pipeline driven over HTTP. Console internals and undocumented error codes change without notice; treat what follows as a failure-mode checklist, not a map of Google’s current behavior.

Google Search Console’s internal batchexecute RPC answers a URL indexing request with a bare integer in a JSON array. For weeks my automation read 8 as one thing: daily quota exhausted, stop everything. On 2026-11-02 I learned it also means this page indexed instantly during the live test — no quota was spent. Both readings are correct. The stop rule was not.

The whole pipeline is three shared accounts, one HTTP client, and a text file

I run a fleet of small static sites (sixteen at the moment) and submit new URLs to Search Console through three shared Google accounts, one window per account (12:00, 13:00, 14:00 local, staggered to avoid tripping Google’s own abuse heuristics). The submitter itself is pure HTTP: a uTLS fingerprinted client carries a forged batchexecute token plus fresh cookies, no browser involved after the mint step. Each accepted URL is appended to submitted.txt, which had grown to 376 lines by the morning of the incident.

The flow had passed final acceptance the day before: all three RPC stages returned 200, a 2,468-byte token blob was minted, and one real submission landed (submitted.txt 373 → 374 on 2026-11-01 15:34).

ssh without -n silently ate four of my five queued URLs

The 13:00 window runs its HTTP loop over ssh on a second machine. First live block claimed five URLs from the queue file. It ran one.

The loop was ssh host script.sh < urls.txt — without -n, ssh quietly consumed the queue file as its own stdin. The script read four more lines, got EOF, and exited successfully. Four URLs were silently dropped: no error, no retry, no log line, and the queue believed they were done. A rehearsal run with a single-URL queue had not caught it, because eating one line of one line is invisible.

I recovered all four from the raw urls.txt pull and re-queued them, patched the script in place (every remote call now carries -n), and pushed the fix to both ends of the tunnel. Embarrassing, but mechanical. The second mine was not.

Error code 8 covers both quota exhaustion and instant indexing, and my stop rule believed both

After the fifth successful submission of the day, the next call returned the Fj3Owf payload with [8] in the result slot. Then another. Seven of them.

The old rule fired on the first one: error 8 = quota hit → halt the window. Which it did — and the 14:00 window, running a different Google account on a different machine, hit the exact same [8] on the exact same URL (tallypunch /8am-to-5pm/) and halted too. Two accounts, two machines, same error on the same page. That coincidence is what made me stop trusting the rule.

I had to decide which failure the code meant, per URL, with only production to test against. The evidence looked like this:

ObservationIf code 8 = quotaIf code 8 = page-level instant index
Same URL errors on two different accountsImplausible coincidenceExpected — the page state is per-URL
Same account: adjacent URLs succeed, this one errorsContradictionExpected
Token mint step shows no quota-exhausted wordingSuggests not quotaConsistent
Check-chain fields identical between success and error responsesUnexplainedConsistent

Four for four pointed the same way. The [8] was a property of the page, not the account: apparently when the live test indexes the URL on the spot, the RPC still reports the quota-style code. There was even a precedent sitting in the runbook — a vinunwrap URL that had indexed in seconds under the real UI months earlier, and later returned [8] to the automation.

Per-URL strike counts, not a global stop rule, now disambiguate the code

Keeping a stop rule that means two things is worse than no rule, because it trains you to ignore it. The replacement, now baked into the window script and the runbook:

  1. Count [8] per URL across all channels — two strikes on the same URL and it moves to an excluded ledger (eight-counts.txt), permanently out of the queue. Two accounts agreeing on one page is signal; one account disagreeing with itself is noise.
  2. Only three consecutive [8]s on distinct URLs halt a window. That is the quota signature.
  3. A halted window retries at its next scheduled slot, with the exclusion list loaded. No manual re-queueing.

Under the new rules, the day’s tally was: 5 URLs submitted (submitted.txt 376 → 381, ledger reconciled), 2 URLs excluded as page-level mines, zero quota consumed by errors.

One full-width comma crashed a failure path that had never executed

While dissecting the acceptance run from the day before, I found the failure path of the submitter script had never once executed in testing — and its first real execution crashed. The notification line contained $REPORT, — a full-width comma glued to the variable name. In the cron shell’s C locale, bash 3.2 parsed the comma’s first byte into the variable name and set -u killed the script with REPORT�: unbound variable. The verdict file was written; the cleanup never ran. A second, dormant instance ($DONE)) waited on the same branch. Both are now ${REPORT} and ${DONE}.

The lesson generalizes: any $VAR immediately followed by a non-ASCII character in a shell script is a live mine under set -u, and failure paths that have never fired have never been tested. I now grep for the pattern in every script that reports to me, including the one that caught this.

Cross-check every undocumented integer against a signal from a different layer

If you are driving Search Console from automation: never let an undocumented integer be your only signal. Cross-check it against account identity, URL-level state, and the token-mint step before letting it stop a pipeline; log the full response body, because the day you need it is the day it is gone; and rehearse with a queue of one only if you enjoy finding stdin-eating bugs in production. My earlier notes on the quota itself and on detectors matching their own targets both trace back to the same root cause: a cheap signal believed too early.

The next window runs 2026-11-03 13:00, exclusions loaded. If the 14:00 account submits one of today’s [8] URLs without complaint, that will be the page-level theory’s final witness — and I will update this post with the result.