🎧 Listen to this article: English
🌍 Read this in your language: हिंदी · தமிழ் · తెలుగు · ಕನ್ನಡ · മലയാളം · ଓଡ଼ିଆ · 日本語 · 中文
When working with software, it's crucial to ensure that tasks run smoothly and without duplication. We’re diving into a common pitfall in distributed systems: how an in-process scheduler can cause a nightly job to run multiple times, leading to duplicate records in your database. This topic is particularly relevant as more companies are adopting microservices architecture, where services scale horizontally.
The Setup
Imagine you have a nightly job that creates records for active entities in your application. This job is supposed to run once a day, creating one new record for each entity. In a typical setup using NestJS, a popular framework for building server-side applications, you might use the @Cron decorator to schedule this job. Here's a simplified version of what that might look like:
@Injectable()
export class WindowGenerationService {
@Cron('0 8 * * *') // every day at 08:00
async generateNextWindows() {
const entities = await this.repo.findActiveEndingSoon();
for (const entity of entities) {
await this.repo.createNextWindow(entity);
}
}
}
This code works perfectly when you have a single instance of your service running. However, when you scale your application horizontally by running multiple instances, things can go wrong.
The Problem
In a scaled setup, if you have three instances of your service, each instance will run its own copy of the scheduled job at the same time. So, instead of one job running at 08:00, you end up with three jobs firing off simultaneously. Each instance creates its own records without checking if they already exist. This results in duplicate entries in your database, where two identical records for the same entity are created seconds apart.
Why This Matters
Duplicate data can lead to various issues, including data integrity problems and wasted resources. Not only are you creating unnecessary records, but you are also paying for the compute resources of three containers doing the same work.
Possible Solutions
After identifying the issue, there are several ways to address it:
Option 1: Use a Distributed Lock
One solution could be to implement a distributed lock. This would allow instances to compete for a lock, letting only one instance run the job. However, this approach has its downsides. If the lock backend fails or if an instance dies while holding the lock, the job might not run at all, leading to missed executions.
Option 2: Implement Idempotency
Another option is to add an idempotency check. This means that if the job runs again, it will simply skip creating records if they already exist. While this is a cheaper solution, it still wakes up all instances, leading to redundant work.
Option 3: Move Scheduling Outside the App
The best solution, as discovered, is to take scheduling out of the application. Instead of having the app manage when jobs run, let an external scheduler handle it. This way, you can create an HTTP endpoint that the external scheduler triggers once at the scheduled time. Here’s how that might look:
@Post('jobs/run')
async runJob(@Body() body: RunJobDto) {
this.assertValidSecret(body.secret);
return this.jobs.run(body.jobKey);
}
With this setup, only one instance will respond to the job trigger, preventing duplicate records from being created. The idempotency check remains in place to ensure that even if the job is triggered multiple times, it will not create duplicates.
What It Costs
When you expose a job as an HTTP endpoint, you need to consider security. Anyone who finds this endpoint could potentially trigger it. To prevent unauthorized access, a shared secret is used to validate requests. This ensures that only legitimate triggers can execute the job.
Another consideration is the scheduling time. The job should run after the start of the day for all users, which can be tricky when dealing with multiple time zones. Choosing a fixed UTC hour can help standardize this across different regions.
The Lesson
This experience highlights a critical lesson: an in-process scheduler can lead to unexpected behavior in a distributed system. When scaling your application, it’s essential to separate the scheduling concern from the application logic. By doing so, you can avoid the pitfalls of duplicate writes and ensure that your jobs run as intended.
Conclusion
In summary, managing cron jobs in a microservices architecture requires careful planning. By moving scheduling outside of your application and implementing proper checks, you can prevent issues like duplicate records and improve the efficiency of your services.
Merits
- Prevents duplicate records in the database.
- Reduces unnecessary compute costs.
- Simplifies job management by externalizing scheduling.
Demerits
- Requires additional setup for an external scheduler.
- Introduces complexity in handling HTTP requests and security.
Caution
This article serves as an educational overview. Be sure to replace any placeholder values with your actual configurations and verify claims against the original source before relying on them.
Frequently asked questions
- What is a cron job? — A cron job is a scheduled task that runs automatically at specified intervals.
- Why did my cron job run multiple times? — This can happen if the job is scheduled in multiple instances of your application.
- How can I prevent duplicate records in a database? — Implement idempotency checks or move scheduling outside the application.
- What is idempotency in programming? — Idempotency means that a function can be called multiple times without changing the result beyond the initial application.
- What is NestJS? — NestJS is a framework for building efficient, scalable Node.js server-side applications.
- How do I secure my HTTP endpoints? — Use authentication methods like shared secrets or tokens to validate requests.
Tags
#cron #nestjs #microservices #scheduling #softwareengineering #dataintegrity #cloudcomputing #distributedsystems
Docker Security Checklist
Lock down your containers from build to runtime — 29 practical controls covering images, runtime flags, secrets, and the daemon. Enter your email — you'll get the PDF instantly, plus new posts on Docker, Linux & security.
Free. No spam — unsubscribe in one click.


Responses
Sign in to leave a response.