Retry wrapper for transient failures
The CLI already retries HTTP 429, 502, 503, and 504 responses up to 3 times with exponential backoff — see Built-in Resilience. For nearly every workflow that’s enough.
This recipe is for the cases where it isn’t:
- A long-running batch job that hits the rate-limit envelope and needs more than 3 retries.
- Network flakes where DNS or TLS itself misbehaves and never reaches the CLI’s retry layer.
- A dependent system (CI runner, proxy) that occasionally rejects requests.
What you’ll see when retry kicks in
Section titled “What you’ll see when retry kicks in”Each built-in retry announces itself on stderr — ⚠ Rate limited, retrying in 1.0s (attempt 1/3)... — unless you passed --json, which suppresses the chatter so it can’t pollute a structured pipeline. After the third and final failure, the CLI prints Bitbucket’s own message to stderr, prefixed with a red ✗ (or ERR under --no-unicode). For a rate limit that is typically:
✗ Rate limit for this resource has been exceededThe HTTP status number does not appear in text mode. To branch on it, pass --json: the error envelope goes to stderr as a single compact line.
{"name":"APIError","code":2004,"message":"Rate limit for this resource has been exceeded","context":{"status":429},"statusCode":429,"response":{"type":"error","error":{"message":"Rate limit for this resource has been exceeded"}}}The envelope is flat — there is no error wrapper object — and code is a number from the Error Codes reference, here 2004 (API_RATE_LIMITED). statusCode and response appear on API errors only; a validation or config error has neither, and context on an API error also carries method and url (trimmed above). A hint string is added for 401, 403 and 404 responses; a 429 gets no hint.
If the operation is idempotent, retrying is safe.
Recipe: bb-with-retry wrapper
Section titled “Recipe: bb-with-retry wrapper”#!/bin/bash# bb-retry.sh - Run a bb command with exponential backoff.## Usage:# ./bb-retry.sh pr list -w myworkspace -r myrepo --json## Env vars:# MAX_ATTEMPTS default 5# INITIAL_DELAY default 2 (seconds)# MAX_DELAY default 60 (seconds)
set -euo pipefail
MAX_ATTEMPTS="${MAX_ATTEMPTS:-5}"INITIAL_DELAY="${INITIAL_DELAY:-2}"MAX_DELAY="${MAX_DELAY:-60}"
attempt=1delay="$INITIAL_DELAY"
while true; do # `bb "$@" && exit 0`, not `if bb "$@"; then exit 0; fi` — after a # non-taken `if` branch, $? is the status of the `if` itself (always 0), # so the wrapper would report "exit 0" and exit 0 on total failure. bb "$@" && exit 0 exit_code=$?
if [ "$attempt" -ge "$MAX_ATTEMPTS" ]; then echo "bb $* failed after $attempt attempts (exit $exit_code)" >&2 exit "$exit_code" fi
# Add jitter: ±25% of the current delay jitter=$(( RANDOM % (delay / 2 + 1) - delay / 4 )) sleep_time=$(( delay + jitter )) [ "$sleep_time" -lt 1 ] && sleep_time=1
echo "bb $* failed (exit $exit_code); retry $((attempt + 1))/$MAX_ATTEMPTS in ${sleep_time}s" >&2 sleep "$sleep_time"
attempt=$(( attempt + 1 )) delay=$(( delay * 2 )) [ "$delay" -gt "$MAX_DELAY" ] && delay="$MAX_DELAY"donechmod +x bb-retry.sh
# Read-only: safe to retry freely../bb-retry.sh pr list -w myworkspace -r myrepo --json > prs.json
# Tune the cap for big jobs.MAX_ATTEMPTS=10 INITIAL_DELAY=5 ./bb-retry.sh pr view 42 --jsonRecipe: retry only on a specific error code
Section titled “Recipe: retry only on a specific error code”To retry rate limits but not authentication failures, run the command with --json and read statusCode off the error envelope. Do not grep the text-mode message for 429: that message is Bitbucket’s own prose and usually contains no digits at all, while any PR title containing “502” would match.
#!/bin/bash## Usage:# ./bb-retry-on-rate-limit.sh pr list -w myworkspace -r myrepo## Appends --json so the error envelope is machine-readable.set -euo pipefail
MAX_ATTEMPTS=5attempt=1delay=2
while true; do err_file=$(mktemp) bb "$@" --json 2> "$err_file" && { rm -f "$err_file"; exit 0; }
err_msg=$(cat "$err_file") rm -f "$err_file"
# statusCode exists on API errors only; empty for validation/config errors. status=$(printf '%s' "$err_msg" | jq -r '.statusCode // empty' 2>/dev/null || true)
# Retry only on rate-limit / transient gateway errors. case "$status" in 429|502|503|504) if [ "$attempt" -ge "$MAX_ATTEMPTS" ]; then echo "$err_msg" >&2 exit 1 fi echo "HTTP $status, retrying in ${delay}s..." >&2 sleep "$delay" delay=$(( delay * 2 )) attempt=$(( attempt + 1 )) ;; *) # Non-transient (or no statusCode at all): fail fast. echo "$err_msg" >&2 exit 1 ;; esacdoneWhen not to write your own retry
Section titled “When not to write your own retry”The wrappers above are the right answer when the built-in retry has been exhausted. They are not the right answer for:
- Auth failures —
Authentication required. Run 'bb auth login' or set BB_USERNAME and BB_API_TOKEN.(code: 1001). Retrying won’t help; fix the credentials. - 404 / not-found errors (
code: 2002) — these don’t get more found over time. - Validation errors (
code: 5001or5002, e.g.Option --title is required) — bugs in your script, not transient.
The CLI exits 1 for all of these, same as for transient errors. Branch on the JSON envelope instead. Use jq -r '.statusCode // .code': statusCode is present on API errors, and code covers the rest.
Related
Section titled “Related”- Scripting & Automation: Built-in Resilience
- Error Codes reference — the numeric
codevalues to branch on - JSON Output reference — the success/error envelope shapes