I've built scheduling infrastructure from scratch. Twice. The first time was for a client project at automatics.dev — a workflow automation that needed to fire webhooks and emails on a schedule. I figured a week, maybe two.
It took two months.
The second time I knew exactly what I was getting into, and it still took six weeks. That's the thing about schedulers — the first version is always easy. It's everything after that kills you.
Here's how it usually starts. You set up node-cron or a setTimeout. It fires on schedule. You write a function that sends an email via SendGrid. It works. You feel good. You think: "What was everyone complaining about?"
Then SendGrid returns a 429 because you hit a rate limit. Your notification just disappears — no retry, no record. You add retry logic with exponential backoff. Then you realize retried jobs need a maximum attempt count, and jobs that exhaust all retries need a dead letter queue so you can investigate later.
You deploy to production with two server instances. Both fire the same job at the same time. Users get duplicate emails. Now you need distributed locking. Redis Redlock? Database advisory locks? ZooKeeper? Each option comes with its own set of failure modes. I went with Redlock the first time and database locks the second time. Neither was fun to debug at 2 AM.
Then the product team wants Slack notifications. And Discord. And a webhook for the Zapier integration. Each channel has its own API, its own error responses, its own rate limits. Your "simple scheduler" now has five delivery adapters, and each one needs its own retry logic because Slack's 429 handling is different from SendGrid's.
Somewhere around week six, you realize you need timezone handling. "Send every Monday at 9 AM in the user's timezone" sounds trivial until DST changes and your 9 AM notification fires at 8 AM for half your users. You pull in the IANA timezone database. The edge cases compound.
By this point you've built: an API to accept schedule requests, a scheduler that scans for due jobs, a job queue, a worker pool, retry handlers per channel, a dead letter queue, distributed locks, and maybe some basic monitoring. That's nine subsystems, and you haven't even gotten to proper observability, provider failover, or rate limiting yet.
I counted once — the full scheduler I built for a production system had eleven distinct subsystems. Each with its own failure modes. Each needing maintenance.
And the maintenance is where it really gets you. The person who builds the scheduler understands the retry edge cases and the locking logic. When that person moves to another project — or leaves the company — the system becomes a black box that everyone's afraid to touch. I've seen it happen twice, once with my own code.
"But what about BullMQ?"
I like BullMQ. It's a solid Redis-backed queue with delayed and repeatable jobs. But it's a primitive, not a solution. You still build everything on top: the scheduling API, multi-channel delivery, provider failover, timezone handling, status tracking, monitoring. BullMQ gives you the engine block. You still need to build the rest of the car.
"What about Novu or Knock?"
I've looked at them closely. Novu, Knock, and Courier are notification platforms — they're great at routing a notification through the right channel at the right time. But scheduling isn't their core concept. To schedule a notification for next Tuesday at 3 PM, you set up a workflow with a delay step and trigger it immediately, hoping the delay resolves correctly. There's no "POST a reminder with a future datetime" as a first-class API. It's possible, but it's not what these platforms were designed for.
Here's what the Pingfyr version looks like:
curl -X POST https://pingfyr.com/api/remind \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "channel": "email", "recipients": ["user@example.com"], "title": "Your trial expires tomorrow", "body": "Upgrade now to keep your data and continue using all features.", "fire_at": "2026-04-01T09:00:00Z", "timezone": "America/New_York" }'
Retry logic, delivery logging, timezone handling, seven delivery channels — it's all there. I built it specifically because I got tired of rebuilding the same eleven subsystems for every project that needed scheduled delivery.
To be honest, there are cases where building your own makes sense. If you're sending hundreds of millions of notifications a month, you probably need custom infrastructure tuned to your specific throughput patterns. If scheduling is the core of your product — if that's what you're selling — then yeah, own it. If you have a dedicated infrastructure team that needs a project, go for it.
But for most of us — solo developers, indie hackers, small SaaS teams — spending weeks on scheduling infrastructure means weeks not spent on the actual product. And in my experience, the build cost is the easy part. It's the maintenance that quietly eats your time for years afterward.
Try Pingfyr free — no credit card required.
New channels, API features, and developer guides — straight to your inbox. No spam.