
One rented Mac, one agent process, many incoming tasks: the only thing you really need to prove is that one task runs at a time and no task is silently dropped or run twice. A hosted broker (SQS, Redis, Temporal) solves this at scale, but a shell agent on a single Mac does not need that. A file-based state machine plus a directory lock gives safe serial execution with nothing to operate.
If the tasks themselves are plain shell, Python, Node, curl, image/PDF tooling or any Intel-compatible binary, the Figment virtual Intel macOS plan at $29/month is the right starting point: 2 vCPU, 8 GiB RAM, 80 GiB disk, accessed through a browser console. Move up to an M-series Mac from $95/month only when a task must execute ARM-only binaries or you need Xcode 27 or visionOS tooling, which require Apple silicon (Apple porting guide).
A polling agent that fetches "the next task" and runs it has two silent failure modes:
Both are prevented by writing task state to disk at every transition. Each task owns a directory and moves through four states:
PENDING → RUNNING → DONE
↘ FAILED
The worker only picks up PENDING. Anything found in RUNNING at startup belonged to a previous run that died; the worker demotes it to FAILED with a reason so a human or a retry policy decides what happens next.
A single mkdir is atomic on APFS, so it doubles as the worker lock. No external tools, no flock, no daemons beyond launchd.
#!/usr/bin/env bash
# queue-loop.sh — single-worker queue state machine
set -euo pipefail
QUEUE_DIR="${HOME}/agent-queue"
LOCK_DIR="${QUEUE_DIR}/.worker.lock"
LOG="${QUEUE_DIR}/worker.log"
mkdir -p "$QUEUE_DIR"
log() { echo "[$(date -u +%FT%TZ)] $*" | tee -a "$LOG"; }
# Acquire exclusive lock (mkdir is atomic). Exit quietly if already held.
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
log "Another worker is running. Exiting."
exit 0
fi
trap 'rmdir "$LOCK_DIR" 2>/dev/null || true' EXIT
# Startup reconciliation: anything still RUNNING died previously.
for task_dir in "$QUEUE_DIR"/*/; do
[[ -f "$task_dir/state" ]] || continue
if [[ "$(cat "$task_dir/state")" == "RUNNING" ]]; then
log "Reclaiming interrupted task $(basename "$task_dir")"
echo "FAILED" > "$task_dir/state"
echo "interrupted_at_startup" > "$task_dir/failure_reason"
fi
done
# Pick one PENDING task.
NEXT_TASK=""
for task_dir in "$QUEUE_DIR"/*/; do
[[ -f "$task_dir/state" ]] || continue
if [[ "$(cat "$task_dir/state")" == "PENDING" ]]; then
NEXT_TASK="$task_dir"; break
fi
done
[[ -z "$NEXT_TASK" ]] && { log "Queue empty."; exit 0; }
TASK_ID=$(basename "$NEXT_TASK")
echo "RUNNING" > "$NEXT_TASK/state"
date -u +%FT%TZ > "$NEXT_TASK/started_at"
log "Starting $TASK_ID"
if bash "$NEXT_TASK/run.sh" >> "$NEXT_TASK/output.log" 2>&1; then
echo "DONE" > "$NEXT_TASK/state"
date -u +%FT%TZ > "$NEXT_TASK/completed_at"
log "$TASK_ID DONE"
else
rc=$?
echo "FAILED" > "$NEXT_TASK/state"
echo "exit_code_${rc}" > "$NEXT_TASK/failure_reason"
log "$TASK_ID FAILED rc=$rc"
exit "$rc"
fi
Enqueue a task by writing a run.sh and a state file:
# enqueue-task.sh <task-id> (pipe the script body on stdin)
TASK_ID="${1:-task-$(date +%s)}"
TASK_DIR="${HOME}/agent-queue/${TASK_ID}"
mkdir -p "$TASK_DIR"
cat > "$TASK_DIR/run.sh"
chmod +x "$TASK_DIR/run.sh"
echo "PENDING" > "$TASK_DIR/state"
date -u +%FT%TZ > "$TASK_DIR/enqueued_at"
This is an unexecuted example. Run it against a throwaway task before pointing it at real work.
Each invocation does one poll and exits. StartInterval schedules the next poll; KeepAlive.SuccessfulExit = false tells launchd to restart the worker only when it exits non-zero, i.e. after a failed task. The two keys evaluate independently (launchd.plist(5), Apple Creating Launchd Jobs).
<!-- ~/Library/LaunchAgents/com.yourteam.queue-worker.plist -->
<plist version="1.0">
<dict>
<key>Label</key><string>com.yourteam.queue-worker</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/agent/agent-queue/queue-loop.sh</string>
</array>
<key>StartInterval</key><integer>30</integer>
<key>KeepAlive</key>
<dict><key>SuccessfulExit</key><false/></dict>
<key>StandardOutPath</key><string>/Users/agent/agent-queue/worker.log</string>
<key>StandardErrorPath</key><string>/Users/agent/agent-queue/worker-err.log</string>
</dict>
</plist>
Load with launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.yourteam.queue-worker.plist. Setting SuccessfulExit implies RunAtLoad, so the first poll happens immediately.
Before trusting the queue, prove the lock works. This test launches two workers against one PENDING task and asserts it ran exactly once.
#!/usr/bin/env bash
# concurrency-test.sh
set -euo pipefail
QUEUE_DIR="${HOME}/agent-queue"
TASK_ID="concurrency-test-$(date +%s)"
TASK_DIR="${QUEUE_DIR}/${TASK_ID}"
mkdir -p "$TASK_DIR"
printf '#!/usr/bin/env bash\nsleep 5\necho "pid=$$" >> "%s/execution_log"\n' \
"$TASK_DIR" > "$TASK_DIR/run.sh"
chmod +x "$TASK_DIR/run.sh"
echo "PENDING" > "$TASK_DIR/state"
bash "${QUEUE_DIR}/queue-loop.sh" & P1=$!
bash "${QUEUE_DIR}/queue-loop.sh" & P2=$!
wait "$P1" || true
wait "$P2" || true
count=$( [[ -f "$TASK_DIR/execution_log" ]] && wc -l < "$TASK_DIR/execution_log" || echo 0 )
state=$(cat "$TASK_DIR/state")
echo "state=$state count=$count"
if [[ "$count" -eq 1 && "$state" == "DONE" ]]; then
echo "PASS"; rm -rf "$TASK_DIR"
else
echo "FAIL"; exit 1
fi
Re-run this after any OS update, launchd reload or edit to queue-loop.sh. A failed test is a real failure, not a benchmark data point.
concurrency-test.sh prints PASS.worker.log shows the ordered transitions PENDING → RUNNING → DONE.run.sh ends with exit 1 ends in state FAILED with a failure_reason.launchctl kickstart -k gui/$(id -u)/com.yourteam.queue-worker during a running task, the task is left in FAILED on the next poll, not RUNNING.~/agent-queue/.worker.lock is absent while no worker is active.| Factor | Virtual Intel macOS, $29/mo | M-series from $95/mo |
|---|---|---|
| Concurrent workers on this design | 1 (lock is per host) | 1 per plist; use separate hosts for real parallelism |
| Disk for queue directories | 80 GiB included | Confirmed in your quote |
| Task runtime requirements | Intel-compatible binaries, shell, Python, Node, curl | ARM-native binaries, Xcode where chip/OS allow |
| Trigger to upgrade | run.sh needs an ARM-only binary, Xcode 27 or visionOS | — |
The single-worker design is deliberate: multiple workers on one host need a distributed lock, and at that point you want multiple hosts instead. The Managed Mac Team plan at $999/month includes two M-series Macs, two CI runners, backups, monitoring and one hour of monthly support — the natural next step when one queue is no longer enough. Exact hardware specs, disk sizes and physical delivery are confirmed in the quote.
Start your hosted Mac · All Mac hosting plans
Related: Mac agent large uploads · Mac Safari scheduled PDF · GitHub Actions self-hosted runner on Mac