Concurrency & backpressure
Driftstack caps the number of concurrent sessions an account can keep open. This page covers the cap per tier, the 429 signal you get when you hit it, and how to back off cleanly from clients.
Concurrent-session caps
Caps below match TIER_CONCURRENT_SESSION_LIMITS in
@driftstack/api-types; the server reads from the
same constant. Numbers are deliberately conservative — Driftstack
bills per-session, not per-call.
| Tier | Cap | Notes |
|---|---|---|
free | 1 | Perpetual free evaluation tier (1 session, 20-min cap). |
solo_manual | 1 | Manual operator workflow. |
team_manual | 3 | Shared across the team. |
agency_manual | 8 | Shared across the agency. |
api_starter | 2 | |
api_builder | 8 | |
api_scale | 24 | |
enterprise | 32 | Contract path can raise this further. |
"Concurrent session" = a session that has not yet been
destroyed (status ≠ destroyed & ≠
errored). Cleanup is your responsibility — leaked
sessions count against the cap until the 30-minute
idle-cleanup sweep runs.
The 429 signal
When a POST /v1/sessions would push you past the
cap, the server responds with:
HTTP/1.1 429 Too Many Requests
Retry-After: 15
Content-Type: application/problem+json
{
"type": "https://errors.driftstack.dev/concurrency-limit",
"title": "Concurrent session limit reached",
"status": 429,
"detail": "Account already has 20 active sessions; tier permits 20.",
"current_sessions": 20,
"limit": 20
}
The Retry-After header is a hint, not a contract —
it's the time we estimate it'd take for one of your current
sessions to naturally complete (based on your average session
duration). Don't sleep blindly past it; respond when one of
your own sessions finishes.
The canonical client backoff loop
Switch on the RFC 7807 type URI — clients should
dispatch on the stable problem-type, not on title or detail
strings:
async function createSessionWithBackoff(client, opts) {
const maxAttempts = 5;
let delay = 1_000;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await client.sessions.create(opts);
} catch (err) {
if (err.type !== 'https://errors.driftstack.dev/concurrency-limit') throw err;
const retryAfter = (err.retryAfterSeconds ?? delay / 1000) * 1000;
await sleep(retryAfter);
delay = Math.min(delay * 2, 60_000); // exponential cap at 60s
}
}
throw new Error('Could not acquire a session slot after retries');
} Don't pre-emptively rate-limit yourself client-side. The server's cap is the truth — back off only when it tells you to.
Pooling sessions vs creating ephemeral ones
Two patterns work well:
- Ephemeral (default): create a session, do the work, destroy. Simple, no leak risk. Throughput is bounded by session-create latency (~500ms).
- Pooled: keep N sessions alive across requests; round-robin work onto them. Higher throughput for burst workloads, but you have to manage health (periodically re-create idle-stale sessions) and respect the cap.
For pooling: don't oversubscribe. If your tier caps at 20, run a pool of 18 — leaves headroom for ad-hoc creates that shouldn't have to evict pooled sessions.
Rate limits vs. concurrency limits
These are separate systems:
- Rate limits (per-second tokens) cap request throughput. See /docs/rate-limits.
- Concurrency limits (this page) cap active-session count. You can hit either one independently.
Both surface as 429s but with different
type URIs
(.../rate-limited vs.
.../concurrency-limit) so clients can dispatch on
the stable problem-type.
Raising the cap
Upgrade to a higher tier via /pricing — the new cap applies immediately on tier change. For Enterprise overrides above the default 500, email [email protected]; we can lift the cap without a tier change for short-term campaign bursts.
Observability
Track your own concurrency from the dashboard or via
GET /v1/account/me (the response includes
concurrent_session_active alongside
concurrent_session_cap). For per-day
patterns, the
audit log carries
session.created + session.destroyed
entries — diff them for an open-session timeseries.
Support
Capacity questions or unexpected 429s: [email protected].