To prevent duplicate permit-expiration emails in NestJS, coordinate scheduled work across application replicas, give each intended notice a stable business-event key, and make enqueueing or processing idempotent. NestJS runs scheduled jobs in every application process, so a local cron setting alone cannot stop multiple replicas from sending the same notice. For stronger crash recovery, write notification intent to a transactional outbox. These measures control application work; they do not guarantee exactly-once delivery by an external email provider.
Why multiple NestJS instances can send the same email
NestJS’s official Task Scheduling documentation states: “The scheduler runs every job in every process of your application.” In a horizontally scaled deployment, each replica can therefore run the same expiration scan or dispatch task. A task that works correctly on one process may produce duplicate emails when several processes run it at the same scheduled time.
As an Amazon Associate I earn from qualifying purchases.
There are two separate problems to address: preventing redundant work across replicas, and ensuring repeated or retried work for the same permit event does not create another notification. A lock helps with the first; a stable event identity and durable deduplication help with the second.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Choose a stable identity for each intended notice
Before adding a lock or queue setting, define what counts as “the same email.” Use a business key that represents one intended notification, such as the permit ID, notification type, and expiration period or version. This is an application design recommendation: the key should remain the same when the same notification is retried, but change when a genuinely new notice is intended.
#1 Best Overall
A cron tick timestamp is usually a poor deduplication key. It identifies when a scan ran, not the permit-expiration event; the same notice found on another tick would have a different timestamp and could pass deduplication. Carry the business key through the scan, queue job, send-attempt record, and logs.
Coordinate scheduled work across replicas
Use a shared distributed lock for one-per-tick work
If only one replica should run a scheduled scan or dispatch task for each tick, acquire a lock from storage shared by all replicas. The NestJS Queues documentation describes distributed locking concepts including renewable leases and fencing tokens. Use an explicit, stable lock key rather than relying on a class or method name: a code rename during deployment should not unintentionally create a different lock identity.
A basic Redis key with an expiration time is not automatically safe. If the process holding it pauses beyond the time-to-live, another replica may acquire the key while the original process is still running. Prefer renewable leases with lock-loss handling and fencing where supported. A process that has lost its lease should not continue acting as though it still owns the task.
Prevent overlap inside a process when necessary
If a task can take longer than its interval, prevent a new run from starting on the same process while the prior run is active. NestJS scheduler options can help with local overlap, but local controls do not coordinate replicas. Use them to solve per-process overlap, not as a substitute for a shared lock in a multi-instance deployment. See the NestJS Task Scheduling documentation for scheduler behavior and options.
Rank #3
Deduplicate queued work and retain the guarantee long enough
For asynchronous email processing with BullMQ, assign a deterministic custom job ID or use a deduplication ID based on the business-event key. BullMQ can reject another job with an ID that is already present, and its deduplication mechanisms can suppress repeated additions according to their configured behavior. See the NestJS Queues documentation and BullMQ’s job deduplication and throttling guides.
Queue-based deduplication is only as durable as the job or deduplication record. If completed or failed jobs are removed, the same ID may become available again. When the dedupe guarantee must outlast queue retention, store a claimed or sent notification record in the database and enforce uniqueness on the business key. Decide explicitly whether a failed attempt may be retried under the same key and how its status changes; do not treat job removal as proof that a notification was never sent.
Rank #4
Use an outbox when permit updates and notification intent must stay together
If a permit update and the decision to notify must commit atomically, insert an outbox row in the same database transaction as the permit change. A separate publisher reads unprocessed rows, enqueues or dispatches the event, and records progress. This prevents a crash between a database commit and a separate queue-add call from silently losing the intent to notify.
Recommended Free Tools
NestJS notes that an ordinary Queue.add() uses BullMQ’s own connection pool in autocommit mode; it does not automatically participate in the application database transaction. See the NestJS Queues documentation. The outbox makes the intent durable with the business change, but publishing and email delivery remain later steps that still need retries, idempotency, and monitoring.
Best Value
Compare the controls by the failure they address
| Approach | Useful when | Key limit |
|---|---|---|
| Shared distributed lock | A scheduled scan or dispatch task should run on one replica per tick. | Coordinates execution, not exactly-once provider delivery. Use stable keys, lease renewal, and lock-loss handling. |
| BullMQ job ID or deduplication | Repeated producers may enqueue the same notice and Redis-backed asynchronous processing fits the system. | Duplicate detection depends on the job or deduplication record remaining available; removal can allow a later duplicate. |
| Transactional outbox | The permit change and intent to notify must not diverge during crashes. | Publication is still a separate step; downstream retries need idempotency and monitoring. |
| Local cron overlap prevention | A long-running task must not overlap another run in the same process. | Does not coordinate multiple replicas. |
Choose based on whether the system needs multi-instance coordination, crash recovery, dedupe state that outlives queue retention, atomicity with permit updates, or better operational visibility. These controls can be combined: for example, a shared lock for a scheduled scan, an outbox for durable intent, and a unique database key for notification-level idempotency.
Handle ambiguous email-provider outcomes
A timeout while sending does not establish whether the provider accepted the message. Persist attempts and final state, and distinguish a definite rejection from an ambiguous outcome. Use a provider idempotency feature only if that provider’s current contract supports it, and reconcile uncertain attempts rather than blindly resending them. A lock, queue job ID, or outbox cannot prove that a recipient received exactly one email.
Monitor both failures and missing runs
A scheduled job can stop running without producing an exception, so alert on missing expected runs as well as failed runs. Log the permit or event key alongside the scheduled time, lock acquisition or dedupe result, provider response, and persisted notification state. This makes it possible to distinguish a duplicate enqueue from a retry after an ambiguous provider response.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




