Inngest: Bikin Durable Functions dan Workflow Event-Driven Tanpa Ribet Ngurus Queue

# Inngest: Bikin Durable Functions dan Workflow Event-Driven Tanpa Ribet Ngurus Queue Temen-temen, kalau kamu pernah bangun aplikasi yang butuh proses di background, misalnya kirim email setelah user...

By Ruby Abdullah · · tutorial
inngestevent-drivenpythonworkflowsbackground-jobs

Inngest: Bikin Durable Functions dan Workflow Event-Driven Tanpa Ribet Ngurus Queue

Temen-temen, kalau kamu pernah bangun aplikasi yang butuh proses di background, misalnya kirim email setelah user daftar, jalanin billing tiap bulan, atau proses video yang lama, pasti kamu ketemu masalah klasik: gimana caranya biar prosesnya reliable? Gimana kalau di tengah jalan servernya mati? Gimana kalau salah satu langkah gagal dan mau di-retry, tapi langkah sebelumnya jangan diulang? Biasanya jawabannya adalah pasang message queue kayak RabbitMQ, Redis, atau SQS, terus bikin worker, terus ngurus retry logic sendiri, terus mikirin dead letter queue, dan seterusnya. Capek kan?

Nah di tutorial ini aku mau ngajak kamu kenalan sama Inngest. Inngest itu platform buat bikin durable, event-driven functions dan workflow tanpa kamu harus ngurus queue infrastructure sendiri. Konsep intinya sederhana: kamu kirim event, event itu men-trigger function, dan di dalam function kamu bisa pecah kerjaan jadi steps yang masing-masing durable dan di-retry secara independen. Jadi kalau step ketiga gagal, step pertama dan kedua nggak diulang, hasilnya udah disimpan sama Inngest. Keren banget kan?

Aku bakal bahas dari konsep dasarnya, cara install pakai Python, cara define function, pakai step buat durability, sleep, nunggu event lain, ngirim event, sampai jalanin dev server buat testing lokal. Yuk kita mulai.

Kenapa Butuh Inngest?

Sebelum masuk ke code, aku mau kamu paham dulu masalah yang mau dipecahin Inngest. Bayangin kamu punya flow onboarding user kayak gini: user daftar, terus kamu mau kirim welcome email, terus tunggu tiga hari, kalau user belum lengkapin profil kirim reminder, terus tunggu tujuh hari, kalau masih belum aktif kirim email khusus. Kalau kamu bikin ini pakai cara tradisional, kamu butuh cron job, tabel database buat nyimpen state, logika buat cek user udah sampai tahap mana, dan penanganan error di tiap titik. Ribet dan gampang bocor.

Dengan Inngest, semua itu jadi satu function yang kelihatan kayak kode biasa dari atas ke bawah. Kamu tulis "kirim email, tidur tiga hari, cek profil, kirim reminder", dan Inngest yang ngurus persistence-nya. Waktu function kamu "tidur" tiga hari, dia nggak beneran nahan proses selama tiga hari. Inngest nyimpen state-nya, function-nya berhenti, dan tiga hari kemudian Inngest bangunin lagi function itu dari titik yang tepat. Ini yang disebut durable execution.

Beberapa hal yang bikin Inngest menarik menurut aku:

  • Nggak perlu infra queue sendiri. Kamu nggak usah setup Redis atau RabbitMQ. Inngest yang jadi orchestrator-nya.
  • Retry otomatis per step. Kalau satu step lempar exception, Inngest retry step itu aja dengan backoff, bukan seluruh function dari awal.
  • Flow control bawaan. Ada throttling, rate limiting, debounce, concurrency limit, semua bisa dikonfig lewat decorator.
  • Event-driven. Function di-trigger sama event, jadi arsitekturmu jadi loosely coupled. Satu event bisa nge-trigger banyak function.
  • Local dev yang enak. Ada dev server yang jalan di laptop kamu dengan UI buat lihat event dan run function secara real-time.

Instalasi

Oke sekarang kita install. Inngest punya SDK resmi buat Python, dan cara installnya standar banget pakai pip. Aku saranin selalu pakai virtual environment biar rapi.

# Bikin virtual environment dulu

python -m venv venv

source venv/bin/activate # Windows: venv\Scripts\activate

Install Inngest SDK

pip install inngest

Kita juga butuh framework web sama server buat serve functions.

Di sini aku pakai FastAPI + uvicorn, tapi Flask/Django juga didukung.

pip install fastapi uvicorn

Selain SDK Python, kamu butuh Inngest Dev Server buat development lokal. Dev server ini jalan lewat npx (butuh Node.js terpasang), dan dia yang bakal jadi otak yang nge-trigger function kamu waktu development. Kita bahas cara jalaninnya nanti di bagian dev server. Buat sekarang, pastiin dua hal terpasang: SDK Python-nya dan Node.js buat npx.

Buat cek versi Inngest yang keinstall, kamu bisa jalanin:

pip show inngest

Basic Usage

Sekarang kita masuk ke bagian yang seru. Aku bakal tunjukin gimana bikin Inngest client, define function pertama, dan serve function itu lewat FastAPI. Ini pondasi yang wajib kamu pahami sebelum masuk ke fitur canggih.

Bikin Inngest Client

Langkah pertama adalah bikin instance dari Inngest client. Client ini yang jadi pusat kendali: dia yang dipakai buat define function dan buat ngirim event. Kamu cukup bikin sekali dan pakai di mana-mana.

import inngest

appid itu identitas unik aplikasimu di Inngest

inngestclient = inngest.Inngest(

appid="myapp",

isproduction=False, # False biar konek ke dev server lokal

)

appid itu penting karena dipakai Inngest buat ngelompokin function-function yang punya kamu. isproduction=False bilang ke SDK bahwa kita lagi development, jadi dia bakal connect ke dev server di localhost, bukan ke Inngest Cloud.

Define Function Pertama

Function di Inngest itu didefinisikan pakai decorator @inngestclient.createfunction. Kamu kasih tahu Inngest tiga hal: ID function-nya, event apa yang men-trigger dia, dan logika di dalamnya. Yuk kita bikin function yang di-trigger waktu ada user baru daftar.

@inngestclient.createfunction(

fnid="welcome-email",

trigger=inngest.TriggerEvent(event="app/user.signup"),

)

async def welcomeemail(ctx: inngest.Context) -> str:

# ctx.event.data berisi payload yang kita kirim waktu trigger event

email = ctx.event.data["email"]

name = ctx.event.data["name"]

ctx.logger.info(f"Ngirim welcome email ke {email}")

# (nanti kita ganti ini dengan step biar durable)

sendemail(to=email, subject=f"Halo {name}!", body="Selamat datang!")

return f"Email terkirim ke {email}"

Perhatiin beberapa hal di sini. fnid itu ID unik buat function, dipakai buat identifikasi di dashboard. trigger=inngest.TriggerEvent(event="app/user.signup") bilang bahwa function ini jalan tiap kali ada event bernama app/user.signup. Function-nya nerima satu argumen ctx (context), yang berisi data event, logger, dan objek step yang bakal kita pakai bentar lagi.

Konvensi penamaan event biasanya pakai format namespace/noun.verb, contohnya app/user.signup, billing/invoice.paid, atau video/upload.completed. Ini bukan aturan wajib, tapi bikin event kamu lebih rapi dan gampang dibaca.

Serve Function via FastAPI

Function udah didefine, tapi Inngest belum tahu function itu ada. Kita perlu "serve" function ke web server kita, biar Inngest dev server bisa nemuin dan manggil function itu lewat HTTP. Ini caranya pakai FastAPI:

import inngest.fastapi

from fastapi import FastAPI

app = FastAPI()

Daftarin semua function ke FastAPI di endpoint /api/inngest

inngest.fastapi.serve(

app,

inngestclient,

[welcomeemail], # list semua function yang mau di-serve

)

Sekarang jalanin server-nya:

uvicorn main:app --reload --port 8000

Inngest bakal bikin endpoint /api/inngest di aplikasimu. Endpoint ini yang dipakai dev server buat discovery (nyari tahu function apa aja yang kamu punya) dan buat manggil function waktu ada event masuk. Jadi arsitekturnya: event masuk ke Inngest, Inngest manggil endpoint kamu lewat HTTP buat jalanin function-nya step per step.

Ngirim Event

Function kita di-trigger sama event, jadi sekarang kita perlu tahu cara ngirim event. Kamu kirim event pakai inngestclient.send. Biasanya ini kamu panggil dari route lain di aplikasimu, misalnya di endpoint registrasi.

@app.post("/signup")

async def signup(email: str, name: str):

# Simpan user ke database dulu (logika bisnismu)

# ...

# Terus kirim event ke Inngest

await inngestclient.send(

inngest.Event(

name="app/user.signup",

data={"email": email, "name": name},

)

)

return {"status": "ok"}

Begitu event app/user.signup terkirim, Inngest bakal otomatis nge-trigger function welcomeemail kita. Yang keren, pengiriman event ini fire-and-forget. Route /signup kamu langsung balik respon cepat, dan pemrosesan email jalan di background. User nggak perlu nunggu.

Kamu juga bisa kirim banyak event sekaligus dengan ngasih list:

await inngestclient.send([

inngest.Event(name="app/user.signup", data={"email": "a@mail.com", "name": "Andi"}),

inngest.Event(name="app/user.signup", data={"email": "b@mail.com", "name": "Budi"}),

])

Advanced Usage

Nah sekarang kita masuk ke jantungnya Inngest: steps. Ini yang bikin Inngest beda dari sekadar background job biasa. Aku bakal bahas step.run, step.sleep, dan step.waitforevent.

step.run: Bikin Step yang Durable

Konsep paling penting di Inngest adalah step. Step itu potongan kerjaan yang hasilnya disimpan sama Inngest. Kalau function-mu jalan lagi karena retry, step yang udah berhasil nggak akan dijalanin ulang. Hasilnya diambil dari cache. Ini yang disebut durability.

Bayangin function yang punya tiga kerjaan: ambil data user, charge kartu kredit, kirim resi. Kalau kirim resi gagal karena email server-nya down, kamu nggak mau charge kartu dua kali kan? Dengan step, charge cuma jalan sekali, hasilnya disimpan, dan pas retry cuma langkah kirim resi yang diulang.

@inngestclient.createfunction(

fnid="process-order",

trigger=inngest.TriggerEvent(event="shop/order.created"),

)

async def processorder(ctx: inngest.Context) -> dict:

orderid = ctx.event.data["orderid"]

# Step 1: ambil detail order dari database

order = await ctx.step.run(

"fetch-order",

lambda: getorderfromdb(orderid),

)

# Step 2: charge pembayaran. Kalau step 3 gagal, ini TIDAK diulang.

charge = await ctx.step.run(

"charge-payment",

lambda: chargecard(order["customerid"], order["total"]),

)

# Step 3: kirim email resi

await ctx.step.run(

"send-receipt",

lambda: sendemail(order["email"], f"Resi #{charge['id']}"),

)

return {"orderid": orderid, "chargeid": charge["id"]}

Aturan penting yang wajib kamu inget: semua yang punya side effect (panggil API, tulis database, kirim email) harus dibungkus dalam step.run. Kenapa? Karena kode di luar step bisa jalan berkali-kali tiap function di-invoke ulang oleh Inngest. Cuma kode di dalam step yang dijamin jalan sekali dan hasilnya disimpan. Argumen pertama step.run itu ID step yang harus unik di dalam function, dan argumen kedua itu function yang mau dijalanin.

Kalau step lempar exception, Inngest otomatis retry step itu dengan exponential backoff. Default-nya retry sampai 4 kali. Kamu bisa atur jumlah retry di level function:

@inngestclient.createfunction(

fnid="process-order",

trigger=inngest.TriggerEvent(event="shop/order.created"),

retries=2, # retry maksimal 2 kali per step

)

async def processorder(ctx: inngest.Context) -> dict:

...

step.sleep: Nunda Tanpa Nahan Proses

Kadang kamu mau nunda sesuatu. Misalnya kirim email follow-up tiga hari setelah user daftar. Dengan Inngest kamu tinggal pakai step.sleep, dan yang ajaib adalah ini nggak nahan proses atau resource apa pun selama itu.

import datetime

@inngestclient.createfunction(

fnid="onboarding-drip",

trigger=inngest.TriggerEvent(event="app/user.signup"),

)

async def onboardingdrip(ctx: inngest.Context) -> None:

email = ctx.event.data["email"]

# Kirim welcome email langsung

await ctx.step.run("welcome", lambda: sendemail(email, "Selamat datang!"))

# Tidur 3 hari. Function berhenti, state disimpan Inngest.

await ctx.step.sleep("wait-3-days", datetime.timedelta(days=3))

# 3 hari kemudian, Inngest bangunin function dari sini

await ctx.step.run("tips", lambda: sendemail(email, "Tips pakai produk kami"))

# Tidur seminggu lagi

await ctx.step.sleep("wait-1-week", datetime.timedelta(weeks=1))

await ctx.step.run("checkin", lambda: sendemail(email, "Gimana kabarnya?"))

Waktu function nyampe step.sleep, Inngest nyimpen posisi eksekusi dan berhenti. Nggak ada thread yang nunggu, nggak ada koneksi yang kebuka. Pas waktunya tiba, Inngest manggil lagi endpoint kamu dan function-nya lanjut dari titik yang tepat. Jadi kamu bisa bikin flow yang jalan berhari-hari atau berminggu-minggu tanpa mikirin cron atau scheduler.

Selain step.sleep yang pakai durasi, ada juga step.sleepuntil kalau kamu mau tidur sampai waktu absolut tertentu:

target = datetime.datetime(2026, 12, 25, 9, 0, 0)

await ctx.step.sleepuntil("wait-until-christmas", target)

step.waitforevent: Nunggu Event Lain

Ini fitur yang aku suka banget. Kadang alur kerja kamu perlu nunggu sesuatu terjadi di tempat lain. Misalnya, setelah kirim invoice kamu mau nunggu event pembayaran. Kalau dalam 24 jam belum dibayar, kirim reminder. step.waitforevent bikin ini gampang.

@inngestclient.createfunction(

fnid="invoice-flow",

trigger=inngest.TriggerEvent(event="billing/invoice.sent"),

)

async def invoiceflow(ctx: inngest.Context) -> str:

invoiceid = ctx.event.data["invoiceid"]

# Tunggu event pembayaran, maksimal 24 jam

payment = await ctx.step.waitforevent(

"wait-for-payment",

event="billing/invoice.paid",

timeout=datetime.timedelta(hours=24),

# cocokan cuma event dengan invoiceid yang sama

ifexp=f"async.data.invoiceid == '{invoiceid}'",

)

if payment is None:

# timeout: nggak ada pembayaran dalam 24 jam

await ctx.step.run(

"send-reminder",

lambda: sendemail("reminder pembayaran"),

)

return "reminder terkirim"

# pembayaran diterima

await ctx.step.run("send-thanks", lambda: sendemail("makasih udah bayar"))

return "pembayaran diterima"

Beberapa hal penting soal waitforevent. Parameter event itu nama event yang ditunggu. timeout itu berapa lama mau nunggu sebelum nyerah. Kalau timeout tercapai tanpa event, return value-nya None, jadi kamu wajib cek itu. Parameter ifexp itu ekspresi buat matching, biar function ini nunggu event yang bener, bukan sembarang event pembayaran. Di contoh di atas, kita cuma mau event pembayaran yang invoiceid-nya cocok sama invoice yang kita kirim.

step.sendevent: Kirim Event dari Dalam Function

Kamu juga bisa kirim event dari dalam function pakai step.sendevent. Ini berguna buat bikin function saling nge-trigger, bikin arsitektur event-driven yang beneran loosely coupled. Bedanya sama inngestclient.send biasa, step.sendevent itu durable, jadi aman dari duplikasi waktu retry.

@inngestclient.createfunction(

fnid="handle-signup",

trigger=inngest.TriggerEvent(event="app/user.signup"),

)

async def handlesignup(ctx: inngest.Context) -> None:

userid = ctx.event.data["userid"]

await ctx.step.run("create-profile", lambda: createprofile(userid))

# Trigger function lain lewat event

await ctx.step.sendevent(

"emit-profile-created",

inngest.Event(

name="app/profile.created",

data={"userid": userid},

),

)

Flow Control: Concurrency, Throttle, dan Rate Limit

Salah satu kelebihan Inngest adalah flow control yang tinggal dikonfig lewat decorator. Misalnya kamu manggil API eksternal yang cuma boleh 5 request bareng, kamu bisa batasi concurrency:

@inngestclient.createfunction(

fnid="call-external-api",

trigger=inngest.TriggerEvent(event="api/task.queued"),

concurrency=[

inngest.Concurrency(limit=5), # maks 5 run bareng

],

throttle=inngest.Throttle(

limit=100,

period=datetime.timedelta(minutes=1), # maks 100 per menit

),

)

async def callexternalapi(ctx: inngest.Context) -> None:

await ctx.step.run("call", lambda: hitexternalapi(ctx.event.data))

Dengan gini kamu nggak perlu bikin semaphore atau rate limiter sendiri. Inngest yang ngatur antrean dan pacing-nya buat kamu.

Menjalankan Dev Server

Selama development, kamu butuh Inngest Dev Server. Dev server ini simulasi Inngest Cloud tapi jalan lokal di laptop kamu, komplit dengan UI buat lihat event dan run function. Cara jalaninnya gampang, cukup pakai npx (butuh Node.js):

npx inngest-cli@latest dev

Secara default dev server jalan di http://localhost:8288. Buka URL itu di browser, kamu bakal lihat dashboard yang nunjukin semua function yang terdaftar, event yang masuk, dan history run tiap function lengkap dengan detail tiap step.

Alur development-nya kayak gini:

  • Jalanin aplikasi kamu (misalnya uvicorn main:app --reload --port 8000).
  • Jalanin dev server (npx inngest-cli@latest dev).
  • Dev server otomatis nyari endpoint /api/inngest di aplikasimu dan register semua function.
  • Kirim event, entah lewat kode aplikasimu atau langsung dari UI dev server (ada tombol buat kirim event test).
  • Lihat run-nya jalan real-time di dashboard, cek output tiap step, dan kalau ada yang gagal kamu bisa replay run-nya.
  • Kalau dev server nggak otomatis nemuin aplikasimu, kamu bisa kasih tahu URL-nya secara eksplisit:

    npx inngest-cli@latest dev -u http://localhost:8000/api/inngest
    

    Fitur replay di UI ini juga berguna banget. Kalau ada function yang gagal, kamu bisa lihat step mana yang error, betulin kodenya, terus replay run yang sama tanpa harus bikin ulang event dari awal. Ini bikin debugging workflow yang kompleks jadi jauh lebih cepat.

    Best Practices

    Setelah pakai Inngest buat beberapa proyek, ada beberapa pelajaran yang mau aku bagi biar kamu nggak kejeblos di lubang yang sama.

    Bungkus semua side effect dalam step. Ini aturan nomor satu. Kode di luar step.run bisa jalan berkali-kali tiap function di-invoke ulang. Jadi kalau kamu panggil API atau tulis database di luar step, kamu berisiko dobel. Selalu bungkus dalam step. Bikin step ID yang unik dan deskriptif. ID step dipakai Inngest buat nyimpen dan ambil hasil. Kalau dua step punya ID sama, hasilnya bisa ketuker. Pakai ID yang jelas kayak charge-payment, send-welcome-email, bukan step1, step2. Usahakan step kamu idempotent. Walau Inngest jamin step jalan sekali di kondisi normal, ada kasus ekstrem (misalnya server mati tepat setelah step selesai tapi sebelum hasilnya kesimpan) di mana step bisa jalan dua kali. Jadi bikin operasimu idempotent, misalnya pakai idempotency key waktu charge pembayaran. Pecah kerjaan jadi step yang masuk akal. Jangan bikin satu step raksasa yang ngerjain segalanya. Pecah jadi unit yang logis biar retry-nya granular dan kamu bisa lihat progres di dashboard. Tapi juga jangan kebanyakan step mikro yang bikin ribet. Selalu cek hasil waitforevent. Kalau timeout tercapai, waitforevent balik None. Lupa cek ini adalah sumber bug umum. Selalu tangani kasus timeout secara eksplisit. Jangan simpan data gede sebagai return step. Hasil step disimpan Inngest, jadi kalau kamu return objek raksasa, itu boros. Simpan referensinya aja (misalnya ID atau URL) dan ambil data aslinya waktu dibutuhkan. Pisahin data event dari logika. Event data harus berisi info secukupnya buat trigger kerjaan, bukan seluruh state aplikasimu. Kirim ID, terus ambil detail lengkap di dalam step. Pakai flow control buat lindungi resource downstream. Kalau function kamu manggil API pihak ketiga yang punya rate limit, pakai throttle atau concurrency biar kamu nggak kena banned. Ini jauh lebih rapi dibanding bikin rate limiter sendiri.

    Kesimpulan

    Temen-temen, Inngest itu solusi yang elegan banget buat masalah yang selama ini bikin pusing, yaitu bangun proses background yang reliable tanpa harus ngurus queue infrastructure sendiri. Konsep intinya cuma tiga: event nge-trigger function, function dipecah jadi steps, dan tiap step durable dengan retry independen. Dari tiga konsep sederhana ini kamu bisa bangun flow yang kompleks, mulai dari onboarding drip berhari-hari, alur pembayaran yang nunggu konfirmasi, sampai pipeline pemrosesan yang tahan banting.

    Yang aku suka dari Inngest adalah kodenya kelihatan lurus dari atas ke bawah, kayak nulis fungsi biasa, tapi di baliknya dia durable dan bisa selamat dari server restart. Kamu nggak perlu lagi mikirin tabel state, cron job, atau dead letter queue. Semua itu diurus Inngest. Ditambah dev server lokal yang enak buat testing dan debugging, developer experience-nya bener-bener terasa.

    Saranku, coba mulai dari yang kecil. Bikin satu function sederhana yang di-trigger event, tambahin step.run buat side effect-nya, terus jalanin dev server dan lihat gimana run-nya di dashboard. Setelah kamu paham pola dasarnya, tambahin step.sleep dan step.waitforevent buat bikin flow yang lebih hidup. Dari situ kamu bakal ngerasain sendiri betapa banyak boilerplate yang selama ini kamu tulis sekarang nggak perlu lagi. Selamat nyoba, dan semoga tutorial ini membantu ya temen-temen!

    Artikel Terkait

    PaddleOCR: Ekstraksi Teks dari Gambar dan Dokumen dengan Akurasi Tinggi

    PaddleOCR: Ekstraksi Teks dari Gambar dan Dokumen dengan Akurasi Tinggi Halo temen-temen, kali ini kita bahas salah satu...

    DSPy: Berhenti Ngoprek Prompt Manual, Biarkan Compiler yang Optimasi

    DSPy: Berhenti Ngoprek Prompt Manual, Biarkan Compiler yang Optimasi Halo temen-temen, kali ini aku mau ngenalin satu li...

    Hamilton: Bikin Dataflow dan Pipeline ML yang Rapi dan Gampang Dirawat di Python

    Hamilton: Bikin Dataflow dan Pipeline ML yang Rapi dan Gampang Dirawat di Python Halo temen-temen, ketemu lagi sama aku ...

    Inspect AI: Framework Evaluasi LLM dari UK AI Safety Institute

    Inspect AI: Framework Evaluasi LLM dari UK AI Safety Institute yang Wajib Kamu Coba Temen-temen, kalau kamu udah mulai s...