Short answer: put each event into a durable notification outbox, let a queue worker claim small batches, and use cron polling only as a recovery trigger; make every delivery attempt idempotent before tuning email or SMS throughput.

That decision keeps the business transaction separate from an external provider call without pretending the two systems can commit atomically. It also puts the hardest question in the right place: not "How fast can this loop send?" but "What happens when the process dies after the provider accepts a message and before the worker records success?"

The answer is at-least-once processing with explicit deduplication. Exactly-once delivery is not a property a normal application can promise across its database, a queue, an email service, mobile networks, and a recipient's handset. Keep the promise narrower: one durable intent, controlled retries, and one stable idempotency identity per channel delivery.

How should a Node.js queue worker batch email and SMS event notifications?

Write the event and its notification intents in the same database transaction. One event may create several intents because email and SMS have different payloads, consent rules, retry policies, and terminal outcomes. The outbox row should carry an immutable event ID, recipient ID, channel, template version, locale, scheduled time, and a deduplication key. Don't store a pre-rendered message unless audit or legal requirements demand it; templates change, but silently changing the content of an already queued transactional message can be just as dangerous. Pick one rule and record the template version.