--- id: backend-cron-workflows-inngest title: Inngest / Trigger.dev — function-as-a-workflow category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [backend, workflow, vibe-coding] tech_stack: { language: "TS", applicable_to: ["Backend"] } applied_in: [] aliases: [Inngest, Trigger.dev, function workflow, durable function, event-driven, cron, retry] --- # Inngest / Trigger.dev > Modern async / cron / workflow. **Code 가 function, retry / step / wait 자동**. Temporal 의 simple version. ## 📖 핵심 개념 - Function = workflow. - Step = retry / dedup unit. - Event 가 trigger. - Cron / wait 가 native. ## 💻 코드 패턴 ### Inngest function ```ts import { Inngest } from 'inngest'; const inngest = new Inngest({ id: 'my-app' }); export const dailyReport = inngest.createFunction( { id: 'daily-report' }, { cron: '0 9 * * *' }, async ({ event, step }) => { const data = await step.run('fetch', async () => fetchData()); await step.sleep('wait', '1m'); await step.run('email', async () => sendEmail(data)); return { ok: true }; } ); ``` → 매 step 가 retry / dedup 자동. ### Event-triggered ```ts export const onUserSignup = inngest.createFunction( { id: 'welcome-email' }, { event: 'user/signup' }, async ({ event, step }) => { await step.run('send', () => sendWelcome(event.data.email)); await step.sleep('wait-3-days', '3d'); await step.run('check-active', async () => { const user = await getUser(event.data.userId); if (!user.active) await sendReengagement(user); }); } ); // Trigger await inngest.send({ name: 'user/signup', data: { userId, email } }); ``` ### Step.sleep (durable wait) ```ts await step.sleep('wait', '1d'); // → Function 가 종료. 1 day 후 resume. // → Server restart 가 OK (state durable). ``` → Long-running workflow. ### Step.waitForEvent ```ts const event = await step.waitForEvent('payment-confirmed', { event: 'payment/confirmed', match: 'data.userId', timeout: '1h', }); if (!event) { // Timeout } ``` → "다음 event 가 도착 까지 wait". ### Trigger.dev ```ts import { client } from './trigger'; client.defineJob({ id: 'daily-report', trigger: cronTrigger({ cron: '0 9 * * *' }), run: async (payload, io) => { const data = await io.runTask('fetch', async () => fetchData()); await io.sendEvent('email', { name: 'send', payload: { data } }); }, }); ``` → Inngest 와 매우 비슷. ### Concurrency limit ```ts { concurrency: { limit: 10 } } // → 10 instance max parallel. ``` ### Throttle ```ts { throttle: { limit: 100, period: '1m' } } // → 1 min 안 100 max. ``` ### Retry policy ```ts inngest.createFunction( { id: 'task', retries: 5 }, ... async ({ step }) => { await step.run('flaky', async () => { // Auto retry 5 times. }); } ); ``` ### Cancel running function ```ts { cancelOn: [{ event: 'user/deleted', if: 'event.data.userId == async.data.userId' }] } ``` → 외부 event 가 cancel. ### Local dev ```bash npx inngest-cli dev # → Dashboard at localhost:8288. ``` → Function trace + replay + UI. ### Production deploy ```ts // Vercel / Cloudflare / Lambda // Inngest 가 serverless 친화. // app/api/inngest/route.ts (Next.js) import { serve } from 'inngest/next'; import { dailyReport, onUserSignup } from '../../inngest'; export const { GET, POST, PUT } = serve({ client: inngest, functions: [dailyReport, onUserSignup], }); ``` ### vs Temporal ``` Temporal: - 강력 (workflow language). - Self-host 가 가능. - 큰 enterprise. - Steeper. Inngest: - TS-friendly. - Managed 친화. - Simple. - 작은 / 중간. → Modern serverless = Inngest. 큰 / on-prem = Temporal. ``` ### vs BullMQ ``` BullMQ: - Redis-backed. - Self-host. - 더 raw. Inngest: - Managed. - Workflow primitive. - Step / sleep native. → Inngest 가 더 production friendly. ``` ### vs AWS Step Functions ``` Step Functions: - AWS native. - ASL (state machine). - Managed scaling. Inngest: - TS code (no DSL). - Multi-cloud. → AWS-only = Step Functions. Multi-cloud / TS = Inngest. ``` ### Use case ``` - Email sequence (welcome → 3 day → 7 day). - LLM agent workflow. - Image processing pipeline. - Daily report. - Subscription billing. - Scheduled cleanup. - Data sync (매 hour). ``` ### LLM agent (Inngest) ```ts export const agent = inngest.createFunction( { id: 'agent' }, { event: 'agent/start' }, async ({ event, step }) => { let context = event.data.task; for (let i = 0; i < 5; i++) { const action = await step.run(`think-${i}`, async () => llm.complete(...)); if (action.type === 'done') return action.result; const result = await step.run(`act-${i}`, async () => execute(action)); context += result; } } ); ``` → Multi-step agent + retry / pause / resume. ### Idempotency ```ts { idempotency: 'event.data.orderId', } // → 같은 orderId 의 event 가 1번만. ``` ### Batching ```ts { batchEvents: { maxSize: 100, timeout: '5s' } } // → 5 sec 또는 100 event 마다 batch. ``` ### Cost ``` Inngest free: 1k step / month. Pro: $20-200 / month. Trigger.dev: 비슷. → Self-host alternative 가 BullMQ + cron. ``` ### Anti-pattern ``` - Step 안에 step.run 가 안: nested 안 됨. - Side effect 가 step.run 외: retry 시 중복. - Long step (>30 sec): timeout (function level). - Throw vs return error: 매 다름. ``` ### Best practice ``` 1. 매 side effect 가 step.run. 2. Idempotent step (key). 3. Concurrency / throttle (외부 API rate limit). 4. Timeout (긴 wait 도). 5. Error monitoring. ``` ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | Modern serverless | Inngest / Trigger.dev | | 큰 / on-prem | Temporal | | Self-host simple | BullMQ | | AWS-only | Step Functions | | LLM agent | Inngest (durable + retry) | | Email sequence | Inngest (sleep) | ## ❌ 안티패턴 - **Side effect 가 step 외**: retry 중복. - **Long sleep 가 step.sleep 안**: function timeout. - **No idempotency**: replay 시 중복. - **No concurrency limit**: external API 폭발. - **Step 가 nested**: 안 됨. ## 🤖 LLM 활용 힌트 - Inngest = durable function + step. - Cron / event / wait native. - LLM agent 에 강함. - Temporal 의 simple alternative. ## 🔗 관련 문서 - [[Backend_Cron_Scheduler_Patterns]] - [[Backend_Job_Scheduling_Temporal]] - [[Backend_Saga_Choreography_vs_Orchestration]]