Serverless Scheduling: Beyond Platform Cron
Platform cron handles fixed recurring tasks. Production apps need dynamic scheduling — per-user timers, event-driven deadlines, retries. Posthook is the scheduling layer that works with any serverless platform.
Last updated: March 24, 2026
Serverless platforms handle “something happens now, respond to it” well. They do not handle “remember to do something later” well. When you deploy to Vercel, Netlify, Cloudflare Workers, or AWS Lambda, you get request handling, edge routing, and automatic scaling — but you lose the ability to schedule work for a specific future time.
Platform cron features partially fill this gap. Vercel Cron Jobs, Cloudflare Cron Triggers, and Netlify Scheduled Functions all support fixed recurring schedules defined at deploy time. But most production scheduling is dynamic — triggered by user actions, with variable future times, needing retries and visibility when things fail. “Send this user a reminder in 24 hours” is one API call with a scheduling service and fundamentally impossible with platform cron.
What platform cron can do
Every major serverless platform offers some form of cron:
- Vercel Cron Jobs — cron expressions in
vercel.json. 100 jobs per project. Daily precision on Hobby, per-minute on Pro. No retry on failure. UTC only. Logs retained 1 hour (Hobby) or 1 day (Pro). - Cloudflare Cron Triggers — cron expressions in
wrangler.toml. 250 triggers on Paid plan. Per-minute precision. No retry. 30-second execution limit. - Netlify Scheduled Functions — cron expressions in function config. Per-minute precision. Background functions get 15-minute execution limit. No retry. No execution history UI.
- AWS EventBridge Scheduler — the most capable option. Cron or one-time schedules via API. Up to 10M schedules per region. Configurable retries. Timezone-aware. But requires IAM roles, SDK integration, and deep AWS infrastructure knowledge.
| Capability | Platform cron (Vercel, Cloudflare, Netlify) | AWS EventBridge Scheduler |
|---|---|---|
| Dynamic scheduling | No — requires redeployment | Yes — API-driven, supports one-time |
| Retry on failure | None | Configurable |
| Timezone support | UTC only | UTC and timezone-aware |
| Delivery visibility | Ephemeral function logs | CloudWatch Logs |
| Infrastructure | Zero — included with platform | IAM roles, SDK integration, AWS account |
Platform cron is the right tool for what it was designed for: a handful of fixed recurring tasks with zero configuration cost. Daily database cleanup, hourly cache warming, periodic data sync — these fit the model cleanly.
AWS EventBridge Scheduler stands apart as the most capable option. It supports one-time schedules created via API, configurable retries, and up to 10 million schedules per region. But it requires an AWS account, IAM roles, SDK integration, and deep infrastructure knowledge — which is often the complexity serverless developers chose their platform to avoid.
The dynamic scheduling gap
The limitation is not that platform cron is unreliable. The limitation is that most scheduling work in a production application is not fixed-cadence. It is event-driven.
“Send this user a reminder in 24 hours.” Each user signs up at a different time. You need a timer per signup event, not a single recurring schedule. Platform cron cannot express this.
“Expire this trial 14 days after activation.” Each trial starts on a different date. Platform cron has no concept of per-entity deadlines. The common workaround is a cron job that scans a database every minute — a workaround with its own failure modes.
“Retry a failed payment next Tuesday at 10am in the user’s timezone.” This requires scheduling a one-time future delivery, in a specific timezone, with DST handling. Not expressible with any platform cron feature except EventBridge.
Cancel or reschedule a pending timer. The appointment was moved — cancel the old reminder and schedule a new one. Platform cron has no API for dynamic schedule management.
Know when a scheduled task failed. Platform cron has no retry mechanism and no alerting. If a cron invocation fails, the failure sits in ephemeral logs until it ages out.
These are not edge cases. They are the core scheduling problems in SaaS applications: reminders, expirations, follow-ups, retries, and notifications — each tied to a specific user, order, or event.
The polling workaround
The most common workaround for dynamic scheduling on serverless is a cron job that polls a database:
- Store scheduled tasks in a table with a
run_attimestamp - Set up a cron job that runs every minute
- Query for tasks where
run_at <= NOW()and process them
This works at low volume. At production scale, it becomes its own operational surface:
- Database load. Every poll cycle runs a query, even when nothing is due. Add
SELECT ... FOR UPDATE SKIP LOCKEDfor concurrency safety, and contention grows with volume. - Timing imprecision. Poll every minute and tasks can fire up to 59 seconds late. Poll every 10 seconds and database load increases 6x.
- Concurrency issues. Multiple serverless function instances polling simultaneously can pick up the same work. Preventing duplicates requires distributed locking — more infrastructure to maintain.
- Connection exhaustion. Each serverless invocation opens a new database connection. Without connection pooling (PgBouncer, Neon Pooler), the database runs out of connections under load.
- Missed windows. If the cron trigger fails — cold start, platform issue, function timeout — an entire interval of work is missed with no retry.
- Cost at idle. A Vercel Pro cron running every minute is 43,200 function invocations per month, even when there is nothing to process.
The polling pattern is not wrong — it is a scheduling system. And maintaining a scheduling system is the infrastructure that serverless was supposed to eliminate.
How Posthook fills the gap
Posthook is a managed scheduling layer. When an event happens in your application — a user signs up, a trial activates, an order is placed — you schedule a hook via API. Posthook persists the schedule, delivers it at the right time via HTTP POST or WebSocket, retries on failure, and tracks every delivery attempt.
No database polling. No cron scanning for work. Each event creates its own timer.
import Posthook from "@posthook/node";
const posthook = new Posthook("phk_...");
// When a user signs up, schedule a reminder for 48 hours later
await posthook.hooks.schedule({
path: "/api/hooks/onboarding-reminder",
postIn: "48h",
data: { userId: "user_abc", step: "complete-profile" },
});
// When a trial activates, schedule expiration at a specific time
await posthook.hooks.schedule({
path: "/api/hooks/expire-trial",
postAt: "2026-04-07T00:00:00Z",
data: { trialId: "trial_xyz" },
});
// Schedule in the user's local timezone (DST-safe)
await posthook.hooks.schedule({
path: "/api/hooks/payment-retry",
postAtLocal: "2026-03-31T10:00:00",
timezone: "America/New_York",
data: { paymentId: "pay_123" },
});
When the hook fires, your handler checks state and decides what to do:
// app/api/hooks/onboarding-reminder/route.ts
export async function POST(req: Request) {
const { data } = await req.json();
const user = await db.users.findById(data.userId);
// Handler checks state — act or skip
if (!user || user.profileCompleted) {
return Response.json({ skipped: true });
}
await sendOnboardingReminder(user);
return Response.json({ sent: true });
}
This works on Vercel, Netlify, Cloudflare Workers, AWS Lambda — any platform with an HTTP endpoint. The handler is a standard serverless function. No new runtime, no SDK lock-in on execution. To see these patterns running end-to-end, try the live Next.js demo or clone the starter repo.
Key capabilities:
- Three scheduling modes —
postAtfor exact UTC timestamps,postAtLocal+ timezone for DST-safe local scheduling,postInfor relative delays - Retries with configurable backoff — fixed, exponential, or jitter, with per-hook overrides at scheduling time
- Per-delivery observability — inspect each hook’s attempt history, status code, response body, and error details from the dashboard
- Anomaly detection — per-endpoint failure rate tracking against historical baselines, with alerts via email, Slack, or webhook
- Incident response — bulk retry, cancel, or replay failed hooks filtered by time range, endpoint, or sequence
- Sequences for recurring work — calendar scheduling with DST handling, dependency graphs, and config-as-code via
posthook.toml
Three categories of serverless scheduling solutions
If you need dynamic scheduling on a serverless platform, the options fall into three categories:
Workflow execution platforms
Inngest, Trigger.dev, Temporal. These are full execution engines. You write functions using their SDK, and your code runs in their infrastructure (or self-hosted infrastructure for Temporal). They handle scheduling, retries, branching, multi-step orchestration, and durable state.
The right tool when the problem is genuinely a workflow — multi-step processes with branching logic, long-running operations, and complex state machines. Broader scope, steeper adoption curve. See Inngest / Trigger.dev vs Posthook and Temporal vs Posthook.
Scheduling and delivery services
Posthook, QStash. API call in, HTTP delivery out. Your code runs in your infrastructure. The service handles persistence, timing, delivery, and retries.
The right tool when the problem is “schedule an HTTP call for later” with delivery guarantees, retries, and observability. Simpler integration model — no new runtime, no SDK abstraction over your execution. See QStash vs Posthook.
DIY infrastructure
Cron + database polling, Redis queues (BullMQ), custom queue workers. Maximum control over every aspect of scheduling and execution. You own the infrastructure, the retry logic, the monitoring, and the operational burden.
The right tool when you have specific infrastructure requirements, an existing ops team, or workloads that do not fit managed service pricing. The tradeoff is engineering time building and maintaining scheduling infrastructure instead of building product. See Cron vs durable scheduling and BullMQ vs Posthook.
When platform cron is enough
Platform cron is the right choice when:
- The schedule is static and known at deploy time — daily cleanup, hourly sync, periodic cache warming
- You have a small number of recurring tasks (not per-user or per-event timers)
- You do not need retries, delivery tracking, or failure alerting
- UTC-only timing is acceptable
- You want zero additional configuration, billing, or external dependencies
If the entire scheduling need is “run this database cleanup every night,” a cron expression in vercel.json or wrangler.toml is the simplest and most direct solution. There is no reason to add another service for that.
The decision point is when scheduling becomes dynamic. The first time a user action needs to create its own timer — a reminder, an expiration, a follow-up — platform cron cannot express the problem, and the polling workaround starts accumulating operational cost.
Frequently asked questions
Ready to get started?
Create your free account and start scheduling hooks in minutes. No credit card required.