รับ Webhook 200 ครั้งต่อนาทีจากผู้ให้บริการภายนอก แต่มั่นใจได้ยังไงว่าไม่ประมวลผลซ้ำ

สรุปสั้น ๆ
Webhook Receiver บน Cloudflare Workers รับ request ได้เร็วเพราะกระจายอยู่บน edge network แต่ต้องออกแบบสามเรื่องให้ครบตั้งแต่ต้น คือตรวจ signature ทุก request ก่อนเชื่อ payload, จัดการ retry จากผู้ให้บริการภายนอกด้วย idempotency key ไม่ให้ประมวลผลซ้ำ และตอบ response กลับให้เร็วพอโดยแยกงานหนักไปทำ background แทนการทำทุกอย่างในเส้นทาง request เดียว
ระบบที่ต้องรับ webhook จากผู้ให้บริการภายนอก เช่น payment gateway หรือ SaaS ที่ยิง event เข้ามาเป็นร้อยครั้งต่อนาทีในช่วงพีค มักเริ่มต้นด้วยโค้ดง่าย ๆ รับ POST request แล้วเขียนลง database ตรง ๆ ซึ่งใช้งานได้ในช่วงทดสอบ แต่พอเข้า production จริงจะเจอปัญหาที่โค้ดง่าย ๆ แบบนั้นรับมือไม่ไหว
ปัญหาหลักสามเรื่องที่ตามมาคือ ผู้ให้บริการภายนอกส่ง event ซ้ำเมื่อไม่ได้รับ response ตรงเวลา ทำให้ต้องมีกลไกกันประมวลผลซ้ำ, ต้องตรวจสอบว่า payload ที่เข้ามาจริงมาจากผู้ส่งที่ถูกต้องผ่าน signature ไม่ใช่ใครก็ได้ที่รู้ URL endpoint และต้องตอบ response กลับให้เร็วพอ ไม่งั้นผู้ส่งจะตีความว่า request ล้มเหลวแล้วส่งซ้ำเข้ามาอีก
บทความนี้จะเจาะทั้งสามเรื่องนี้บน Cloudflare Workers โดยเฉพาะ เพราะ Workers มีข้อจำกัดเรื่อง execution time และไม่มี long-running process แบบ server ทั่วไป ทำให้บางแนวทางที่ใช้ได้บน Node server ต้องปรับก่อนนำมาใช้บน edge runtime
โครงสร้างพื้นฐานของ Webhook Receiver บน Cloudflare Workers
Webhook receiver ที่ดีไม่ใช่แค่ route เดียวที่รับ POST แล้วเขียนข้อมูล แต่ควรแยกเป็นสามชั้นทำงานต่อกัน ชั้นแรกคือรับ request แล้วตรวจสอบเบื้องต้นให้เร็วที่สุด เช่นตรวจ signature และ format ของ payload ชั้นที่สองคือบันทึกว่า event นี้เคยได้รับมาก่อนหรือยัง เพื่อกันประมวลผลซ้ำ ชั้นที่สามคือส่ง event ไปประมวลผลจริง ซึ่งอาจทำทันทีถ้าเบาพอ หรือส่งเข้า queue ถ้าเป็นงานที่ใช้เวลานาน
การแยกชั้นแบบนี้สำคัญเพราะ Workers มีเพดานเวลาทำงานต่อ request ที่จำกัดกว่า long-running server ถ้าเขียน logic ทั้งหมดไว้ใน handler เดียวโดยไม่แยกงานหนักออกไป เมื่อมี event ที่ต้องประมวลผลนาน เช่นเรียก API ภายนอกหลายตัวต่อกัน อาจทำให้ Worker ทำงานไม่ทันเพดานเวลาและ request ล้มเหลวทั้งที่ payload ถูกต้องทุกอย่าง
รูปแบบที่แนะนำคือให้ Worker ตัวรับ webhook ทำหน้าที่แค่ตรวจสอบและบันทึกว่าเคยได้รับ event นี้หรือยังให้เร็วที่สุด แล้วส่งต่อการประมวลผลจริงไปที่ Cloudflare Queues หรือเรียก Worker อีกตัวแบบ asynchronous แทน ทำให้ response กลับไปหาผู้ส่งได้เร็ว และมีพื้นที่จัดการงานหนักแยกออกจากเส้นทางหลัก
ตรวจ Signature ก่อนเชื่อ Payload ทุกครั้ง ทำยังไงให้ปลอดภัยจริง
ผู้ให้บริการที่ส่ง webhook ส่วนใหญ่แนบ signature มากับ header ของ request ซึ่งคำนวณจาก payload กับ secret key ที่ทั้งสองฝั่งรู้ร่วมกัน โดยใช้ HMAC เป็นอัลกอริทึมหลัก หน้าที่ของ Worker คือคำนวณ signature จาก payload ที่ได้รับด้วย secret เดียวกัน แล้วเทียบว่าตรงกับ signature ที่แนบมาหรือไม่ ก่อนที่จะเชื่อว่า request นี้มาจากผู้ส่งจริง
Web Crypto API ที่ Workers รองรับเต็มรูปแบบอยู่แล้วสามารถคำนวณ HMAC ได้โดยไม่ต้องพึ่ง library ภายนอก ซึ่งเข้ากับธรรมชาติของ Workers ที่เน้น Web API มาตรฐาน จุดที่ต้องระวังคือการเทียบ signature ต้องใช้วิธีเทียบแบบ constant-time ไม่ใช่เทียบ string ตรง ๆ ด้วย operator เปรียบเทียบทั่วไป เพราะการเทียบแบบธรรมดาเปิดช่องให้โจมตีแบบ timing attack ได้ในทางทฤษฎี แม้จะใช้ประโยชน์ได้ยากในทางปฏิบัติ แต่ก็ควรทำให้ถูกตั้งแต่ต้น
อีกจุดที่มักถูกมองข้ามคือต้องอ่าน raw body ของ request มาคำนวณ signature ไม่ใช่ parse เป็น JSON ก่อนแล้วค่อยคำนวณ เพราะการแปลง object กลับเป็น string อาจได้ format ที่ต่างจาก payload ต้นฉบับเล็กน้อย เช่นลำดับ key หรือช่องว่าง ทำให้ signature ที่คำนวณได้ไม่ตรงกับที่ผู้ส่งคำนวณมา ควรอ่าน body เป็น text ดิบมาคำนวณ signature ก่อน แล้วค่อย parse เป็น JSON หลังจากยืนยันว่า signature ถูกต้องแล้วเท่านั้น
จัดการ Retry จากผู้ให้บริการภายนอกโดยไม่ประมวลผลซ้ำ
ผู้ให้บริการ webhook ส่วนใหญ่มีนโยบาย retry เมื่อไม่ได้รับ response ที่ถือว่าสำเร็จ เช่นไม่ได้ status code 2xx กลับไปภายในเวลาที่กำหนด บาง provider ส่ง retry ซ้ำหลายรอบในระยะเวลาสั้น ๆ ถ้า Worker ประมวลผล event เดิมซ้ำทุกครั้งที่ได้รับ retry จะทำให้เกิดผลข้างเคียงซ้ำ เช่นบันทึก transaction ซ้ำหรือส่ง notification ซ้ำหลายครั้งให้ผู้ใช้ปลายทาง
วิธีป้องกันคือทุก event ต้องมี identifier เฉพาะตัวที่ provider แนบมาให้ (มักอยู่ใน header หรือ field ของ payload) แล้ว Worker ต้องตรวจสอบก่อนประมวลผลว่า identifier นี้เคยถูกบันทึกว่าประมวลผลสำเร็จแล้วหรือยัง ถ้าเคยแล้วให้ตอบ response สำเร็จกลับไปทันทีโดยไม่ทำงานซ้ำ ไม่ใช่ปฏิเสธ request เพราะผู้ส่งจะตีความการปฏิเสธเป็นความล้มเหลวแล้ว retry ต่อไปเรื่อย ๆ
การเก็บสถานะว่า event ไหนประมวลผลไปแล้วบ้าง สามารถใช้ Cloudflare KV เก็บแบบ key-value ง่าย ๆ โดยตั้ง key เป็น identifier ของ event และตั้ง expiration ให้เหมาะกับช่วงเวลาที่ provider มักจะ retry ซ้ำ (เช่นไม่กี่วัน) เพื่อไม่ให้ข้อมูลค้างอยู่ถาวรโดยไม่จำเป็น ถ้าต้องการเก็บประวัติ event ระยะยาวเพื่อ audit ควรส่งต่อไปเก็บใน database แยกต่างหาก เช่นผ่านแนวทางที่อธิบายไว้ในบทความ Cloudflare Workers เชื่อม Supabase Postgres
- อ่าน identifier เฉพาะตัวของ event จาก header หรือ payload ก่อนทำอย่างอื่น
- ตรวจสอบใน KV หรือ storage อื่นว่า identifier นี้เคยถูกบันทึกว่าประมวลผลสำเร็จแล้วหรือไม่
- ถ้าเคยแล้ว ให้ตอบ response สำเร็จกลับทันทีโดยไม่ทำงานซ้ำ
- ถ้ายังไม่เคย ให้บันทึก identifier พร้อมสถานะ 'กำลังประมวลผล' ก่อนเริ่มทำงานจริง เพื่อกัน race condition จาก retry ที่มาซ้อนกันในเวลาใกล้เคียงกัน
- หลังประมวลผลสำเร็จ อัปเดตสถานะเป็น 'สำเร็จแล้ว' พร้อมตั้ง expiration ให้เหมาะสม
ตอบ Response เร็วแค่ไหนถึงจะปลอดภัยจากผู้ส่ง Webhook
ผู้ให้บริการแต่ละรายมีเพดานเวลารอ response ก่อนถือว่า request ล้มเหลวไม่เท่ากัน บางรายรอไม่กี่วินาที บางรายรอนานกว่านั้น แต่หลักการทั่วไปคือควรตอบ response กลับให้เร็วที่สุดเท่าที่จะทำได้ โดยทำเฉพาะงานที่จำเป็นต้องเสร็จก่อนตอบ เช่นตรวจ signature และบันทึกว่าได้รับ event แล้ว ส่วนงานที่ไม่จำเป็นต้องเสร็จก่อนตอบ เช่นส่ง notification หรือเรียก API ภายนอกอื่นต่อ ควรแยกไปทำหลังตอบ response แล้ว
Cloudflare Workers มี API สำหรับสั่งงานที่ทำต่อหลังตอบ response ไปแล้วโดยไม่ทำให้ Worker ปิดตัวก่อนงานเสร็จ ทำให้สามารถตอบ 200 กลับไปหาผู้ส่งได้ทันทีที่ตรวจสอบและบันทึก event เสร็จ แล้วค่อยทำงานที่เหลือต่อในพื้นหลังโดยผู้ส่ง webhook ไม่ต้องรอ ซึ่งช่วยลดโอกาสที่ผู้ส่งจะตีความว่า request ช้าเกินไปแล้วส่ง retry ซ้ำเข้ามาโดยไม่จำเป็น
ข้อควรระวังคืองานที่ทำหลังตอบ response แล้วต้องมีการจัดการ error ของตัวเองแยกต่างหาก เพราะถ้างานนั้นล้มเหลว ผู้ส่ง webhook จะไม่รู้เลยเนื่องจากตอบสำเร็จไปแล้ว ควรมีระบบ log หรือ alert แยกสำหรับตรวจสอบว่างานในพื้นหลังทำงานสำเร็จจริงหรือไม่ ไม่ใช่ปล่อยให้เงียบหายไปเฉย ๆ เมื่อเกิดปัญหา
เก็บ Event ลง Storage ไหนดี ระหว่าง KV, Queue, D1 หรือส่งต่อ Supabase
Cloudflare มีบริการเก็บข้อมูลหลายแบบที่ใช้ร่วมกับ Workers ได้ แต่ละแบบเหมาะกับงานต่างกัน สรุปเป็นตารางเทียบให้เห็นภาพก่อนตัดสินใจเลือก
| บริการ | เหมาะกับงานแบบไหน | ข้อจำกัดหลัก |
|---|---|---|
| Cloudflare KV | เก็บสถานะ idempotency แบบ key-value ที่อ่านเร็ว | ไม่เหมาะกับ query ที่ซับซ้อนหรือความสัมพันธ์ระหว่างข้อมูล |
| Cloudflare Queues | ส่งงานประมวลผลหนักไปทำแบบ asynchronous หลังตอบ response | ต้องออกแบบ consumer แยกต่างหากมารับงานจาก queue |
| Cloudflare D1 | เก็บ event history แบบมีโครงสร้าง query ได้ด้วย SQL เบื้องต้น | เหมาะกับข้อมูลขนาดไม่ใหญ่มาก ไม่ใช่ทดแทน Postgres เต็มรูปแบบ |
| ส่งต่อไป Supabase Postgres | ต้องการ query ซับซ้อน หรือรวมกับข้อมูลอื่นของระบบที่อยู่ใน Supabase อยู่แล้ว | ต้องผ่าน REST API หรือ Hyperdrive เพราะ Workers ต่อ TCP ตรงไม่ได้ |
ตัวอย่างสมมติ: Webhook พังตอนผู้ให้บริการ Retry ถี่กว่าที่คาด
ลองดูตัวอย่างสมมติของทีมที่สร้าง Worker รับ webhook จากระบบชำระเงินภายนอก โดยตอนออกแบบแรกเริ่มเขียน logic ทั้งหมดไว้ในเส้นทางเดียว ทั้งตรวจ signature บันทึกข้อมูล และเรียก API ภายในอีกตัวเพื่ออัปเดตสถานะออเดอร์ ก่อนค่อยตอบ response กลับ (ตัวเลขทั้งหมดเป็นตัวอย่างสมมติเพื่อประกอบการอธิบาย)
ช่วงที่ API ภายในตอบช้ากว่าปกติเพราะกำลังมี load สูงพร้อมกันจากงานอื่น ทำให้ Worker ใช้เวลารวมต่อ request นานขึ้นจนเกินเพดานเวลาที่ผู้ให้บริการชำระเงินรอ response ผู้ให้บริการจึงเริ่ม retry ซ้ำ event เดิมเข้ามาถี่ขึ้นเรื่อย ๆ เพราะคิดว่า request ก่อนหน้าล้มเหลว ทำให้จำนวน request ที่เข้ามาทั้งหมดเพิ่มขึ้นเป็นทวีคูณในช่วงเวลาสั้น ๆ
เพราะไม่มีการตรวจ idempotency มาก่อน แต่ละ retry ถูกประมวลผลซ้ำจนสถานะออเดอร์ถูกอัปเดตซ้ำหลายรอบ และมีการเรียก API ภายในซ้ำเพิ่มภาระให้ระบบที่โหลดสูงอยู่แล้วหนักขึ้นไปอีก กลายเป็นปัญหาลูกโซ่ที่ยิ่งแก้ยิ่งช้าลง
ทีมแก้ปัญหาด้วยการแยก Worker ตัวรับ webhook ออกจากส่วนประมวลผลจริง ให้ตัวรับทำแค่ตรวจ signature และบันทึก identifier ลง KV ก่อนตอบ response ทันที แล้วส่งงานจริงเข้า queue แยกต่างหากให้ consumer อีกตัวค่อย ๆ ดึงไปทำ พร้อมเพิ่มการตรวจ idempotency ก่อนประมวลผลทุกครั้ง ผลคือแม้ผู้ให้บริการยัง retry เข้ามาบ้างในบางช่วง แต่ไม่มีการประมวลผลซ้ำอีก และเวลาตอบ response กลับไปหาผู้ส่งก็สั้นลงมากจนแทบไม่เกิด retry เพิ่มเติมจากสาเหตุนี้อีกเลย
ทำแบบนี้แล้วพัง เพราะอะไร
ข้อผิดพลาดที่พบบ่อยเมื่อทีมสร้าง webhook receiver บน Workers แบบเร่งรีบโดยไม่คิดเรื่อง retry และ signature ตั้งแต่ต้น
- parse payload เป็น JSON ก่อนคำนวณ signature — ทำให้ signature ที่คำนวณได้ไม่ตรงกับต้นฉบับ เพราะ format ของ string เปลี่ยนไปจากตอนแปลงกลับจาก object
- ไม่มี idempotency key เก็บสถานะ event — ทำให้ retry จากผู้ให้บริการภายนอกกลายเป็นการประมวลผลซ้ำทุกครั้งโดยไม่รู้ตัว
- ทำงานหนักทั้งหมดก่อนตอบ response — เพิ่มความเสี่ยงที่ผู้ส่งจะ retry ซ้ำเพราะรอ response นานเกินเพดานเวลาที่ผู้ส่งกำหนดไว้
- ไม่มี log หรือ alert สำหรับงานที่ทำหลังตอบ response แล้ว — ถ้างานพื้นหลังล้มเหลว ไม่มีใครรู้เลยเพราะผู้ส่ง webhook ได้รับ response สำเร็จไปแล้ว
สรุป
Webhook receiver บน Cloudflare Workers ทำได้เร็วและกระจายอยู่บน edge network ที่กว้าง แต่ความเร็วนั้นไม่ได้แปลว่าปลอดภัยหรือถูกต้องโดยอัตโนมัติ ต้องออกแบบให้ตรวจ signature ทุกครั้ง จัดการ retry ด้วย idempotency และตอบ response ให้เร็วพอ ทั้งสามเรื่องต้องทำควบคู่กัน ขาดข้อใดข้อหนึ่งไปก็เสี่ยงเจอปัญหาที่ debug ยากตอน production จริง
การแยกชั้นระหว่างตัวรับ webhook กับส่วนประมวลผลจริงผ่าน queue หรือ Worker แยกต่างหาก ช่วยทั้งเรื่องความเร็วในการตอบ response และความทนทานเมื่อ retry เข้ามาถี่ในช่วงที่ระบบอื่นมีปัญหา ไม่ใช่แค่เรื่องความสวยงามของโครงสร้างโค้ดเท่านั้น
- ตรวจ signature จาก raw body เสมอ ไม่ parse เป็น JSON ก่อนคำนวณ
- ใช้ idempotency key กัน retry จากผู้ให้บริการภายนอกประมวลผลซ้ำ
- แยกงานหนักออกจากเส้นทางตอบ response หลัก เพื่อลดโอกาสถูก retry เพิ่มเติม
คำถามที่พบบ่อย
จำเป็นต้องตรวจ signature ทุก webhook endpoint ไหม
จำเป็น เพราะถ้าไม่ตรวจ signature ใครก็ตามที่รู้ URL endpoint สามารถส่ง payload ปลอมเข้ามาแล้วให้ระบบเชื่อว่าเป็น event จริงได้ ควรตรวจ signature ก่อนประมวลผลทุกครั้งไม่มีข้อยกเว้น
ทำไมต้องเก็บ raw body แทนการ parse เป็น JSON ก่อนคำนวณ signature
เพราะการแปลง JSON กลับเป็น string อาจได้ format ต่างจาก payload ต้นฉบับเล็กน้อย ทำให้ signature ที่คำนวณได้ไม่ตรงกับที่ผู้ส่งคำนวณมา ต้องใช้ raw text ดิบมาคำนวณเท่านั้น
ควรเก็บสถานะ idempotency นานแค่ไหน
ขึ้นกับนโยบาย retry ของผู้ให้บริการแต่ละราย ควรตั้ง expiration ให้ครอบคลุมช่วงเวลาที่ provider มักจะ retry ซ้ำ ถ้าต้องการเก็บประวัติระยะยาวเพื่อ audit ควรแยกไปเก็บใน database ต่างหาก
ใช้ Cloudflare Queues แทน KV เก็บสถานะ idempotency ได้ไหม
ไม่แนะนำสำหรับส่วนนี้ เพราะ KV เหมาะกับการอ่านสถานะแบบ key-value ที่เร็วกว่า ส่วน Queues เหมาะกับการส่งงานไปประมวลผลแบบ asynchronous มากกว่าการเก็บสถานะเช็คซ้ำ
ถ้าตอบ response ช้าไปแล้วโดนผู้ให้บริการ retry ซ้ำจะแก้ยังไง
ควรแยกงานที่ไม่จำเป็นต้องเสร็จก่อนตอบ response ออกไปทำหลังตอบแล้วผ่าน API ที่ Workers รองรับ พร้อมเพิ่ม idempotency ให้แน่ใจว่า retry ที่เข้ามาซ้ำจะไม่ถูกประมวลผลซ้ำอีก
ต้องใช้ library ภายนอกช่วยคำนวณ HMAC บน Workers ไหม
ไม่จำเป็น เพราะ Web Crypto API ที่ Workers รองรับอยู่แล้วสามารถคำนวณ HMAC ได้โดยตรง ไม่ต้องพึ่ง library เฉพาะทางที่อาจไม่รองรับ edge runtime
ลองตรวจด้วยตัวเอง
LINE Webhook Payload Inspector
วาง JSON payload จาก LINE แล้วดูว่าเป็น event ชนิดไหน มี field ครบตาม schema ปัจจุบันหรือไม่
ตรวจ payload ฟรี →ใช้ฟรี ไม่ต้องสมัคร ไม่เก็บข้อมูลที่คุณกรอก
อ่านต่อแบบเจาะลึก
วัด Conversion LINE ทุกแบรนด์ รวมศูนย์ที่เดียว
องค์กรที่มีหลายแบรนด์ หลายสาขา หลาย LINE OA — linli รวม Tracking และส่ง Conversion กลับทุกแพลตฟอร์ม พร้อม Custom Limit เริ่มฟรี 14 วัน
ติดต่อทีม Salesบทความที่เกี่ยวข้อง

สร้าง Webhook ด้วย Supabase Edge Functions แล้วเก็บทุก Event ลง Postgres ทันที

Vercel Serverless Function กับ Edge Function รับ Webhook ต่างกันตรงไหนเรื่อง Timeout
