# 4 Days, 3 Wasted Calls Per Run: My Retry Loop Mistook a Quota-Limit Message for a 'Too Short' Article

> Source: <https://dev.to/bokuwalily/4-days-3-wasted-calls-per-run-my-retry-loop-mistook-a-quota-limit-message-for-a-too-short-4l23>
> Published: 2026-09-21 05:00:05+00:00

Last time I wrote about [using a probe to decide which jobs to stop first when the quota runs dry](https://zenn.dev/bokuwalily/articles/quota-circuit-priority-probe). This post is about the problem sitting right next to it: my script *could* tell when the quota was exhausted, but it treated the limit message as if it were real article body text, then kept retrying a failure that no rewrite could ever fix. From September 13 to 16, every run burned 3 `claude -p` calls to produce nothing. After the fix, it burns 1 and bails out.

`note2-daily-stock.sh` auto-generates product review articles for note2 (bokumolily) using `claude -p`. For each product it calls `claude -p` up to 3 passes to produce the body. Each pass is judged by a quality check (`article_quality_ok`) with criteria like "does the character count exceed MIN_CHARS (7,000)?" and "are there at least 2 affiliate links?"

For 4 days, 9/13 through 9/16, this script failed 3 times in a row with the exact same symptom. The cause: when `claude -p` hits the Max plan's limit, it returns a **one-line limit-reached message** (no `TITLE:` line), and the script treated that as "an ordinary generation failure where the body is too short." The comment in the actual code records exactly what happened:

```
  # 2026-09-16: claude -pがクォータ上限メッセージ(1行・TITLE:行なし)を返すと
  # 本文が空のまま「文字数不足」として3回とも空振りし、上限中に3回分のclaude呼び出しを浪費していた
  # (note2-daily 9/13-9/16 4日連続で同一症状を実測)。上限メッセージは即検出して1回で中断する。
```

(note2-daily-stock.sh:565-567)

While the limit is in effect, no amount of rewriting will produce a body. Yet `article_quality_ok` only ever said "quality NG reason: not enough characters," so the script concluded "maybe one more pass will fix it" and threw away 2 more pointless `claude -p` calls during the limit window.

**Note:** A retry loop needs more than the monolithic fact that "it failed." **If you don't distinguish "a failure a rewrite can fix" from "a failure a rewrite cannot fix," you'll keep walking into the same wall.**

The fix is to grep for the limit-message pattern immediately after each pass's generation, and if it matches, abort right there instead of spending the remaining passes.

```
  for pass in 1 2 3; do
    RAW=$(generate_article_raw "$PRODUCT_JSON" "$IMG_COUNT")
    if printf '%s' "$RAW" | grep -qiE "hit your (weekly |5-hour |usage |session )?limit|usage limit reached|rate limit|resets? (at |in |on )?[0-9]{1,2}(:[0-9]{2})? *[ap]m"; then
      QUOTA_HIT=1
      log "ABORT: quota-message検出のため中断 (pass $pass) $SLUG raw先頭=[$(printf '%s' "$RAW" | tr '\n' ' ' | cut -c1-160)]"
      break
    fi
    write_article_file "$RAW" "$OUT"
    ...
  done
```

(note2-daily-stock.sh:563-572, excerpt)

When `QUOTA_HIT` is set, the script deletes the half-written output file and any images downloaded along the way, notifies Discord, and exits immediately. Instead of 3 calls, it consumes exactly 1 at that point and retreats.

```
  if [ "$QUOTA_HIT" -eq 1 ]; then
    [ -e "$OUT" ] && rm -f "$OUT"
    rm -f "$ROOT/${IMG_BASENAME}-p"*.jpg
    discord_notify "⚠️ note2-daily: claudeクォータ上限のため中断（3回消費せず中止）: $ITEM_NAME"
    exit 1
  fi
```

(note2-daily-stock.sh:590-595)

"Not enough characters" and "limit reached" look alike on the surface (the body is nearly empty), but the correct response is completely different. The former might be fixed by a rewrite; the latter won't be fixed no matter how many times you retry until the reset time. Separating those two cases up front with a single grep is the entire fix.

There are 3 other scripts that share the same shape as note2-daily-stock.sh — "have `claude -p` write the body, retry if the quality check fails": `article-daily-stock.sh` (the script that writes this very Zenn article series), `maker-daily-stock.sh`, and `series-daily-stock.sh`. I grepped all 3, and none of them contained the limit-message detection pattern.

``` bash
$ grep -n "hit your\|usage limit\|QUOTA_HIT\|rate limit" \
    article-daily-stock.sh maker-daily-stock.sh series-daily-stock.sh
(該当なし)
```

In particular, `run_pass()` in `series-daily-stock.sh` has almost exactly the same structure that note2 tripped over.

``` php
run_pass(){ # $1=prompt $2=outfile $3=label
  local prompt="$1" outfile="$2" label="$3" attempt wait
  for attempt in 1 2 3; do
    rm -f "$outfile"
    timeout 600 "$CLAUDE" ... -p "$prompt" ... > "$outfile" ...
    if [ -s "$outfile" ]; then
      ...
    fi
    log "WARN: pass $label 空出力(attempt $attempt/3)"
    if [ "$attempt" -lt 3 ]; then
      wait=$((attempt * 30))
      sleep "$wait"
    fi
  done
```

(series-daily-stock.sh:254-279, excerpt)

Waiting 30 seconds, then 60 seconds, on "empty output (attempt N/3)" and **hammering the same call 3 times for the same reason** is essentially the same structure note2 hit on 9/13–9/16. If this script runs during a limit window, it should waste calls in exactly the same way — and it hasn't been fixed yet.

`maker-daily-stock.sh` only has a single generation pass, so it doesn't fail 3 in a row, but the same dates show up in its comments too.

```
  # 失敗時もキューを1件進める。進めないと同じ題材が先頭に居座り続け、翌日以降も
  # 同じ理由で落ち続ける（2026-09-13〜16 に同一題材が7回連続で失敗した実測）。
```

(maker-daily-stock.sh:412-413)

In other words, the Max plan limit on 9/13–9/16 didn't just hit note2 — it dragged the maker pipeline down with it. On the maker side, the underlying problem of "misdiagnosing the limit message as an incomplete body" is still there, exactly as in note2; it's being held together by a different band-aid: "even on failure, at least advance the topic queue." **The same root cause has a different emergency patch in each script, applied whenever someone happened to notice.**

`claude -p` calls during the limit window`maker-daily-stock.sh` also failed 7 times in a row on the same topic — the limit hit multiple pipelines simultaneously. But maker's fix is only "advance the queue," and the underlying misdiagnosis remains unfixed there as well as in `article-daily-stock.sh` and `series-daily-stock.sh`
Next time I plan to write about whether that propagation itself can be automated — a mechanism to mechanically carry a fix found in one script over to its "structurally similar siblings."

How do your retry loops tell a fixable failure from one that no retry will ever fix?

*Written by **Lily** — I ship iOS apps and automate my content stack with Claude Code.

Follow along: [Portfolio](https://bokuwalily.com) · [X](https://x.com/bokuwalily) · [GitHub](https://github.com/bokuwalily)*
