Rate Limit Safeguards & Task Queue System
Overview
This skill documents the architecture and operational patterns for preventing API rate-limit crashes in automated AI agent task loops. The core insight: never inject tasks into a session that cannot respond. Instead, queue them and drain slowly on recovery.
Born from a real incident where a feedback loop of automated prompts into a rate-limited session extended the lockout period catastrophically. The queue system eliminates that class of failure.
When to Use
Apply these safeguards whenever:
- You are building or modifying a scheduled task injection system
- You are spawning 3+ agents in a single session
- You observe signs of rate-limit pressure (slow responses, early compaction, growing queue)
- You are designing any system that sends repeated automated prompts to an AI session
Architecture
Queue System
Tasks that cannot be injected immediately are stored in a JSON queue file and drained slowly once the session recovers. No task is ever lost -- just spaced out.
/tmp/task_queue.json
Queue entry structure:
{"type": "task-type-name", "message": "...", "queued_at": 1743500000}
Dedup by type: If the same task type is already in the queue, the duplicate is dropped silently. This prevents backlog explosion for recurring tasks.
Responsiveness Detection
session_is_responsive() uses two signals:
- Activity file modification time -- checks the most recently modified activity file
for your session. If modified within
RECENT_THRESHOLD=300seconds (5 minutes), session is considered active. - Human interaction file -- checks a human interaction timestamp file against the same 300-second threshold.
If either signal is fresh, the session is responsive. Both stale = unresponsive = queue mode.
inject_or_queue Routing
Central routing function called by every scheduled task:
inject_or_queue "task-type" "$TASK_MESSAGE" "$SESSION"
Decision tree:
SESSION_RESPONSIVE == trueANDCYCLE_INJECTIONS < MAX_INJECTIONS_PER_CYCLE-> Acquire lock, inject via tmux send-keys, increment counter, release lock- Any other condition
->
queue_task()-- append to JSON queue, log "Queued task: "
Queue Drain on Recovery
At the start of each cycle, if the session is responsive and the queue is non-empty:
drain_queue 2 30 "$SESSION"
Parameters:
max_drain=2-- drain at most 2 tasks per cyclespacing=30-- wait 30 seconds between each drained task- Oldest entries drained first (sorted by
queued_at) - Drained count is added to
CYCLE_INJECTIONSbefore any new injections
Cycle Injection Cap
MAX_INJECTIONS_PER_CYCLE=3
Total injections (queue drain + new) per 30-minute cycle. This prevents API overload when multiple tasks become due simultaneously (e.g., after a 2-hour outage clears).
Budget allocation example:
- Queue had 5 items -> drain 2 (uses 2 of 3 slots)
- 1 new task slot remaining for new tasks this cycle
- Remaining 3 queue items defer to next cycle automatically
Alert System
When queue size reaches 10+ items, an alert is sent to the operator via your preferred notification channel (Telegram, Slack, email, etc.) once per alert period. Use a guard file to prevent repeated alerts. Alert resets when queue drops below 10.
Implementation
Key Configuration Values
| Variable | Value | Purpose |
|---|---|---|
TASK_QUEUE_FILE |
/tmp/task_queue.json |
Queue storage |
RECENT_THRESHOLD |
300 (5 min) |
Responsiveness window |
MAX_INJECTIONS_PER_CYCLE |
3 |
Per-cycle injection cap |
LOCK_TTL |
900 (15 min) |
Lock staleness threshold |
| Queue alert threshold | 10 items |
Alert trigger |
| Drain per cycle | 2 |
Max queue drain per cycle |
| Drain spacing | 30s |
Delay between drained tasks |
Key Functions
| Function | Purpose |
|---|---|
session_is_responsive() |
Returns 0 if session active, 1 if stale |
inject_or_queue(type, msg, session) |
Routes task to injection or queue |
queue_task(type, message) |
Appends to JSON queue with dedup |
drain_queue(max, spacing, session) |
Drains N oldest tasks with spacing |
queue_size() |
Returns current queue length |
acquire_lock(name) |
Creates lock file. Returns 1 if held. |
release_lock(name) |
Removes lock file (caller must be holder) |
Lock System
Prevents multiple simultaneous tmux injections:
- Lock file:
/tmp/task_inject_lock - TTL: 15 minutes (
LOCK_TTL=900) - Stale detection: If lock older than TTL, automatically reclaimed (prevents a crashed task from permanently blocking the queue)
inject_or_queuehandles lock internally. Callers do NOT call acquire/release directly.
Agent Spawning Guidelines
These are separate from the task queue but address the same root cause: API overload from too many concurrent requests.
| Scenario | Approach |
|---|---|
| 1-2 independent agents | Parallel is fine |
| 3+ agents | Sequential -- wait for each to complete before spawning next |
| Background agents | Max 2 concurrent. Use run_in_background: true. |
| After spawning background agent | Wait at least 30 seconds before spawning another |
Principle: Prefer sequential spawning with spacing over concurrent hard caps. Spacing prevents API burst without sacrificing throughput on single-agent tasks.
What Caused the Original Crash
Root cause: Automated prompts kept firing into a rate-limited session.
Mechanism:
- The AI session hit API rate limits -- session became unresponsive
- Scheduled tasks continued injecting every 30 minutes
- Each injection consumed API tokens (the tmux send-keys itself triggered API calls)
- The token consumption extended the rate-limit lockout period
- Extended lockout meant more tasks piled up, compounding the problem
- Feedback loop sustained itself for hours
Fix applied: The queue system prevents injection entirely when the session is unresponsive. Tasks accumulate in JSON without touching the API. When the session recovers, they drain slowly (2 per cycle, 30s spacing, under the 3-injection cap), staying well under per-minute limits.
Recovery Protocol
When rate-limited:
Automatic handling -- the queue system accumulates tasks in the queue file without injecting them. No manual intervention needed.
When session recovers -- next cycle detects responsiveness, drains up to 2 queued tasks with 30-second spacing, respecting the 3-injection cap.
If outage exceeds
5 cycles (150 min) -- queue grows to 10+ items, operator is alerted. They can assess whether manual intervention is needed.Resume normal operations -- once session activity resumes, the queue drains automatically over subsequent cycles.
Manual drain -- the queue self-resolves. No manual intervention needed unless the alert fires and the operator determines the backlog is stale/irrelevant (in which case:
echo "[]" > /tmp/task_queue.json).
Signs of Approaching Rate Limits
Watch for these early warning signals:
- Response times increasing noticeably
- Tool calls taking longer than usual
- Context compaction firing earlier than expected
- Task queue growing (check:
python3 -c "import json; print(len(json.load(open('/tmp/task_queue.json'))))") - Alert notification about queue size
Adoption Guide
To adopt this pattern in your own AI agent setup:
Implement the queue functions:
session_is_responsive()queue_task()drain_queue()queue_size()inject_or_queue()acquire_lock()/release_lock()
Replace all direct
tmux send-keysinjection calls withinject_or_queue "type" "$MSG" "$SESSION"Add the drain block at the start of your main loop (before any new injections):
SESSION_RESPONSIVE="false" CYCLE_INJECTIONS=0 if session_is_responsive; then SESSION_RESPONSIVE="true" Q_SIZE=$(queue_size) if [[ $Q_SIZE -gt 0 ]]; then DRAINED=$(drain_queue 2 30 "$SESSION") CYCLE_INJECTIONS=$((CYCLE_INJECTIONS + DRAINED)) fi fiSet alert thresholds appropriate for your notification channel and operator preferences.