Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Respect API Rate Limits from a Hosted Mac Agent

If a polling agent keeps hitting 429 responses, the fix is not a slower manual sleep. It is reading the Retry-After header the server already sent and sizing your launchd interval against the published rate window. This page gives you a complete agent you can run on a hosted Mac, plus a worksheet to pick the interval.

Pick the Figment plan that fits a polling agent

For pure API polling with curl, requests, or a Node client, the $29/month virtual Intel macOS environment (2 vCPU, 8 GiB RAM, 80 GiB disk) is the right starting point. It runs a real macOS userland on Linux/KVM, so launchd, security, and Homebrew-installed clients all work. There is no GPU and no audio, which a network-bound polling agent does not need. Access is through the browser console; commands run in the guest's desktop terminal.

Upgrade when the workload needs more than network I/O:

  • An Apple silicon runtime, Xcode 27, or visionOS tooling → M-series from $95/month (request-based; the exact chip, RAM and disk are confirmed on the quote).
  • A physical Intel host for kernel extensions or hardware-sensitive tests → i3 Mac $49/month (request-based while supply is confirmed).
  • A production agent you want someone else to watch → Managed Build Mac $249/month, which includes one M-series Mac, CI setup, backups, monitoring, and 30 minutes of monthly support.

The $29 price is a monthly charge; it is not pro-rated to shorter windows, so a two-week project still pays for the month. For the physical tiers, ask us to confirm current availability in your quote rather than assuming instant provisioning.

Inputs and permissions to pin down first

Document the agent's contract before writing poll code. Missing inputs fail silently inside a launchd job.

InputWhere it lives
API base URLEnvironmentVariables in the launchd plist
Auth tokenmacOS Keychain via security add-generic-password
Poll intervalStartInterval (seconds)
Output pathLocal path or mounted volume
Rate-limit budgetFrom your API provider's docs

Keep the token out of the plist and out of shell history. Add it once from the browser console terminal, then read it at runtime:

TOKEN="$(security find-generic-password -a poll-agent -s api-token -w)"

If the API uses OAuth, run the one-time browser authorization from the hosted Mac, write the refresh token to Keychain, and let the agent refresh headlessly from then on.

A Retry-After-aware polling loop

RFC 6585 defines 429 Too Many Requests and notes the response MAY include a Retry-After header. Retry-After can be an integer number of seconds or an HTTP-date (see RFC 7231 §7.1.3). The script below parses the integer form and falls back to exponential backoff when the header is missing or date-formatted; extend it with date -j -f if your provider uses HTTP-dates.

#!/usr/bin/env bash
set -euo pipefail

API_URL="${API_BASE_URL:?API_BASE_URL not set}"
TOKEN="$(security find-generic-password -a poll-agent -s api-token -w)"
LOG="$HOME/Library/Logs/poll-agent.log"
BODY="$(mktemp -t poll-body)"
HEADERS="$(mktemp -t poll-headers)"
trap 'rm -f "$BODY" "$HEADERS"' EXIT

MAX_RETRIES=5
BACKOFF=10
attempt=0

while (( attempt < MAX_RETRIES )); do
  HTTP_CODE=$(curl -sS \
    -o "$BODY" -D "$HEADERS" -w "%{http_code}" \
    -H "Authorization: Bearer $TOKEN" \
    "$API_URL")

  case "$HTTP_CODE" in
    200)
      printf '%s OK\n' "$(date -u +%FT%TZ)" >> "$LOG"
      # Hand "$BODY" to your own processor here.
      exit 0
      ;;
    429)
      RA=$(awk 'BEGIN{IGNORECASE=1} /^Retry-After:/ {gsub(/\r/,""); print $2; exit}' "$HEADERS")
      if [[ "$RA" =~ ^[0-9]+$ ]]; then
        printf '%s 429 Retry-After=%ss\n' "$(date -u +%FT%TZ)" "$RA" >> "$LOG"
        sleep "$RA"
      else
        printf '%s 429 backoff=%ss\n' "$(date -u +%FT%TZ)" "$BACKOFF" >> "$LOG"
        sleep "$BACKOFF"
        BACKOFF=$(( BACKOFF * 2 ))
      fi
      ;;
    *)
      printf '%s HTTP %s abort\n' "$(date -u +%FT%TZ)" "$HTTP_CODE" >> "$LOG"
      exit 1
      ;;
  esac
  attempt=$(( attempt + 1 ))
done

printf '%s max retries exceeded\n' "$(date -u +%FT%TZ)" >> "$LOG"
exit 1

This is an unexecuted example, not a benchmark. Save it as ~/bin/poll-agent.sh, chmod +x, then test it once by hand against your real endpoint before letting launchd run it.

Poll scheduling worksheet

Fill the rightmost column, then copy it into the plist's StartInterval. The rule is interval >= ceil(window / limit) * 1.1 so clock drift does not push you over.

API documented limitWindowSafe StartInterval (s)Retry-After behaviour
60 req/min60 s66Use header; fall back to 60 s
1,000 req/day86,400 s95Use header; fall back to 300 s
100 req/hour3,600 s40Use header; fall back to 120 s
5 req/sec burst1 s5Header is mandatory
Undocumented—300Header is mandatory

At StartInterval=66 the agent makes about 1,309 calls per day, inside a 60/min quota with headroom for the occasional retry.

Install the launchd agent

Save this to ~/Library/LaunchAgents/com.example.poll-agent.plist. Key semantics are defined in Apple's Creating launchd Jobs reference.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>com.example.poll-agent</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>/Users/agent/bin/poll-agent.sh</string>
  </array>
  <key>StartInterval</key><integer>66</integer>
  <key>StandardOutPath</key><string>/Users/agent/Library/Logs/poll-agent.out</string>
  <key>StandardErrorPath</key><string>/Users/agent/Library/Logs/poll-agent.err</string>
  <key>EnvironmentVariables</key>
  <dict>
    <key>API_BASE_URL</key><string>https://api.example.com/v1/items</string>
  </dict>
</dict>
</plist>

Load with launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.example.poll-agent.plist. Omit KeepAlive: the job should exit each run and let launchd relaunch on the interval, so a bad call never trips the "respawning too fast" throttle.

Failure and recovery checks

Review after the first 24 hours:

  • Log pattern. tail -50 ~/Library/Logs/poll-agent.log should be dominated by OK. More than 10% 429 rows means the window is too tight.
  • Auth expiry. A run of HTTP 401 means refresh failed. Re-run the Keychain read by hand to isolate token versus network.
  • Missed runs after sleep. launchd fires one catch-up invocation after wake, not a queue. For strict schedules keep the Mac awake via pmset or your hosting console.
  • Log growth. poll-agent.log is append-only. Add a newsyslog.d rule or truncate in-script above a size threshold.
  • 5xx responses. These are server-side; use a flat 30-second retry rather than burning rate-limit budget.

Start on Figment

Open Mac Hosting, pick the $29 virtual Intel macOS plan for a network-only agent or request a quote for i3 or M-series if you need a physical host. Reach the guest through the browser console, create ~/bin, save the script, store the token in Keychain, drop the plist in ~/Library/LaunchAgents, and bootstrap it.

Migrating from another host? Copy the script and plist files, re-add secrets directly on the new machine (do not copy a Keychain database between hosts), bootstrap the agent, and confirm one clean OK in the log before you decommission the old runner.

Start your Mac →

Related guides


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms