
Every project eventually needs something to run on a schedule. Send a daily digest email. Clean up expired sessions. Rotate logs. Sync data from an external API. On a single server, you would reach for cron and be done in five minutes. In the cloud, you have more options, and the differences between them matter more than you might expect.
The core question with any scheduler is what happens when a run fails or a machine is unavailable. A traditional cron job on one server simply does not run if that server is down. Some of your scheduled work might be fine with that. Some of it, like billing or backups, is not. Choosing the right tool is mostly about matching the guarantee to the job.
If you are running a single VPS, plain cron is still a perfectly reasonable choice. It is simple, it is already installed, and you can see exactly what is scheduled by reading one file. The catch is that it has no retry logic and no visibility. If a job fails, you find out by reading logs, if you remember to. For low-stakes jobs on a machine you control, that is fine.
Managed schedulers from cloud providers, like AWS EventBridge Scheduler or Google Cloud Scheduler, give you cron-like syntax with a few upgrades. They run independently of your servers, so a dead instance does not stop the job. They can retry on failure, and they can invoke a Lambda, a container, or an HTTP endpoint. The tradeoff is that you are now configuring the job in the provider's console or IaC, and debugging a failed run means digging through their logs rather than tailing a file on your server.
Platform-native schedulers, like Vercel Cron or Render Cron Jobs, are the easiest option if you are already hosting there. You define the schedule in a config file, point it at an endpoint in your app, and the platform handles the rest. The downside is portability. If you move hosts, you rewrite the schedule. For projects that are happy on one platform, this is often the best balance of simplicity and reliability.
There is also the option of a dedicated job runner, like a small worker process that polls a queue. This is more work to set up, but it gives you the most control. You can retry, you can run jobs in parallel, and you can observe everything through your own metrics. This is worth it when scheduled work is a core part of your product rather than a background chore.
Whatever you choose, the single most valuable thing you can add is a heartbeat. Have your job record a timestamp somewhere every time it runs successfully, whether that is a row in a database, a file in object storage, or a metric in your monitoring system. Then set up an alert that fires if the timestamp is older than expected. This catches the failure mode that schedulers handle worst: the job that simply stops running and nobody notices for a week.
Log the start and end of every run, along with how long it took and whether it succeeded. If a job normally takes thirty seconds and suddenly takes ten minutes, something changed and you want to know before it starts timing out. If a job that usually processes a hundred records suddenly processes zero, that is a signal too. These patterns are invisible without logging, and they are often the first sign of a problem upstream.
Keep your jobs idempotent where you can. If a job runs twice because of a retry, it should not double-charge a customer or send a duplicate email. A common pattern is to record a unique key for each unit of work and skip anything already processed. This makes retries safe and lets you re-run a job manually without worrying about the consequences. It is a small habit that pays off the first time a scheduler misbehaves.