Use a recurring NestJS job to find permits approaching expiration, but do not send notifications directly from a fragile cron callback. A reliable design stores the permit’s expiration meaning, claims each reminder exactly once for processing, and delivers it through a retryable worker. For a simple deployment, @nestjs/schedule can run the scan; multiple application replicas, outages, or delivery retries require additional coordination and durable state.
Choose the scheduling approach
Start with a recurring database scan when the alert rule is predictable—for example, a daily sweep for active permits expiring within a configured reminder window. NestJS’s Task Scheduling package supports declarative cron jobs, timeouts, and intervals. Its ScheduleModule.forRoot() initializes the scheduler and registers declared jobs; import it in one module only, normally the root application module.
As an Amazon Associate I earn from qualifying purchases.
| Approach | Best fit | Trade-off |
|---|---|---|
In-process @Cron() scan |
A simple recurring sweep with modest operational requirements | Application lifecycle and coordination among replicas are your responsibility. |
| Durable workflow schedule | Missed-run and overlap behavior must be persisted and explicit | It adds a separate framework facility; verify compatibility with your NestJS version and deployment. |
| Queue-backed notification worker | Delivery retries or independent worker scaling are needed | Requires queue infrastructure and idempotent delivery handling. |
| Database claim or outbox | Durable work tracking close to permit data is desirable | Requires transaction design, monitoring, and operational handling of old records. |
These choices are complementary: a cron scan can populate an outbox or queue, and a worker can deliver the resulting messages. The basic scheduler does not provide a cross-replica lock or a durable record of work.
Define what “expires” means before storing dates
A permit may expire at a precise instant, at the end of a local calendar day, or on a date whose interpretation is defined by a jurisdiction. Confirm the rule with the authority that issued the permit; a technical scheduler cannot determine the legal deadline.
#1 Best Overall
For PostgreSQL, timestamp with time zone converts input to UTC for storage, then displays it using the session timezone. It does not retain the original zone supplied with the timestamp. If the permit’s local interpretation matters, store the applicable IANA timezone or jurisdiction separately. PostgreSQL also notes that timezone conventions can change for political reasons, including daylight-saving rules. See PostgreSQL Date/Time Types.
For a jurisdiction-defined calendar deadline, retain the legal date and governing timezone or jurisdiction, then calculate the reminder instant according to that policy. Do not assume that midnight UTC is the permit holder’s local end-of-day.
Set up the NestJS scheduler
Import the scheduler once and register a provider containing the cron handler. The official NestJS scheduling guide documents this pattern:
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { PermitAlertsService } from './permit-alerts.service';
@Module({
imports: [ScheduleModule.forRoot()],
providers: [PermitAlertsService],
})
export class AppModule {}
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
@Injectable()
export class PermitAlertsService {
private readonly logger = new Logger(PermitAlertsService.name);
@Cron('0 0 8 * * *', {
name: 'permit-expiration-scan',
timeZone: 'America/New_York',
waitForCompletion: true,
})
async scanForExpiringPermits(): Promise<void> {
// Find due permits and persist reminder work; do not send each message here.
}
}
This example uses a six-field cron expression, including seconds, to run at 08:00 in the specified timezone. Choose the timezone and cadence that match the product’s policy rather than copying the example blindly. NestJS also supports utcOffset; use either a timezone policy or an offset appropriate to the schedule. With waitForCompletion: true, the scheduler skips an invocation if the previous callback is still running. That option prevents overlap within that scheduler, not between application replicas.
Rank #3
Make the scan repeatable and safe
A scan should select eligible permits, create durable reminder work, and return. A process may restart, a scan may run again, and delivery may fail after work has been claimed. Design around those normal failure cases instead of assuming the callback runs exactly once.
- Define eligibility. Select active permits whose expiration instant falls within the configured alert window, using the agreed timezone and date semantics.
- Claim each reminder atomically. Insert a reminder record with a unique key such as permit ID, reminder offset, and destination. A database uniqueness constraint prevents a repeat scan from creating duplicate work for that same reminder.
- Persist work before delivery. Store an outbox record in the same transaction as the claim, or enqueue the work through a queue after establishing a durable claim. Avoid relying only on an in-memory list.
- Deliver asynchronously. A worker sends email, SMS, or another configured notification and records the result. Retry transient failures according to a defined policy.
- Keep an audit trail. Record when a reminder became due, was claimed, was attempted, and succeeded or failed, together with enough outcome detail to investigate delays or disputes.
Idempotency applies to delivery as well as selection. A worker can send successfully and then crash before recording success, causing a retry. Where the delivery provider supports an idempotency key, use the reminder’s stable identifier; otherwise make duplicate handling explicit and keep delivery status durable.
Rank #4
Index the query and coordinate database workers
Build an index around the actual scan predicates—often tenant or status together with expiration timestamp—and inspect the query plan against the real schema and data. A partial index may help when it covers a stable subset such as active permits. PostgreSQL partial-index predicates can reference only columns in the indexed table, and index expressions must be immutable; a rolling predicate such as “expires before now” is therefore not suitable. See PostgreSQL CREATE INDEX.
Recommended Free Tools
If multiple database workers claim rows, PostgreSQL’s FOR UPDATE SKIP LOCKED can keep them from waiting on rows already locked by another worker. Use it as part of a deliberate transaction-based claim protocol. PostgreSQL warns that skipped rows produce an inconsistent view, so this is appropriate for queue-like consumption, not general-purpose reads. It does not replace a unique reminder constraint or idempotent delivery. See PostgreSQL SELECT.
Best Value
Account for replicas, outages, and missed runs
Every running application instance that registers an in-process cron job may run its own copy. In a horizontally scaled deployment, do not assume one decorator means one global execution. Coordinate the scan through a distributed lock or database claim protocol, run a dedicated scheduler instance, or use a durable workflow mechanism when the failure model requires it.
NestJS’s separate Durable Workflows facility documents cron schedules, fixed intervals, and RFC 5545 recurrence rules, with timezone, missed-run, and overlap policies. Its documented missed-run modes are skip (the default), once (start the latest missed occurrence), and all (start missed occurrences, up to the latest 100); all requires overlap: 'allow'. This is a different approach from the basic scheduler, and availability depends on project version and suitability.
For asynchronous notification processing, NestJS’s Queues v9 guide describes queues as a way to scale backend work and separate it from the application process. The cited guide is version 9 documentation, so check compatibility with the project’s current NestJS version before adopting package-specific details. A queue supports independent processing and retries, but it does not by itself make the underlying reminder selection or external message exactly-once.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Quick Recap
Operational checks to add
- Alert on scan failures, queue or outbox backlog, and reminders that remain pending beyond their intended send window.
- Make the scan safe to rerun after a deployment or outage, and decide whether a delayed scan should process all due reminders or only the current window.
- Test daylight-saving transitions and date-boundary behavior for each supported jurisdiction.
- Keep reminder offsets and destination rules explicit, so a policy change does not silently resend old reminders.
- Test duplicate scans, simultaneous workers, worker restarts, provider timeouts, and failure after a provider accepts a message but before the database records success.
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.

