ทำไมคุณไม่ควรส่งอีเมลระหว่างรอ API ตอบกลับ
เวลาเราสร้างระบบสมัครสมาชิก (Signup) สิ่งแรกที่มือใหม่มักทำคือการเขียนโค้ดให้ทำงานเรียงกันไปเป็นเส้นตรง เมื่อผู้ใช้กดปุ่มสมัคร ระบบจะบันทึกข้อมูลลงฐานข้อมูล (Database - ที่เก็บข้อมูล) จากนั้นก็สั่งให้ระบบอีเมลส่งจดหมายต้อนรับทันที แล้วค่อยตอบกลับผู้ใช้ว่าสมัครเสร็จแล้ว นี่คือวิธีที่ดูเหมือนจะง่ายและทำตามได้ไม่ยาก แต่ในโลกความเป็นจริงที่คนใช้งานพร้อมกันหลายคน วิธีนี้อาจกลายเป็นฝันร้ายของโปรแกรมเมอร์ได้เลย
การส่งอีเมลเป็นกระบวนการที่กินเวลาและไม่แน่นอน Latency (ความหน่วงหรือเวลาที่ใช้ในการรอคอย) ของผู้ให้บริการส่งอีเมลนั้นสูงกว่าการเขียนข้อมูลลงฐานข้อมูลมาก หากคุณให้ผู้ใช้รอจนกว่าอีเมลจะส่งออกไปสำเร็จ แอปพลิเคชันของคุณจะช้าลงอย่างเห็นได้ชัด และหากผู้ให้บริการอีเมลล่มหรือทำงานช้า ระบบของคุณก็จะหยุดชะงักตามไปด้วย นี่คือเหตุผลว่าทำไมเราถึงไม่ควรเอาการส่งอีเมลไปรวมไว้ในขั้นตอนเดียวกับที่ผู้ใช้กำลังรอคำตอบจาก API (ช่องทางเชื่อมต่อระหว่างแอปพลิเคชัน)
ลองนึกภาพว่าคุณไปสั่งอาหารที่ร้านแห่งหนึ่ง แล้วพนักงานบอกว่าคุณต้องยืนรออยู่หน้าเคาน์เตอร์จนกว่าเชฟจะทำอาหารเสร็จและพนักงานจะเดินไปเสิร์ฟถึงโต๊ะคุณก่อน ถึงจะอนุญาตให้คุณไปนั่งที่โต๊ะได้ ถ้าวันไหนลูกค้าเยอะหรือเชฟทำอาหารช้า แถวหน้าร้านก็จะยาวเหยียดและลูกค้าคนอื่นก็เข้าไม่ได้ การเขียนโค้ดส่งอีเมลไว้ใน Request (คำขอจากผู้ใช้) ก็เหมือนกับการให้ลูกค้าคนนั้นยืนรอจนกว่าอีเมลจะส่งออกไปนั่นเอง
รูปแบบโค้ดที่ดูเหมือนจะดีแต่แอบแฝงอันตราย
ลองมาดูตัวอย่างโค้ดที่มือใหม่มักเขียนกัน ซึ่งมันทำงานได้ดีตอนรันบนเครื่องตัวเอง แต่จะเริ่มมีปัญหาเมื่อนำไปใช้จริง โค้ดชุดนี้จะทำงานแบบ Synchronous (การทำงานแบบเรียงลำดับที่ต้องรอให้งานหนึ่งเสร็จก่อนถึงจะทำต่อไปได้) ซึ่งเป็นจุดเริ่มต้นของปัญหาคอขวดในระบบ
// โค้ดสมัครสมาชิกแบบที่มือใหม่ชอบใช้
app.post('/signup', async (c) => {
const body = await c.req.json();
// 1. บันทึกข้อมูลลงฐานข้อมูล
const user = await db.users.create({ email: body.email });
// 2. สั่งส่งอีเมลทันที (จุดที่ทำให้ระบบช้า)
await emailClient.send({
to: user.email,
subject: 'ยินดีต้อนรับ'
});
// 3. ตอบกลับผู้ใช้
return c.json({ message: 'สมัครสำเร็จ' }, 201);
});
ในโค้ดนี้ บรรทัดที่เรียกใช้ emailClient.send คือกับดักสำคัญ ระบบจะหยุดทำงานตรงนี้จนกว่าเซิร์ฟเวอร์อีเมลจะตอบกลับมาว่าส่งสำเร็จแล้ว หากเซิร์ฟเวอร์อีเมลใช้เวลา 3 วินาที ผู้ใช้ของคุณก็ต้องรอหน้าจอนิ่งๆ ไป 3 วินาทีเต็มๆ ซึ่งนานเกินไปสำหรับแอปพลิเคชันสมัยใหม่ที่ควรตอบสนองภายในไม่กี่ร้อยมิลลิวินาที
นอกจากเรื่องความช้าแล้ว หากบรรทัดส่งอีเมลเกิดข้อผิดพลาดขึ้นมา ระบบจะโยน Error (ข้อผิดพลาดที่เกิดขึ้นขณะทำงาน) ออกมาทันที ทำให้ผู้ใช้เห็นหน้าจอแจ้งเตือนว่าสมัครไม่สำเร็จ ทั้งที่จริงแล้วข้อมูลผู้ใช้ถูกบันทึกลงฐานข้อมูลไปแล้ว การจัดการกับข้อผิดพลาดแบบนี้ในโค้ดฝั่งเดียวจะทำให้ระบบของคุณซับซ้อนและดูแลรักษายากขึ้นเรื่อยๆ
ปัญหาที่เกิดขึ้นเมื่อระบบต้องรองรับผู้ใช้จำนวนมาก
เมื่อคุณมีผู้ใช้หลักพันหรือหลักหมื่น ความผิดพลาดของระบบภายนอกจะส่งผลกระทบโดยตรงต่อแอปของคุณ หากผู้ให้บริการอีเมลมีปัญหา (Outage) ระบบสมัครสมาชิกของคุณจะพังตามไปด้วยทันที ผู้ใช้จะกดสมัครไม่ได้เพราะ API ของคุณกำลังติดแหง็กอยู่ที่การพยายามส่งอีเมลที่ไม่มีวันสำเร็จ
อีกเรื่องที่น่ากลัวคือ Rate Limits (ข้อจำกัดจำนวนครั้งในการเรียกใช้งาน) ผู้ให้บริการอีเมลมักจะมีกฎว่าส่งได้กี่ฉบับต่อนาที หากวันหนึ่งมีคนสมัครเข้ามาพร้อมกันเยอะๆ ระบบของคุณจะโดนบล็อกการส่งอีเมล และนั่นจะกลายเป็นเหตุผลที่ทำให้ API ของคุณส่งค่า 500 (รหัสแจ้งข้อผิดพลาดจากเซิร์ฟเวอร์) กลับไปหาผู้ใช้ ทั้งที่จริงๆ แล้วระบบฐานข้อมูลของคุณไม่ได้มีปัญหาอะไรเลย
การปล่อยให้ผู้ใช้รอคอยนานเกินไปจะทำให้เกิดพฤติกรรม "กดย้ำ" เมื่อคนเห็นหน้าจอค้างนานๆ เขามักจะกดปุ่มสมัครซ้ำหลายรอบ ผลลัพธ์คือคุณอาจจะได้ข้อมูลผู้ใช้ซ้ำซ้อนในฐานข้อมูล หรือแย่ไปกว่านั้นคือส่งอีเมลต้อนรับไปหาเขาถึง 5 ฉบับ ทำให้ภาพลักษณ์ของแอปดูไม่เป็นมืออาชีพและสร้างความสับสนให้ผู้ใช้งาน
กฎทอง: API ควรทำหน้าที่แค่ "จองงาน" ไม่ใช่ "ลงมือทำ"
วิธีแก้ปัญหาที่ดีที่สุดคือการเปลี่ยนวิธีคิด ให้มองว่าการส่งอีเมลเป็น Side Effect (งานที่ต้องทำตามมาหลังจากงานหลักเสร็จสิ้น) ซึ่งไม่จำเป็นต้องทำทันทีในวินาทีนั้น เราควรแยกงานส่งอีเมลออกจากเส้นทางการตอบสนองของผู้ใช้ โดยใช้สิ่งที่เรียกว่า Queue (คิวหรือแถวลำดับงานที่รอการประมวลผล) เข้ามาช่วย
หลักการคือเมื่อผู้ใช้กดสมัคร API ของคุณจะทำแค่สองอย่าง คือบันทึกข้อมูลลงฐานข้อมูลและ "หย่อนงาน" ลงในคิว จากนั้นก็ตอบกลับผู้ใช้ทันทีว่า "รับเรื่องแล้ว" ส่วนงานส่งอีเมลจะมี Worker (โปรแกรมหรือกระบวนการที่คอยดึงงานจากคิวไปทำต่อ) มาคอยหยิบงานในคิวไปจัดการทีละอย่างตามความเร็วที่มันทำได้
วิธีนี้ทำให้ API ของคุณทำงานได้เร็วมาก เพราะมันไม่ต้องรอการตอบกลับจากระบบภายนอกอีกต่อไป ต่อให้ระบบอีเมลจะล่มหรือช้าแค่ไหน ผู้ใช้ของคุณก็ยังสมัครสมาชิกได้ปกติ ส่วนอีเมลก็จะถูกส่งออกไปเมื่อระบบอีเมลพร้อมใช้งานอีกครั้ง นี่คือหัวใจสำคัญของการออกแบบระบบที่รองรับการขยายตัว (Scalability) ในอนาคต
วิธีเปลี่ยนมาใช้ระบบคิว (Background Queue)
ในการเขียนโค้ดจริงเรามักใช้เครื่องมืออย่าง BullMQ ควบคู่กับ Redis (ระบบเก็บข้อมูลในหน่วยความจำที่ทำงานได้เร็วมาก) เพื่อทำหน้าที่เป็นคิวเก็บงาน ลองดูตัวอย่างการปรับแก้โค้ดให้เป็นแบบ Asynchronous (การทำงานที่ไม่ต้องรอผลลัพธ์ทันที) ดังนี้
// โค้ดที่ใช้ระบบคิว (แบบที่ถูกต้อง)
app.post('/signup', async (c) => {
const body = await c.req.json();
const user = await db.users.create({ email: body.email });
// ส่งงานเข้าคิวแทนการส่งอีเมลโดยตรง
await emailQueue.add('send-welcome-email', {
email: user.email
});
// ตอบกลับผู้ใช้ทันที
return c.json({ message: 'สมัครสำเร็จ' }, 202);
});
ในโค้ดนี้บรรทัด emailQueue.add จะทำการบันทึกข้อมูลงานลงใน Redis ซึ่งใช้เวลาเพียงเสี้ยววินาที API ของเราจึงสามารถส่งคำตอบ 202 (สถานะยอมรับคำขอแต่ยังประมวลผลไม่เสร็จ) กลับไปหาผู้ใช้ได้ทันที ทำให้ผู้ใช้รู้สึกว่าระบบทำงานเร็วและตอบสนองดีมาก
ส่วนงานส่งอีเมลจะถูกย้ายไปที่ไฟล์ worker.js ซึ่งเป็นโปรแกรมแยกต่างหากที่คอยวนลูปดึงงานจากคิวมาทำ หากงานชิ้นไหนส่งไม่สำเร็จ เรายังสามารถตั้งค่าให้มัน Retry (การลองทำซ้ำ) ได้โดยอัตโนมัติ ซึ่งเป็นเรื่องที่ทำไม่ได้เลยหากส่งอีเมลใน Request หลัก
ข้อควรระวังและแนวปฏิบัติที่ดี
แม้การใช้คิวจะดีมาก แต่คุณต้องระวังเรื่อง Idempotency (คุณสมบัติที่การทำซ้ำกี่ครั้งก็ได้ผลลัพธ์เท่าเดิม) เพราะระบบคิวอาจจะหยิบงานเดิมมาทำซ้ำหากมีข้อผิดพลาดเกิดขึ้น คุณควรมีตัวระบุงานที่ชัดเจน เช่น ใช้ user_id เป็นกุญแจสำคัญ เพื่อป้องกันไม่ให้ผู้ใช้ได้รับอีเมลต้อนรับซ้ำหลายรอบ
นอกจากเรื่องอีเมลแล้ว งานอื่นๆ ที่ควรแยกออกมาทำในคิว ได้แก่ การย่อขนาดรูปภาพ การสร้างไฟล์ PDF การส่งข้อมูลไปยังระบบวิเคราะห์ข้อมูล (Analytics) หรือการเรียกใช้ AI API ที่กินเวลานาน หลักการนี้ใช้ได้กับงานทุกอย่างที่ผู้ใช้ไม่จำเป็นต้องรอผลลัพธ์ในทันที
สุดท้ายอย่าลืมตรวจสอบสถานะของงานในคิวอยู่เสมอ ควรมีการตั้งค่า Dead Letter Queue (คิวสำหรับเก็บงานที่พยายามทำซ้ำจนเกินกำหนดแล้วแต่ยังไม่สำเร็จ) ไว้ด้วย เพื่อให้คุณสามารถกลับมาตรวจสอบได้ว่ามีอีเมลฉบับไหนที่ส่งไม่ไปจริงๆ และเข้าไปแก้ไขปัญหาได้อย่างตรงจุด
สรุป
การส่งอีเมลภายใน API Request คือกับดักที่โปรแกรมเมอร์มือใหม่มักพลาด เพราะมันทำให้ระบบช้าและเปราะบางต่อความผิดพลาดของระบบภายนอก การย้ายงานเหล่านี้ไปไว้ในระบบคิวจะช่วยให้ API ของคุณทำงานได้รวดเร็วและมีความน่าเชื่อถือสูงขึ้น แม้ในวันที่ผู้ให้บริการอีเมลของคุณจะมีปัญหา ระบบของคุณก็ยังคงให้บริการผู้ใช้ต่อไปได้
หากคุณกำลังทำโปรเจกต์ส่วนตัวหรือโปรเจกต์สมัครงาน ให้ลองฝึกใช้ระบบคิวตั้งแต่ตอนนี้ แม้จะดูเหมือนยุ่งยากกว่าการเขียนโค้ดบรรทัดเดียว แต่มันเป็นทักษะสำคัญที่บริษัทไอทีชั้นนำมองหา เพราะมันแสดงให้เห็นว่าคุณเข้าใจวิธีการออกแบบระบบที่มีประสิทธิภาพและรองรับการใช้งานจริงได้
ตัวอย่างการนำไปใช้จริง: หากคุณทำโปรเจกต์แอปแชท แทนที่จะส่งการแจ้งเตือน (Push Notification) ทันทีที่ข้อความเข้า ให้คุณลองใช้ระบบคิวมาจัดการแทน เพื่อให้มั่นใจว่าต่อให้คนส่งข้อความพร้อมกันเป็นหมื่นคน แอปของคุณก็จะไม่ค้างและผู้ใช้จะได้รับการแจ้งเตือนอย่างครบถ้วนแน่นอน
ที่มา: Why You Should Never Send Emails Inside Your API Requests — freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More