Pengirim webhook menunggu hitungan detik, bukan menit: Slack memberi handler Anda 3 detik untuk menjawab, Shopify 5, GitHub 10, sedangkan handler yang memanggil LLM biasanya butuh lebih lama, sehingga pengirim mencatat kegagalan dan mencoba ulang (retry) atau menyerah; jika batas waktu terlewat cukup sering, Slack mematikan langganan event Anda, dan Shopify dapat menghapus webhook tersebut. Solusinya adalah menjawab 200 begitu permintaan terverifikasi dan memanggil model setelah respons terkirim, entah lewat pekerjaan latar belakang yang Anda jalankan sendiri atau di balik relay yang menjawab pengirim untuk Anda.
Sisa artikel ini adalah versi panjang dari dua kalimat itu: berapa lama setiap pengirim benar-benar menunggu, apa yang dilakukannya saat Anda terlambat, empat cara singkat untuk menjawab dulu dan mengerjakannya kemudian (beserta kelemahan masing-masing), dan apa yang berubah bila relay yang menyimpan event-nya.
TL;DR
- Waktu tunggu pengirim mencakup seluruh handler Anda: pemeriksaan tanda tangan (signature), pembacaan database, dan panggilan model
- Lewat dari batas itu, pengirim mencoba ulang. Handler yang sudah mengerjakan tugasnya akan mengerjakannya lagi: dua balasan di Slack, satu pesanan diberi tag dua kali, dua email
- Jika cukup sering terlambat, Slack menonaktifkan event subscription Anda, Shopify bisa menghapus webhook-nya. GitHub sama sekali tidak pernah mencoba ulang
- Jawab dulu, kerjakan kemudian:
after()di Next.js,ctx.waitUntil()di Cloudflare (paling lama 30 detik), tugas latar belakang di Express atau FastAPI - Tidak satu pun dari cara itu menyimpan event jika pekerjaan latar belakangnya mati. Versi yang persisten butuh penyimpanan, worker, dan jadwal percobaan ulang, yang tidak lain adalah antrean
- Relay adalah antrean yang tidak perlu Anda tulis: AnyHook menjawab pengirim dalam sekitar satu detik dan memberi handler Anda 60 detik di paket Free, dalam batas jatah komputasi (compute allowance) paket tersebut, dengan percobaan ulang
Berapa lama setiap pengirim menunggu
Data ini diambil dari dokumentasi masing-masing vendor dan kami kumpulkan beserta sumbernya di referensi provider webhook kami.
| Pengirim | Menunggu 2xx dari Anda | Setelah pengiriman gagal |
|---|---|---|
| Slack | 3 detik | 3 percobaan ulang: segera, setelah 1 menit, setelah 5 menit. Event subscription dinonaktifkan jika lebih dari 95% upaya gagal dalam 60 menit |
| LINE | 2 detik (jawaban yang lebih lambat dicatat sebagai request_timeout) | Pengiriman ulang nonaktif secara default. LINE dapat menangguhkan pengiriman webhook jika server Anda terus gagal |
| Shopify | 5 detik (1 detik untuk terhubung) | 8 percobaan ulang dalam 4 jam. Subscription yang dibuat lewat Admin API dihapus setelah 8 kegagalan berturut-turut |
| HubSpot | 5 detik, untuk seluruh batch berisi hingga 100 notifikasi | Hingga 10 upaya yang tersebar dalam 24 jam |
| Paddle | 5 detik | Mode live: hingga 60 upaya dalam sekitar 2 hari |
| GitHub | 10 detik | Tidak ada. Anda dapat mengirim ulang secara manual selama 3 hari |
| Twilio | 15 detik secara default, dapat diatur | 1 percobaan ulang secara default, dapat diatur dari 0 sampai 5 |
| Stripe | Tidak dipublikasikan. Dokumentasinya meminta Anda mengembalikan 2xx sebelum logika yang lambat | Backoff eksponensial hingga 3 hari dalam mode live |
Batas waktu Anda yang sebenarnya adalah milik pengirim paling ketat yang mengirim ke Anda. Bot di Slack punya 3 detik, dan cold start bisa menghabiskan sebagian besar waktu itu sebelum baris pertama kode Anda berjalan.
Mengapa panggilan LLM membuatnya gagal
Panggilan model adalah langkah yang lambat di hampir setiap handler yang ditulis pada 2026. Completion singkat butuh beberapa detik; completion dengan pemanggilan tool, jawaban panjang, atau percobaan ulang di dalam SDK Anda butuh puluhan detik. Tambahkan cold start, dan pengirim sudah memutus koneksi sebelum token pertama tiba.
Apa yang terjadi selanjutnya tergantung pengirimnya, dan tidak ada satu pun yang baik:
- Slack mencoba ulang. Handler Anda menerima event yang sama lagi saat eksekusi pertama masih menunggu model. Kedua eksekusi selesai dan keduanya mengirim pesan. Pengguna melihat bot menjawab dua kali
- Shopify mencoba ulang, lalu menghapus. Delapan kegagalan berturut-turut dalam empat jam, dan subscription yang dibuat lewat Admin API hilang. Pesanan terus masuk, tetapi aplikasi Anda tidak lagi mendapat kabar tentangnya
- GitHub tidak melakukan apa-apa. Pengiriman ditandai gagal dan berhenti di situ, kecuali ada yang mengirimnya ulang secara manual dalam tiga hari
- Pengiriman ulang di LINE nonaktif kecuali Anda menyalakannya, jadi satu timeout berarti satu pesan yang tidak pernah dijawab bot Anda
Handler itu tidak gagal. Ia hanya lebih lambat daripada jam yang tidak dapat dilihatnya.
Solusinya: jawab dulu, kerjakan kemudian
Verifikasi permintaan, jawab 200, lalu panggil model. Setiap platform punya cara untuk tetap berjalan setelah respons dikirim. Masing-masing juga punya kelemahan, yang dijelaskan di bawah kodenya.
Next.js (App Router)
// app/api/slack/events/route.ts
import { after } from "next/server";
export async function POST(req: Request) {
const raw = await req.text();
if (!verifySlackSignature(raw, req.headers)) {
return new Response("bad signature", { status: 401 });
}
const body = JSON.parse(raw);
if (body.type === "url_verification") {
return Response.json({ challenge: body.challenge });
}
after(async () => {
const answer = await askModel(body.event.text); // the slow part
await postToSlack(body.event.channel, answer);
});
return new Response(null, { status: 200 }); // well inside 3 seconds
}
Kelemahannya: after() berjalan selama durasi maksimum route tersebut dan tidak lebih. Jika panggilan model melempar error, atau instance-nya didaur ulang, event itu hilang. Slack sudah menerima respons 200 dan tidak akan mengirimnya lagi, dan tidak ada yang mencatat bahwa event itu pernah ada.
Cloudflare Workers
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext) {
const raw = await req.text();
if (!(await verify(raw, req.headers, env))) {
return new Response("bad signature", { status: 401 });
}
ctx.waitUntil(handle(JSON.parse(raw), env)); // runs after the response
return new Response(null, { status: 200 });
},
};
Kelemahannya: waitUntil mendapat paling lama 30 detik setelah respons, dibagi untuk semua yang Anda serahkan kepadanya, lalu dibatalkan. Panggilan model yang lambat tidak muat di sana. Jawaban Cloudflare sendiri untuk pekerjaan yang lebih panjang adalah Queue dengan consumer Worker, dan itu jawaban yang tepat. Panduan Workers kami membahas jebakan lain di platform ini.
Express atau server Node lain yang berjalan terus-menerus
app.post("/webhooks/slack", express.raw({ type: "application/json" }), (req, res) => {
if (!verifySlackSignature(req.body, req.headers)) return res.sendStatus(401);
res.sendStatus(200); // answer first
setImmediate(() => handle(JSON.parse(req.body)).catch(console.error));
});
Kelemahannya: cara ini aman sampai prosesnya restart. Deploy, crash, atau proses yang dimatikan karena kehabisan memori di tengah panggilan akan menghilangkan apa pun yang sedang berjalan. Di host serverless, proses juga bisa dibekukan begitu respons terkirim.
Python (FastAPI)
from fastapi import BackgroundTasks, FastAPI, Request, Response
app = FastAPI()
@app.post("/webhooks/line")
async def line_webhook(request: Request, background: BackgroundTasks):
raw = await request.body()
if not verify_line_signature(raw, request.headers.get("x-line-signature")):
return Response(status_code=401)
background.add_task(handle_events, raw) # runs after the response is sent
return Response(status_code=200)
Kelemahannya sama dengan Express: tugas itu hidup di dalam proses. Restart prosesnya, dan tugas itu hilang, tanpa percobaan ulang dan tanpa catatan.
Kesamaan keempatnya
Masing-masing menjawab 200 untuk event yang, pada saat itu, hanya ada di memori. Pengirim tidak peduli lagi, dan jika pekerjaan latar belakang Anda gagal, tidak ada kesempatan kedua dan tidak ada yang bisa diperiksa sesudahnya.
Versi yang persisten punya bentuk yang sudah dikenal: catat event sebelum Anda menjawab, lalu biarkan worker memprosesnya dan mencobanya ulang saat gagal, dan simpan catatan yang cukup untuk melihat apa yang gagal lalu mengirimnya lagi. Itulah antrean dengan daftar dead-letter dan tombol putar ulang (replay). Membangunnya tidak sulit. Namun itu satu komponen lagi yang harus dijalankan, dipantau, dan bisa membangunkan Anda saat giliran on-call, padahal bot Anda tidak dibuat untuk itu. Panduan Vercel kami menunjukkan versi simpan-dulu-baru-jawab jika Anda ingin mengelolanya sendiri.
Jika Anda tidak ingin menjalankan antrean
Pasang relay di depan handler. Anda hanya mengganti URL webhook di sisi pengirim menjadi URL AnyHook Anda, tidak lebih.
- Pengirim mendapat respons 200 dalam sekitar satu detik. Event disimpan sebelum respons 200 itu dikirim
- Setelah itu handler Anda menerima event dengan batas waktu pengiriman 60 detik di paket Free, 120 detik di Pro, dan 5 menit di Scale, dalam batas jatah komputasi paket Anda. Panggilan model muat di dalamnya, jadi handler dapat bekerja langsung di dalam permintaan itu dan menjawab setelah selesai
- Jika handler Anda gagal atau kehabisan waktu, AnyHook mencoba ulang dengan jadwalnya sendiri: 5 menit, 30 menit, 3 jam, 18 jam, lalu setiap hari, hingga 3 upaya di Free, 5 di Pro, dan 10 di Scale
- Setiap event disimpan di log yang bisa dicari (3 hari di Free, lebih lama di paket berbayar), dan event mana pun dapat diputar ulang setelah Anda memperbaiki bug
- AnyHook menjawab challenge
url_verificationdari Slack untuk Anda, jadi URL-nya berhasil disimpan pada percobaan pertama
Pengiriman bersifat setidaknya sekali (at-least-once). Handler yang sudah menyelesaikan tugasnya tetapi menjawab setelah batas waktu habis akan menerima event itu lagi, jadi buat handler Anda idempoten: lewati setiap event dengan header X-AnyHook-Delivery (ID event, sama di setiap percobaan ulang) yang sudah pernah Anda tangani. ID milik pengirim sendiri, seperti event_id dari Slack atau X-GitHub-Delivery dari GitHub, juga ikut diteruskan. Polanya ada di Event duplikat dan idempotensi.
Yang tidak bisa dilakukan relay adalah menjawab untuk Anda ketika pengirim membutuhkan body respons yang sebenarnya:
- Slash command dan tombol interaktif Slack mengharapkan balasan Anda di dalam respons atau konfirmasi cepat. Jawab dari endpoint Anda sendiri dengan 200 kosong, lalu kirim jawaban sebenarnya ke
response_urlyang dikirim Slack, yang menerima hingga lima pesan dalam 30 menit - Interaksi Discord membutuhkan respons awal dalam 3 detik. Balas dengan deferred response (tipe 5), lalu edit dengan jawaban model saat jawaban itu tiba; token interaksi tetap berlaku selama 15 menit
- Webhook suara dan pesan Twilio mengharapkan TwiML sebagai balasan. Jawab segera dengan TwiML, lalu kirim jawaban yang lambat lewat REST API
Relay dibuat untuk webhook event, jenis yang hanya butuh 2xx: Events API Slack, LINE, Shopify, GitHub, HubSpot, Stripe, dan sebagian besar lainnya.
Periksa waktu handler Anda
Anda tidak perlu menebak di sisi mana garis batas handler Anda berada. Pemeriksa timeout webhook mengirim permintaan uji ke endpoint Anda, mengukur waktunya, dan menempatkan hasilnya di samping batas waktu setiap pengirim. Alat ini berjalan dua kali, jadi permintaan pertama yang lambat setelah lama sepi akan terlihat apa adanya: cold start.
FAQ
Mengapa Slack bilang bot saya timeout, atau mengapa bot membalas dua kali?
Slack menunggu 3 detik untuk respons 2xx. Jika handler Anda memanggil model sebelum menjawab, prosesnya lebih lama, Slack menghitungnya sebagai kegagalan dan mengirim event itu lagi, lalu kedua eksekusi mengirim balasan. Jawab dulu, kirim balasan sesudahnya dengan chat.postMessage, dan buang event yang sudah pernah Anda lihat.
Bisakah saya mengembalikan 200 saja lalu terus bekerja di Vercel?
Bisa, dengan after() dari next/server (atau waitUntil), dan pekerjaan itu berjalan selama durasi maksimum route. Cara ini tidak persisten: jika pekerjaan gagal atau instance didaur ulang, event hilang dan pengirim tidak akan mengirimnya ulang. Cukup untuk notifikasi, salah untuk apa pun yang hilangnya akan disadari pengguna.
Berapa lama LINE menunggu respons webhook?
Dua detik. LINE mencatat webhook yang tidak dijawab server bot Anda dalam 2 detik sebagai request_timeout di statistik error-nya, dan menyarankan agar pekerjaan dilakukan secara asinkron. Pengiriman ulang webhook nonaktif secara default; aktifkan di pengaturan Messaging API channel Anda.
Apakah memasang antrean di depan membuat balasan bot saya lebih lambat? Tidak. Relay meneruskan event begitu event itu tersimpan, jadi handler Anda mulai berjalan kira-kira pada saat yang sama seperti tanpa relay. Yang berubah adalah pengirim tidak lagi menahan koneksi selama model berpikir. Percobaan ulang hanya menambah waktu ketika sebuah pengiriman gagal, dan tanpa percobaan ulang, event itu akan hilang.
Bagaimana jika handler saya butuh lebih dari 60 detik? Di Free, AnyHook menunggu jawaban handler Anda selama 60 detik; Pro menunggu 120 detik dan Scale 5 menit, dalam batas jatah komputasi paket Anda. Untuk pekerjaan yang lebih lama dari itu, jawab 202 begitu job sudah masuk antrean, lalu selesaikan di sisi Anda. Relay menghitung 202 sebagai terkirim.
Apakah ini berlaku untuk bot Discord dan slash command Slack?
Tidak lewat relay. Keduanya membutuhkan body respons tertentu dalam 3 detik, dan hanya kode Anda sendiri yang bisa memberikannya. Gunakan deferred response Discord dan response_url Slack seperti dijelaskan di atas. Events API Slack, yang hanya membutuhkan 2xx, bisa berjalan di balik relay.
Apakah handler saya bisa menerima event yang sama dua kali? Bisa, dengan atau tanpa relay: pengirim mencoba ulang, begitu pula AnyHook saat handler Anda gagal. Rancang setiap handler untuk pengiriman setidaknya sekali dan lewati ID yang sudah pernah Anda tangani.