How Paperclip prevents overlapping runs for the same agent using PostgreSQL advisory locks and row-level locking. What happens when a lock can't be acquired and the fallback behavior.
How Paperclip runs multiple agents in parallel within a single tick, configure concurrency limits, and understand the performance trade-offs.
By default, Paperclip invokes all active agents in parallel within one tick using Promise.allSettled. With five active agents, all five adapter calls start simultaneously.
Tick T=0:00
Agent A invoke ──────────────────► complete T=0:28
Agent B invoke ──────────────────► complete T=0:31
Agent C invoke ──────────────────► complete T=0:15
Agent D invoke ──────────────────► complete T=0:42
Agent E invoke ──────────────────► complete T=0:35
Tick T=0:42 (overall completion time = slowest agent)
The tick's completion time equals the slowest agent, not the sum of all durations.
Control how many agents run in parallel:
Company Settings → Heartbeat → Max concurrent agents (default: 10).
Or in the heartbeat config:
// Limit to 3 parallel adapters
const semaphore = new Semaphore(maxConcurrentAgents ?? 10);
await Promise.allSettled(
agents.map(agent =>
semaphore.acquire().then(release =>
dispatchOne(agent, ...).finally(release)
)
)
);
| Situation | Recommended limit |
|---|---|
| Free-tier API keys (strict rate limits) | 1–2 |
| Production with standard API keys | 5–10 (default) |
| Self-hosted models (Ollama on a single GPU) | 1 |
| Multiple cloud providers, generous rate limits | 20+ |
When the LLM API returns 429 (rate limit), the adapter throws and the issue is marked as failed + retried next tick. Set a lower concurrency limit to stay within your API quota.
For claude-code adapter specifically: Anthropic rate limits are per-API-key and per-model. Using different models (claude-haiku vs claude-opus) for different agents can double your effective throughput.
By default each agent claims one issue per tick. To let an agent claim and process multiple issues in one tick:
{
"adapter_config": {
"max_issues_per_tick": 3
}
}
This is useful for a "light" agent doing quick classification tasks. Do not use it for expensive LLM agents unless you're confident about budget.
-- See max concurrent adapters in a given run
SELECT
id,
agents_invoked,
issues_claimed,
ROUND(
EXTRACT(epoch FROM (finished_at - started_at)) / NULLIF(agents_invoked, 0)
) AS avg_seconds_per_agent,
EXTRACT(epoch FROM (finished_at - started_at)) AS total_seconds
FROM heartbeat_runs
WHERE company_id = 'your-company-id'
ORDER BY started_at DESC
LIMIT 5;
max_concurrent_agents for rate-limited APIsmax_issues_per_tick > 1 is appropriate