feat: Automatically detect rate limit from AWS with ability to override in .env
This commit is contained in:
@@ -54,6 +54,12 @@ AWS_SES_SECRET_ACCESS_KEY=
|
|||||||
SES_CONFIGURATION_SET=plunk-configuration-set # Default: with open/click tracking
|
SES_CONFIGURATION_SET=plunk-configuration-set # Default: with open/click tracking
|
||||||
SES_CONFIGURATION_SET_NO_TRACKING=plunk-configuration-set-no-tracking # Optional: without tracking (enables toggle in UI)
|
SES_CONFIGURATION_SET_NO_TRACKING=plunk-configuration-set-no-tracking # Optional: without tracking (enables toggle in UI)
|
||||||
|
|
||||||
|
# Email sending rate limit (emails per second)
|
||||||
|
# If not set, automatically fetches from AWS SES account quota (recommended)
|
||||||
|
# Set this to override AWS quota (useful for setting lower limits or testing)
|
||||||
|
# Default: Fetched from AWS (typically 14 for sandbox, higher for production accounts)
|
||||||
|
# EMAIL_RATE_LIMIT_PER_SECOND=14
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# OAuth (Optional - for social login)
|
# OAuth (Optional - for social login)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|||||||
@@ -45,6 +45,13 @@ export const AWS_SES_REGION = validateEnv('AWS_SES_REGION');
|
|||||||
export const AWS_SES_ACCESS_KEY_ID = validateEnv('AWS_SES_ACCESS_KEY_ID');
|
export const AWS_SES_ACCESS_KEY_ID = validateEnv('AWS_SES_ACCESS_KEY_ID');
|
||||||
export const AWS_SES_SECRET_ACCESS_KEY = validateEnv('AWS_SES_SECRET_ACCESS_KEY');
|
export const AWS_SES_SECRET_ACCESS_KEY = validateEnv('AWS_SES_SECRET_ACCESS_KEY');
|
||||||
|
|
||||||
|
// Email Processing Rate Limit (optional override)
|
||||||
|
// If not set, will automatically fetch from AWS SES account quota
|
||||||
|
// Set this to override AWS quota (useful for setting lower limits or testing)
|
||||||
|
export const EMAIL_RATE_LIMIT_PER_SECOND = process.env.EMAIL_RATE_LIMIT_PER_SECOND
|
||||||
|
? Number(process.env.EMAIL_RATE_LIMIT_PER_SECOND)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// Storage
|
// Storage
|
||||||
export const REDIS_URL = validateEnv('REDIS_URL');
|
export const REDIS_URL = validateEnv('REDIS_URL');
|
||||||
export const DATABASE_URL = validateEnv('DATABASE_URL');
|
export const DATABASE_URL = validateEnv('DATABASE_URL');
|
||||||
|
|||||||
@@ -11,10 +11,41 @@ import {EmailService} from '../services/EmailService.js';
|
|||||||
import {EventService} from '../services/EventService.js';
|
import {EventService} from '../services/EventService.js';
|
||||||
import {MeterService} from '../services/MeterService.js';
|
import {MeterService} from '../services/MeterService.js';
|
||||||
import {emailQueue, type SendEmailJobData} from '../services/QueueService.js';
|
import {emailQueue, type SendEmailJobData} from '../services/QueueService.js';
|
||||||
import {sendRawEmail} from '../services/SESService.js';
|
import {getSendingQuota, sendRawEmail} from '../services/SESService.js';
|
||||||
import {DASHBOARD_URI} from '../app/constants.js';
|
import {DASHBOARD_URI, EMAIL_RATE_LIMIT_PER_SECOND} from '../app/constants.js';
|
||||||
|
|
||||||
export function createEmailWorker() {
|
/**
|
||||||
|
* Determine the email sending rate limit (emails per second)
|
||||||
|
* Priority: ENV variable > AWS SES quota > Safe default (14)
|
||||||
|
*/
|
||||||
|
async function getEmailRateLimit(): Promise<number> {
|
||||||
|
const DEFAULT_RATE_LIMIT = 14; // AWS SES sandbox limit - safe default
|
||||||
|
|
||||||
|
// If env variable is set, use it (override)
|
||||||
|
if (EMAIL_RATE_LIMIT_PER_SECOND !== undefined) {
|
||||||
|
console.log(`[EMAIL-PROCESSOR] Using rate limit from environment: ${EMAIL_RATE_LIMIT_PER_SECOND} emails/second`);
|
||||||
|
return EMAIL_RATE_LIMIT_PER_SECOND;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to fetch from AWS SES
|
||||||
|
console.log('[EMAIL-PROCESSOR] Fetching rate limit from AWS SES...');
|
||||||
|
const quota = await getSendingQuota();
|
||||||
|
|
||||||
|
if (quota) {
|
||||||
|
console.log(
|
||||||
|
`[EMAIL-PROCESSOR] AWS SES quota: ${quota.maxSendRate} emails/second (${quota.sentLast24Hours}/${quota.max24HourSend} emails sent today)`,
|
||||||
|
);
|
||||||
|
return quota.maxSendRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to safe default
|
||||||
|
console.warn(`[EMAIL-PROCESSOR] Failed to fetch AWS quota, using safe default: ${DEFAULT_RATE_LIMIT} emails/second`);
|
||||||
|
return DEFAULT_RATE_LIMIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createEmailWorker() {
|
||||||
|
// Fetch the rate limit (from env, AWS, or default)
|
||||||
|
const rateLimit = await getEmailRateLimit();
|
||||||
const worker = new Worker<SendEmailJobData>(
|
const worker = new Worker<SendEmailJobData>(
|
||||||
emailQueue.name,
|
emailQueue.name,
|
||||||
async (job: Job<SendEmailJobData>) => {
|
async (job: Job<SendEmailJobData>) => {
|
||||||
@@ -157,7 +188,7 @@ export function createEmailWorker() {
|
|||||||
connection: emailQueue.opts.connection,
|
connection: emailQueue.opts.connection,
|
||||||
concurrency: 10, // Process up to 10 emails concurrently
|
concurrency: 10, // Process up to 10 emails concurrently
|
||||||
limiter: {
|
limiter: {
|
||||||
max: 25, // Max 25 emails per second
|
max: rateLimit, // Max emails per second (from env, AWS SES quota, or default)
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ async function startWorkers() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Start email worker
|
// Start email worker
|
||||||
const emailWorker = createEmailWorker();
|
const emailWorker = await createEmailWorker();
|
||||||
workers.push({name: 'email', worker: emailWorker});
|
workers.push({name: 'email', worker: emailWorker});
|
||||||
signale.success('[WORKER] Email worker started');
|
signale.success('[WORKER] Email worker started');
|
||||||
|
|
||||||
|
|||||||
@@ -256,3 +256,26 @@ export const disableFeedbackForwarding = async (domain: string): Promise<void> =
|
|||||||
ForwardingEnabled: false,
|
ForwardingEnabled: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get AWS SES account sending quota and rate limit
|
||||||
|
* @returns MaxSendRate (emails per second) or null if the call fails
|
||||||
|
*/
|
||||||
|
export const getSendingQuota = async (): Promise<{
|
||||||
|
maxSendRate: number;
|
||||||
|
max24HourSend: number;
|
||||||
|
sentLast24Hours: number;
|
||||||
|
} | null> => {
|
||||||
|
try {
|
||||||
|
const quota = await ses.getSendQuota({});
|
||||||
|
|
||||||
|
return {
|
||||||
|
maxSendRate: quota.MaxSendRate ?? 14, // Default to sandbox limit if not provided
|
||||||
|
max24HourSend: quota.Max24HourSend ?? 200, // Default sandbox daily limit
|
||||||
|
sentLast24Hours: quota.SentLast24Hours ?? 0,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SES] Failed to fetch sending quota:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user