Retry · endpoint ล่ม · การส่งซ้ำ
ถ้า endpoint ของคุณไม่ตอบ 2xx เราจะส่งซ้ำตามตารางคงที่ข้างล่าง · ตัวเลขในหน้านี้เป็นชุดเดียวในระบบ ไม่มีค่าอื่น
ตารางรอบ retry
| attempt | หน่วงจากครั้งก่อน | สะสมจากเวลาที่สร้าง event |
|---|---|---|
| 1 | ทันที | 00:00:00 |
| 2 | 10 วินาที | 00:00:10 |
| 3 | 1 นาที | 00:01:10 |
| 4 | 5 นาที | 00:06:10 |
| 5 | 30 นาที | 00:36:10 |
| 6 | 2 ชั่วโมง | 02:36:10 |
| 7 | 6 ชั่วโมง | 08:36:10 |
| 8 | 12 ชั่วโมง | 20:36:10 |
| 9 | 24 ชั่วโมง | 44:36:10 |
| 10 | 24 ชั่วโมง | 68:36:10 |
| 11 | 24 ชั่วโมง | 92:36:10 |
- รวม 11 ครั้ง · หน้าต่างประมาณ 92 ชั่วโมง 36 นาที (3 วัน 20 ชั่วโมง)
- มี jitter ±10% ต่อรอบ ⇒ ตารางนี้เป็นค่าเป้าหมาย · เวลาจริงของแต่ละใบดูที่
next_retry_atในหน้าจัดการ X-PG-Attemptheader บอกว่ากำลังอยู่รอบไหน · delivery metadata ไม่อยู่ใน event body
ทำไม inbox ต้องเก็บ 31 วันทั้งที่ retry มีแค่ ~92 ชั่วโมง
manual resend ทำได้ถึง 30 วันหลังสร้าง event ดังนั้น pg_webhook_inbox ต้องเก็บอย่างน้อย 31 วัน เพื่อให้การส่งซ้ำวันสุดท้ายยังชน dedupe row
รอบที่ 9–11 ห่างกันวันละครั้ง
ถ้า endpoint คุณล่มไป 2 ชั่วโมงแล้วกลับมา คุณจะยังไม่ได้ event ที่ค้างทันที — ต้องรอถึงรอบถัดไปตามตาราง
อย่ารอเฉย ๆ · ใช้ replay ย้อนหลัง หรือ reconcile ด้วย GET /v1/pay/events ดึงของที่ค้างมาเลย
อะไรนับว่าสำเร็จ
| เกณฑ์ | ค่า |
|---|---|
| สำเร็จ | HTTP 200 201 202 204 เท่านั้น |
| timeout ตอนเชื่อมต่อ | 5 วินาที |
| timeout ถึงไบต์แรกของ response | 10 วินาที |
| timeout รวมทั้ง request | 15 วินาที (แนะนำให้ตอบภายใน 3 วินาที) |
| response body ของคุณ | เราอ่านสูงสุด 8 KB ไปเก็บ log แล้วปิด connection · เนื้อหาไม่มีผลต่อความสำเร็จ |
| request body ของเรา | ≤ 256 KB (รับประกัน) |
3xx | 🔴 ไม่ตาม redirect = failure (กัน SSRF + กันไม่ให้ลายเซ็นไปโผล่โฮสต์อื่น) |
410 Gone | failure ถาวร → หยุด retry ทันที + ติดธง endpoint_gone |
429 | failure → retry ตามตาราง · 🔴 เราไม่อ่าน Retry-After ของคุณ ตารางคงที่เพื่อให้คาดเดาได้ |
4xx/5xx อื่น · TLS/DNS/connection error | failure → retry ตามตาราง |
เลิก retry เมื่อ ได้ 2xx · ครบ 11 attempt · ได้ 410 · คุณลบหรือปิด endpoint (delivery ที่ค้างถูกยกเลิก) · ระบบปิด endpoint อัตโนมัติ
สถานะของ delivery มี 4 ค่าเท่านั้น: pending · delivered · failed · canceled
ตอบ 3 วินาที ไม่ใช่ 15
15 วินาทีคือเส้นตายที่เราตัด ไม่ใช่งบเวลาที่ให้ใช้
handler ที่ใช้ 12 วินาทีจะเริ่ม timeout ทันทีที่ traffic เพิ่มขึ้นนิดเดียว แล้วเข้าวงจร: timeout → retry → ทำงานซ้ำ → ช้าลง → timeout อีก
รูปแบบที่ถูก: verify → transaction ที่ insert/dedupe inbox พร้อม outbox → commit → ตอบ 200 · outbox relay ทำงานต่อภายหลัง
อย่าตอบ 500 เมื่อ verify ลายเซ็นไม่ผ่าน
ตอบ 500 = failure = เรา retry ทั้ง 11 รอบ ทุก event ที่ verify ไม่ผ่าน = คุณถล่มตัวเอง
ลายเซ็นไม่ผ่าน → 401 · timestamp เกินหน้าต่าง → 400 · event ซ้ำ → 🔴 200
endpoint ล่มยาว
ล่มติดกัน 6 ชม. → อีเมล warning + banner ในหน้าจัดการ (ยัง retry ตามปกติ)
ล่มติดกัน 12 ชม. → อีเมล warning ครั้งที่ 2 + แจ้งผู้ให้บริการที่ดูแลบัญชีคุณ
ล่มติดกัน 24 ชม. → 🔴 ปิด endpoint อัตโนมัติเงื่อนไขการปิดอัตโนมัติ (ต้องครบทั้งสองข้อ): ใน 24 ชั่วโมงล่าสุด success = 0 และ delivery ที่ fail ≥ 50
สิ่งที่เกิดขึ้นเมื่อถูกปิด
endpoint.status = 'disabled_by_system'- หยุดสร้าง delivery ใหม่ — แต่ event ยังถูกเก็บครบ 30 วัน
- delivery ที่ค้างอยู่ถูกยกเลิก
- ยิง
webhook.endpoint.disabledไป endpoint กลุ่มอื่นที่ยังดี + อีเมล + บันทึก audit - 🔴 เปิดคืนต้องกดเองจากหน้าจัดการ — มีปุ่ม Replay failed deliveries (72 ชม. ล่าสุด) ให้กดต่อทันที
endpoint ที่ flaky ไม่ถูกปิด
มี success อย่างน้อย 1 ครั้งใน 24 ชั่วโมง = ไม่ถูกปิด ⇒ endpoint ที่ fail 90% ยังอยู่ต่อได้ (แต่จะโดนเตือน)
เกณฑ์ตั้งไว้แบบนี้เพราะการปิด endpoint ของ merchant ที่แค่ช้า อันตรายกว่าการปล่อยให้ retry ต่อ
ถ้าคุณมี endpoint เดียวและมันถูกปิด
event ที่ ปิดรับไม่ได้ (deposit.reversed · withdrawal.fill_reversed · balance.debited · webhook.key.rotating · webhook.endpoint.disabled) จะถูกส่งทาง อีเมล ไปที่ technical contact พร้อม banner สีแดงค้างในหน้าจัดการ
⇒ ตั้ง technical contact เป็นเมลที่มีคนอ่านจริง ไม่ใช่ noreply@ · และควรตั้ง endpoint สำรองไว้อย่างน้อยอีกกลุ่ม
การกดส่งซ้ำเอง
ส่งซ้ำใบเดียว
curl -X POST \
"https://api.example-wl.com/v1/bo/webhooks/whep_01K2A9F3TNXR7VB2HD0MKQ5CJW/deliveries/whd_01K3M8QX5DHN2VRT0BKZQ7W4YM/resend" \
-H "Authorization: Bearer $BO_OIDC_ACCESS_TOKEN" \
-H "X-PG-Step-Up-Token: $BO_STEP_UP_TOKEN" \
-H "Idempotency-Key: resend-whd_01K3M8QX-1"ส่งซ้ำเป็นชุด (replay)
curl -X POST \
"https://api.example-wl.com/v1/bo/webhooks/whep_01K2A9F3TNXR7VB2HD0MKQ5CJW/replay" \
-H "Authorization: Bearer $BO_OIDC_ACCESS_TOKEN" \
-H "X-PG-Step-Up-Token: $BO_STEP_UP_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: replay-2026-08-18-to-21" \
-d '{
"event_types": ["deposit.settled", "deposit.reversed"],
"created_after": "2026-08-18T00:00:00.000Z",
"created_before": "2026-08-21T00:00:00.000Z",
"only_failed": true
}'resend/replay เป็น /v1/bo/* จึงใช้ OIDC access token ของเจ้าหน้าที่พร้อม step-up token ไม่ใช้ sk_live_* หรือ PG HMAC
กติกาของการส่งซ้ำ
| เรื่อง | เป็นยังไง |
|---|---|
event.id | 🔴 เหมือนเดิมทุกประการ ⇒ dedupe ฝั่งคุณยังทำงาน — กด replay ทั้งสัปดาห์ก็ไม่เครดิตซ้ำ ถ้าคุณ dedupe ถูก |
| ไบต์ของ body | เหมือนเดิมทุกไบต์ — เราเก็บ payload ไว้ตั้งแต่ตอนสร้าง ไม่ render ใหม่ ⇒ api_version ที่เปลี่ยนกลางทางไม่ทำให้ payload เพี้ยน |
| ลายเซ็น | คำนวณใหม่ (t ต้องใหม่) และเซ็นด้วยกุญแจที่ active ตอนนี้ |
| delivery metadata | อยู่ใน header เท่านั้น: X-PG-Delivery-Id ใหม่, X-PG-Attempt: 1, X-PG-Resend: manual · event body ไม่เปลี่ยน |
| retry | ได้ครบ 11 รอบใหม่ |
| bulk | 10,000 event ต่อคำสั่ง ปล่อยที่ 50 ใบ/วินาที · มี audit ทุกครั้ง |
| อายุ | ส่งซ้ำได้ 30 วัน นับจาก created_at ของ event |
| rate limit | bucket webhook_resend — 10 คำสั่ง/นาที ต่อ endpoint (นับคำสั่ง ไม่ใช่จำนวน delivery) ⇒ คำสั่ง replay ที่มี 10,000 ใบ นับเป็น 1 |
replay 10,000 ใบ = 50 ใบ/วินาที ยิงเข้า endpoint คุณ
ก่อนกด ให้แน่ใจว่า handler ตอบเร็ว (< 3 วิ) และ dedupe ทำงาน · ไม่งั้นคุณจะสร้าง incident เองจากการกู้ incident
เมื่อ event หายจริง ๆ
หมดรอบ 11 ครั้งแล้วยังไม่สำเร็จ → webhook_deliveries.status = 'failed' · event ยังอยู่ในระบบ 30 วัน และส่งซ้ำได้
แต่ทางที่ถูกต้องกว่าเมื่อกลับมาจาก downtime ยาวคือ ดึงเอง ไม่ใช่รอ:
# ดึงทุก event ตั้งแต่ sequence ล่าสุดที่คุณเห็น
curl -s "https://api.example-wl.com/v1/pay/events?since_sequence=88214&limit=100" \
-H "Authorization: Bearer sk_live_xxx" \
-H "PG-Signature: ..." -H "PG-Timestamp: ..." -H "PG-Nonce: ..." -H "PG-Version: 2026-08-01"- ย้อนหลังได้ 30 วัน
since_sequenceเป็น cursor ตัวเดียว ของ endpoint นี้ (ไม่ใช้cursorแบบ endpoint อื่น)- ป้อนเข้า handler เดิมได้เลย — dedupe และ version guard จะจัดการซ้ำให้เอง
🔴 webhook ไม่ใช่ source of truth — ledger คือความจริง webhook คือการแจ้งเตือน · reconcile job รายวันเป็นเงื่อนไขบังคับก่อน go-live ไม่ใช่ทางเลือก
เฝ้าอะไรบ้าง
| ตั้ง alert เมื่อ | เพราะ |
|---|---|
| verify ลายเซ็นไม่ผ่าน > 0 | อาจกำลังหมุนกุญแจ หรือมีคนยิงของปลอม |
| handler ตอบ non-2xx เกิน 1% ใน 5 นาที | ก่อนถึงเกณฑ์ปิด endpoint อัตโนมัติ 24 ชม. |
X-PG-Attempt > 3 บ่อยผิดปกติ | endpoint ช้าหรือ timeout เป็นระบบ |
sequence มี gap ค้างเกิน 4 วัน | หายจริง (gap ชั่วคราวเป็นเรื่องปกติ — ดู ordering) |
log ที่ต้องมีทุกใบ: event.id + X-PG-Delivery-Id + X-PG-Attempt — delivery headers ใช้เพื่อ observability เท่านั้น ห้ามขับ business logic
ขั้นต่อไป
| อ่านต่อ | เมื่อ |
|---|---|
| ลำดับไม่การันตี — version guard | 🔴 retry 92 ชม. แปลว่า event เก่ามาถึงทีหลังได้เสมอ — อ่านหน้านี้ |
| ตรวจลายเซ็น Ed25519 | ยัง verify ไม่ผ่านและกำลังโดน retry ถล่ม |
| ทดสอบก่อนขึ้น production | อยากลองทำให้ endpoint ล่มแล้วดูผล |
| ภาพรวม webhook | ต้องการเกณฑ์ตั้ง endpoint และโครงซองอีกครั้ง |
| เช็กลิสต์ก่อน go-live | ใกล้ขึ้น production |
