Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when handling external API rate limits (e.g., OpenAI 429s, HubSpot or Stripe rate limits), preventing duplicate work from rapid event bursts (debouncing user actions), spreading load over time, ensuring per-tenant fairness, processing events in batches, limiting concurrent runs of the same operation, or assigning priority to important runs. Covers Inngest flow control: concurrency limits with keys, throttling, rate limiting, debounce, priority, singleton, and event batching.
.claude/skills/asymmetric-al-inngest-flow-control/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | -38% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 10% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 104% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 32% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 59% | 0% |
Master Inngest flow control mechanisms to manage resources, prevent overloading systems, and ensure application reliability. This skill covers all flow control options with prescriptive guidance on when and how to use each.
> These skills are focused on TypeScript. For Python or Go, refer to the Inngest documentation for language-specific guidance. Core concepts apply across all languages.
When to use: Limit the number of executing steps (not function runs) to manage computing resources and prevent system overwhelm.
Key insight: Concurrency limits active code execution, not function runs. A function waiting on step.sleep() or step.waitForEvent() doesn't count against the limit.
typescriptinngest.createFunction( { id: "process-images", concurrency: 5, triggers: [{ event: "media/image.uploaded" }], }, async ({ event, step }) => { // Only 5 steps can execute simultaneously await step.run("resize", () => resizeImage(event.data.imageUrl)); }, );
Use key parameter to apply limit per unique value of the key.
typescriptinngest.createFunction( { id: "user-sync", concurrency: [ { key: "event.data.user_id", limit: 1, }, ], triggers: [{ event: "user/profile.updated" }], }, async ({ event, step }) => { // Only 1 step per user can execute at once // Prevents race conditions in user-specific operations }, );
typescriptinngest.createFunction( { id: "ai-summary", concurrency: [ { scope: "account", key: `"openai"`, limit: 60, }, ], triggers: [{ event: "ai/summary.requested" }], }, async ({ event, step }) => { // Share 60 concurrent OpenAI calls across all functions }, );
When to use each:
When to use: Control the rate of function starts over time to work around API rate limits or smooth traffic spikes.
Key difference from concurrency: Throttling limits function run starts; concurrency limits step execution.
typescriptinngest.createFunction( { id: "sync-crm-data", throttle: { limit: 10, // 10 function starts period: "60s", // per minute burst: 5, // plus 5 immediate bursts key: "event.data.customer_id", // per customer }, triggers: [{ event: "crm/contact.updated" }], }, async ({ event, step }) => { // Respects CRM API rate limits: 10 calls/min per customer await step.run("sync", () => crmApi.updateContact(event.data)); }, );
Configuration:
limit: Functions that can start per periodperiod: Time window (1s to 7d)burst: Extra immediate starts allowedkey: Apply limits per unique key valueWhen to use: Hard limit to prevent abuse or skip excessive duplicate events.
Key difference from throttling: Rate limiting discards events; throttling delays them.
typescriptinngest.createFunction( { id: "webhook-processor", rateLimit: { limit: 1, period: "4h", key: "event.data.webhook_id", }, triggers: [{ event: "webhook/data.received" }], }, async ({ event, step }) => { // Process each webhook only once per 4 hours // Prevents duplicate webhook spam }, );
Use cases:
When to use: Wait for a series of events to stop arriving before processing the latest one.
typescriptinngest.createFunction( { id: "save-document", debounce: { period: "5m", // Wait 5min after last edit key: "event.data.document_id", timeout: "30m", // Force save after 30min max }, triggers: [{ event: "document/content.changed" }], }, async ({ event, step }) => { // Saves document only after user stops editing // Uses the LAST event received await step.run("save", () => saveDocument(event.data)); }, );
Perfect for:
When to use: Execute some function runs ahead of others based on dynamic data.
typescriptinngest.createFunction( { id: "process-order", priority: { // VIP users get priority up to 120 seconds ahead run: "event.data.user_tier == 'vip' ? 120 : 0", }, triggers: [{ event: "order/placed" }], }, async ({ event, step }) => { // VIP orders jump ahead in the queue }, );
Advanced example:
typescriptinngest.createFunction( { id: "support-ticket", priority: { run: ` event.data.severity == 'critical' ? 300 : event.data.severity == 'high' ? 120 : event.data.user_plan == 'enterprise' ? 60 : 0 `, }, triggers: [{ event: "support/ticket.created" }], }, async ({ event, step }) => { // Critical tickets get highest priority (300s ahead) // High severity: 120s ahead // Enterprise users: 60s ahead // Everyone else: normal priority }, );
When to use: Ensure only one instance of a function runs at a time.
typescriptinngest.createFunction( { id: "data-backup", singleton: { key: "event.data.database_id", mode: "skip", }, triggers: [{ event: "backup/requested" }], }, async ({ event, step }) => { // Skip new backups if one is already running for this database await step.run("backup", () => performBackup(event.data.database_id)); }, );
typescriptinngest.createFunction( { id: "realtime-sync", singleton: { key: "event.data.user_id", mode: "cancel", }, triggers: [{ event: "user/data.changed" }], }, async ({ event, step }) => { // Cancel previous sync and start with latest data await step.run("sync", () => syncUserData(event.data)); }, );
When to use: Process multiple events together for efficiency.
typescriptinngest.createFunction( { id: "bulk-email-send", batchEvents: { maxSize: 100, // Up to 100 events timeout: "30s", // Or 30 seconds, whichever first // `key` groups events into separate batches per unique value // This is different from expressions `if` which filters events key: "event.data.campaign_id", // Batch per campaign }, triggers: [{ event: "email/send.queued" }], }, async ({ events, step }) => { // Process array of events together const emails = events.map((evt) => ({ to: evt.data.email, subject: evt.data.subject, body: evt.data.body, })); await step.run("send-batch", () => emailService.sendBulk(emails)); }, );
typescriptinngest.createFunction( { id: "ai-image-processing", // Global throttling for API limits throttle: { limit: 50, period: "60s", key: `"gpu-cluster"`, }, // Per-user concurrency for fairness concurrency: [ { key: "event.data.user_id", limit: 3, }, ], // VIP users get priority priority: { run: "event.data.plan == 'pro' ? 60 : 0", }, triggers: [{ event: "ai/image.generate" }], }, async ({ event, step }) => { // Combines multiple flow controls for optimal resource usage }, );
Pro tip: Most production functions benefit from combining 1-3 flow control mechanisms for optimal reliability and performance.
These upstream Inngest instructions are vendored for agent tooling and integration work in this monorepo.
Use this skill when inngest-flow-control matches the current Inngest task. If the right skill is unclear, start with docs/ai/skills/inngest/SKILL.md.
integration.
inngest-brownfield-audit before changing existing app workflows orfragile background work.
AGENTS.md, reporulebooks, framework docs, and runtime evidence.
INNGEST_* envrequirements out of agent-tooling-only changes.
or dependencies.
workflow behavior.
port.
docs/ai/skills/inngest/references/upstream.md.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-02 | pass→pass | 12,769 | 5,252 | -59% | 1 | 1 | 0% | 2,507 | 3,675 | +47% | 0 | 0 | — |
case-03 | fail→pass | 33,011 | 5,333 | -84% | 1 | 1 | 0% | 6,015 | 3,726 | -38% | 0 | 0 | — |
case-01 | pass→pass | 15,427 | 7,062 | -54% | 1 | 1 | 0% | 2,761 | 4,010 | +45% | 0 | 0 | — |
case-04 | fail→pass | 17,400 | 4,201 | -76% | 1 | 1 | 0% | 3,110 | 3,421 | +10% | 0 | 0 | — |
case-05 | fail→pass | 10,707 | 5,044 | -53% | 1 | 1 | 0% | 1,824 | 3,719 | +104% | 0 | 0 | — |
case-06 | pass→pass | 11,275 | 5,657 | -50% | 1 | 1 | 0% | 1,874 | 3,664 | +96% | 0 | 0 | — |
case-07 | pass→pass | 9,400 | 4,546 | -52% | 1 | 1 | 0% | 1,802 | 3,414 | +89% | 0 | 0 | — |
case-08 | fail→pass | 17,002 | 5,119 | -70% | 1 | 1 | 0% | 2,779 | 3,657 | +32% | 0 | 0 | — |
case-09 | pass→pass | 11,705 | 4,042 | -65% | 1 | 1 | 0% | 2,105 | 3,459 | +64% | 0 | 0 | — |
case-10 | pass→pass | 8,676 | 4,519 | -48% | 1 | 1 | 0% | 1,708 | 3,564 | +109% | 0 | 0 | — |
case-11 | pass→pass | 7,826 | 5,735 | -27% | 1 | 1 | 0% | 1,544 | 3,761 | +144% | 0 | 0 | — |
case-12 | fail→pass | 11,465 | 5,655 | -51% | 1 | 1 | 0% | 2,401 | 3,818 | +59% | 0 | 0 | — |
case-13 | pass→pass | 11,998 | 6,298 | -48% | 1 | 1 | 0% | 2,253 | 3,839 | +70% | 0 | 0 | — |
case-14 | pass→pass | 13,534 | 7,886 | -42% | 1 | 1 | 0% | 2,394 | 4,164 | +74% | 0 | 0 | — |
case-15 | pass→pass | 10,976 | 5,900 | -46% | 1 | 1 | 0% | 1,900 | 3,671 | +93% | 0 | 0 | — |
case-16 | pass→pass | 6,078 | 4,648 | -24% | 1 | 1 | 0% | 1,134 | 3,528 | +211% | 0 | 0 | — |
case-17 | fail→pass | 15,398 | 3,886 | -75% | 1 | 1 | 0% | 2,439 | 3,347 | +37% | 0 | 0 | — |
case-18 | pass→pass | 14,037 | 5,703 | -59% | 1 | 1 | 0% | 2,400 | 3,652 | +52% | 0 | 0 | — |
case-19 | pass→pass | 9,368 | 3,874 | -59% | 1 | 1 | 0% | 1,674 | 3,384 | +102% | 0 | 0 | — |
case-20 | pass→pass | 10,895 | 4,869 | -55% | 1 | 1 | 0% | 1,929 | 3,584 | +86% | 0 | 0 | — |
case-21 | pass→pass | 11,761 | 8,694 | -26% | 1 | 1 | 0% | 2,179 | 4,257 | +95% | 0 | 0 | — |
case-22 | pass→pass | 11,336 | 7,740 | -32% | 1 | 1 | 0% | 2,246 | 4,224 | +88% | 0 | 0 | — |
case-23 | pass→pass | 10,498 | 6,468 | -38% | 1 | 1 | 0% | 2,038 | 3,950 | +94% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +26 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.