مهندسی · 21 سپتامبر 2026

کلیدهای idempotency برای APIهای ایمیل

با پیاده‌سازی کلیدهای idempotency از ارسال ایمیل‌های تکراری هنگام تلاش مجدد شبکه جلوگیری کنید. یاد بگیرید چگونه خطاهای سیستم‌های توزیع‌شده را بدون اسپم کردن کاربران مدیریت کنید.

مشکل ایمیل تکراری

ایمیل تکراری زمانی رخ می‌دهد که کلاینت درخواستی می‌فرستد، سرور آن را پردازش می‌کند، اما پیش از رسیدن پاسخ موفق به کلاینت، شبکه قطع می‌شود. کلاینت با دیدن timeout یا خطای 5xx درخواست را دوباره تلاش می‌کند. بدون idempotency، سرور تلاش مجدد را درخواستی جدید تلقی می‌کند و ایمیل را دوباره می‌فرستد. کلیدهای idempotency با امکان تشخیص درخواست تکراری توسط سرور و بازگرداندن نتیجه اصلی بدون اجرای دوباره اثر جانبی، از این مشکل جلوگیری می‌کنند.

برای مهندسی که مسئول صف رخدادهاست، هیچ چیز بدتر از یک "طوفان ایمیل تکراری" نیست. این معمولاً هنگام قطعی جزئی یک ارائه‌دهنده بالادستی یا بن‌بست (deadlock) پایگاه داده که زمان پاسخ را کند می‌کند رخ می‌دهد. منطق تلاش مجدد شما که برای قابلیت اطمینان طراحی شده بود، به سلاحی تبدیل می‌شود که کاربرانتان را اسپم می‌کند و به اعتبار فرستنده شما آسیب می‌زند.

چرا تلاش مجدد بدون idempotency شکست می‌خورد

در یک سیستم توزیع‌شده، هر فراخوانی API سه نقطه شکست دارد:

  1. درخواست هرگز به سرور نمی‌رسد.
  2. سرور درخواست را پردازش می‌کند، اما پاسخ گم می‌شود.
  3. سرور در میانه پردازش از کار می‌افتد.

اگر در حالت 1 تلاش مجدد کنید، مشکلی نیست. اگر در حالت 2 تلاش مجدد کنید، ایمیل تکراری می‌فرستید. اگر در حالت 3 تلاش مجدد کنید، بسته به اینکه خرابی در کجا رخ داده، ممکن است ایمیل تکراری بفرستید.

ارسال ایمیل یک اثر جانبی خارجی است. برخلاف به‌روزرسانی نام کاربر در پایگاه داده (که اگر از SET name = 'Alice' استفاده کنید ذاتاً idempotent است)، ارسال ایمیل عملی افزایشی است. هر فراخوانی endpoint send یک پیام جدید در دنیای واقعی ایجاد می‌کند. برای idempotent کردن آن، باید یک شناسه یکتا برای قصد ارسال معرفی کنید که کلید idempotency نام دارد.

پیاده‌سازی کلیدهای idempotency

کلید idempotency مقداری یکتا (معمولاً یک UUID v4) است که کلاینت تولید می‌کند و در هدر درخواست می‌فرستد. سرور از این کلید برای پیگیری وضعیت درخواست استفاده می‌کند.

گردش‌کار سمت سرور

  1. دریافت درخواست: سرور بررسی می‌کند آیا هدر Idempotency-Key وجود دارد.
  2. Lookup: سرور آن کلید را در یک ذخیره‌ساز با دسترسی سریع (مانند Redis) جست‌وجو می‌کند.
  3. یافتن در کش (cache hit): اگر کلید وجود داشته باشد، سرور بدون فراخوانی موتور تحویل ایمیل، فوراً پاسخ ذخیره‌شده را برمی‌گرداند.
  4. نبودن در کش (cache miss): سرور کلید را قفل می‌کند، ارسال ایمیل را پردازش می‌کند، پاسخ را ذخیره می‌کند و آن را به کلاینت برمی‌گرداند.
  5. انقضا: کلید طوری تنظیم می‌شود که پس از یک بازه (مثلاً 24 ساعت) منقضی شود تا پایگاه داده بی‌نهایت بزرگ نشود.

نمونه عینی payload

هنگام استفاده از APIای مانند SendHQ، درخواست باید این‌گونه باشد:

POST /v1/send Host: api.sendhq.cc Content-Type: application/json Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 Authorization: Bearer YOUR_API_KEY { "to": "user@example.com", "template_id": "welcome-email", "variables": { "name": "Alex" } }

مدیریت حالت‌های خطا

با همه تلاش‌های مجدد نباید یکسان رفتار کرد. باید میان خطاهای کلاینت و خطاهای سرور تمایز قائل شوید.

  • خطاهای 4xx: اگر سرور 400 (Bad Request) یا 422 (Unprocessable Entity) برگرداند، درخواست نامعتبر است. تلاش مجدد با همان کلید باید همان خطای 4xx را برگرداند. payload را تغییر ندهید و همان کلید را دوباره استفاده نکنید، چون این کار تعارض ایجاد می‌کند.
  • خطاهای 5xx: اگر سرور 500 یا 503 برگرداند، کلاینت باید تلاش مجدد کند. اگر سرور پیش‌تر ایمیل را با موفقیت به MTA (عامل انتقال ایمیل) تحویل داده باشد، کلید idempotency تضمین می‌کند که تلاش مجدد به‌جای ارسال ایمیل دوم، 200 OK برگرداند.
  • درخواست‌های هم‌زمان: اگر دو درخواست یکسان با کلید یکسان دقیقاً در یک میلی‌ثانیه برسند، سرور باید برای درخواست دوم 409 Conflict برگرداند تا نشان دهد درخواست اول هنوز در حال پردازش است.

Idempotency برای ایجنت‌های هوش مصنوعی

ایجنت‌های هوش مصنوعی (که از سرورهای MCP یا کارت‌های A2A استفاده می‌کنند) لایه تازه‌ای از ریسک اضافه می‌کنند. LLMها می‌توانند غیرقطعی باشند و اگر در حلقه خطایی تشخیص دهند، ممکن است یک فراخوانی ابزار را چند بار اجرا کنند.

هنگام ساخت یکپارچه‌سازی‌های آماده برای ایجنت، هرگز نباید اجازه دهید ایجنت بدون مرحله تأیید یا کلید idempotency قطعی‌ای که orchestrator تولید می‌کند، عمل send را اجرا کند. orchestrator باید قصد ایجنت (مثلاً "گزارش هفتگی را برای Bob بفرست") را بر اساس شناسه گزارش و تاریخ به یک کلید پایدار نگاشت کند. این کار مانع می‌شود که ایجنت به‌خاطر اینکه "فکر کرده" فراخوانی اول شکست خورده، همان گزارش را به‌طور تصادفی پنج بار بفرستد.

هزینه شکست: مقایسه ارائه‌دهندگان

وقتی idempotency را پیاده‌سازی نکنید، فقط کاربران را آزرده نمی‌کنید؛ پول هم هدر می‌دهید. برخی ارائه‌دهندگان ارزان‌ترند، اما هزینه موارد تکراری به‌سرعت افزایش می‌یابد.

طبق صفحات رسمی قیمت‌گذاری (تا سپتامبر 2026):

  • Amazon SES: در مدل a la carte برابر 0.10 USD به‌ازای هر 1,000 ایمیل (قیمت‌گذاری Amazon SES). پلن‌های سطح‌بندی‌شده جدید که در 21 ژوئیه 2026 معرفی شدند شامل Essentials (0.16 USD به‌ازای هر 1,000)، Pro (0.22 USD به‌ازای هر 1,000 به‌علاوه 105 USD در ماه برای هر region) و Enterprise (0.23 USD به‌ازای هر 1,000 به‌علاوه 500 USD در ماه) هستند.
  • Resend: سطح رایگان 3,000 ایمیل در ماه با سقف 100 ایمیل در روز است. پلن Pro ماهانه 20 USD برای 50,000 ایمیل است و مصرف مازاد 0.90 USD به‌ازای هر 1,000 محاسبه می‌شود (قیمت‌گذاری Resend).
  • SendGrid: سطح رایگان اکنون یک دوره آزمایشی 60 روزه است و پلن‌های Essentials از 19.95 USD در ماه شروع می‌شوند (قیمت‌گذاری SendGrid).
  • Mailgun: ماهانه 15 USD برای 10,000 ایمیل، با مصرف مازاد از 1.80 تا 1.10 USD به‌ازای هر 1,000 (قیمت‌گذاری Mailgun).
  • Postmark: ماهانه 15 USD برای 10,000 ایمیل، با مصرف مازاد از 1.80 تا 1.20 USD به‌ازای هر 1,000 (قیمت‌گذاری Postmark).

برای درک بهتر، ارسال 50,000 ایمیل در SES با مدل a la carte حدود 5 USD هزینه دارد، در مقایسه با حدود 66 USD در سطوح Postmark. اگر یک حلقه تلاش مجدد بدون idempotency به‌طور تصادفی حجم ارسال شما را 10 برابر کند، تفاوت مالی میان ارائه‌دهندگان به یک قلم قابل‌توجه در گزارش رخداد شما تبدیل می‌شود.

تحویل‌پذیری در برابر پذیرش

درک این نکته حیاتی است که idempotency فقط مسئله پذیرش را حل می‌کند.

  1. پذیرش: API درخواست شما را می‌پذیرد و 200 OK برمی‌گرداند. کلیدهای idempotency در این مرحله عمل می‌کنند.
  2. تحویل: API ایمیل را به سرور گیرنده (مثلاً Gmail) تحویل می‌دهد. اینجاست که SPF و DKIM/DMARC اهمیت پیدا می‌کنند.
  3. رسیدن به صندوق ورودی: سرور گیرنده تصمیم می‌گیرد ایمیل به صندوق ورودی برود یا به پوشه اسپم.

کلید idempotency تضمین می‌کند که درخواست را فقط یک بار بپذیرید. تضمین نمی‌کند که ایمیل تحویل داده شود یا به پوشه اسپم نرود. برای اطمینان از اینکه زیرساخت شما برای تحویل درست پیکربندی شده است، از ابزارهایی مانند بررسی DNS ایمیل SendHQ برای بررسی رکوردهای خود استفاده کنید.

چک‌لیست پیاده‌سازی برای مهندسان

اگر امروز منطق ارسال ایمیل خود را بازبینی می‌کنید، از این چک‌لیست استفاده کنید:

  • تولید کلید سمت کلاینت: آیا برای هر قصد یکتای ارسال ایمیل یک UUID v4 تولید می‌کنید؟
  • پیاده‌سازی هدر: آیا کلید به‌جای بدنه درخواست، در یک هدر استاندارد (برای مثال Idempotency-Key) ارسال می‌شود؟
  • لایه ذخیره‌سازی: آیا برای کلیدهای idempotency خود TTL (Time To Live) دارید تا از انباشت فضای ذخیره‌سازی جلوگیری شود؟
  • قفل‌گذاری اتمی: آیا سرور شما برای جلوگیری از race condition روی یک کلید، از قفل توزیع‌شده (مانند SET NX در Redis) استفاده می‌کند؟
  • کش‌کردن پاسخ: آیا پاسخ کامل (کد وضعیت و بدنه) را ذخیره می‌کنید تا در تلاش‌های مجدد به کلاینت برگردانید؟
  • حفاظ‌های ایمنی ایجنت: اگر از ایجنت‌های AI استفاده می‌کنید، آیا کلید به‌جای LLM توسط orchestrator سیستم تولید می‌شود؟

نمونه کد: middleware idempotency در Node.js

این یک نمونه ساده‌شده از نحوه پیاده‌سازی این منطق در محیط Node.js با استفاده از Redis است.

const redis = require('redis'); const client = redis.createClient(); async function sendEmailHandler(req, res) { const idempotencyKey = req.headers['idempotency-key']; if (!idempotencyKey) { return res.status(400).json({ error: 'Idempotency-Key header is required' }); } // Try to acquire a lock and check for existing response const cachedResponse = await client.get(`idempotency:${idempotencyKey}`); if (cachedResponse) { const { status, body } = JSON.parse(cachedResponse); return res.status(status).json(body); } // Set a lock to prevent concurrent requests const lock = await client.set(`lock:${idempotencyKey}`, 'true', 'NX', 'EX', 30); if (!lock) { return res.status(409).json({ error: 'Request is currently being processed' }); } try { // Actual email sending logic const result = await emailProvider.send(req.body); const responsePayload = { status: 200, body: result }; // Cache the result for 24 hours await client.set(`idempotency:${idempotencyKey}`, JSON.stringify(responsePayload), 'EX', 86400); return res.status(200).json(result); } catch (error) { return res.status(500).json({ error: 'Internal Server Error' }); } finally { await client.del(`lock:${idempotencyKey}`); } }

جمع‌بندی

Idempotency برای ایمیل تراکنشی یک "امکان اختیاری" نیست؛ برای هر سیستمی که به تجربه کاربری و کنترل هزینه اهمیت می‌دهد یک الزام است. با سپردن مسئولیت یکتایی به کلاینت و فراهم کردن سازوکاری برای پیگیری آن در سرور، خطر ارسال تکراری در زمان ناپایداری شبکه را از بین می‌برید.

چه یک محصول SaaS سنتی بسازید و چه یک ایجنت هوش مصنوعی خودمختار، در نظر گرفتن ایمیل به‌عنوان یک اثر جانبی حیاتی تضمین می‌کند که سیستم شما قابل‌اعتماد بماند و کاربرانتان راضی باشند. برای یک API ایمیل توسعه‌دهنده‌محور که این پیچیدگی‌ها را مدیریت می‌کند، به https://sendhq.cc سر بزنید.