AdTech Runs on Schedules: Where Scheduler0 Fits in Your Ad Stack
Most software is event-driven. AdTech is clock-driven. A campaign goes live at 9 AM in Tokyo and pauses at 11 PM in São Paulo. Budgets pace in five-minute slices so a flight does not blow through its daily cap by lunch. Daypart multipliers raise bids during morning commutes and drop them at 2 AM. Audience segments rebuild every 15 minutes off a fresh CDP snapshot. Conversion postbacks fan out to a dozen partners with strict SLAs. Reconciliation reports between your ad server, DSP, and SSPs settle on an hourly cadence and have to agree.
Strip out the buzzwords and an ad platform is, in large part, an elaborate scheduler with creative and bidding logic bolted on top. Which is also why most ad platforms eventually outgrow whatever cron they started with. This post walks through the workloads in AdTech where a real distributed scheduler earns its keep, and shows how Scheduler0 handles each one — with concrete API calls and the trade-offs that actually matter when ad money is on the line.
Why generic cron struggles with AdTech
Before the use cases, it is worth being precise about why a crontab on a box, or a single managed cron service, tends to break down for ad platforms. Four properties of AdTech work make scheduling unusually demanding:
- Per-tenant timezones. A campaign's "midnight" is defined by the advertiser, not the server. A single platform routinely runs jobs in dozens of timezones at the same time. Storing a global
TZenv var and converting in application code is a class of bugs you do not want around the time DST shifts. - Money on every miss. A skipped pacing tick is overspend. A skipped postback retry is a partner blaming you for a missed conversion. Jobs must be HA, retried, and idempotent — and "idempotent" must mean "we will not double-bill the advertiser if the cluster restarts mid-tick".
- High fan-out, mixed targets. A single nightly job often has to refresh a feature store on AWS, push reports to an SFTP on Azure, and warm a cache on GCP — because the ad stack is rarely single-cloud. The scheduler needs to dispatch to all of them.
- Authoring by non-engineers. Media planners, campaign managers, and account managers want to schedule things. Teaching them cron syntax is not the right answer; neither is a queue of tickets to engineering.
Scheduler0 was designed around these four properties. Per-job timezones, first-class retries with SHA-256 idempotency keys, executors that target webhooks plus AWS/Azure/GCP cloud functions, and a natural-language /v1/prompt endpoint are not unrelated features — they are the same answer to the same shape of problem.
The workloads, one by one
Here are the recurring scheduling shapes in a modern ad stack, and the Scheduler0 primitives that map cleanly to each.
1. Campaign flight scheduling
Every campaign has a start, an end, and frequently a set of pause and resume windows in between (dark hours, weekend-only flights, week-long promotions). The "obvious" implementation — write the next start/end time into a row, then poll — works for ten advertisers and falls over at ten thousand.
In Scheduler0, a flight is a job per state transition, each with its own per-tenant timezone:
curl -X POST "https://api.scheduler0.com/v1/jobs" \
-H "Content-Type: application/json" \
-H "X-API-Key: $KEY" -H "X-Secret-Key: $SECRET" -H "X-Account-ID: $ACCT" \
-d '[
{
"projectId": 42,
"executorId": 11,
"spec": "0 0 9 * * MON-FRI",
"data": "{\"campaignId\":\"cmp_8821\",\"action\":\"activate\"}",
"timezone": "Asia/Tokyo",
"retryMax": 5,
"createdBy": "campaign-svc"
},
{
"projectId": 42,
"executorId": 11,
"spec": "0 0 23 * * MON-FRI",
"data": "{\"campaignId\":\"cmp_8821\",\"action\":\"pause\"}",
"timezone": "Asia/Tokyo",
"retryMax": 5,
"createdBy": "campaign-svc"
}
]'
Two things to notice. First, the timezone is on the job, not on the cluster — a Tokyo flight and a São Paulo flight live in the same project and the scheduler handles DST without your application code being involved. Second, the cron expression uses the six-field form (second minute hour dom month dow), which becomes important the moment you need sub-minute precision for the pacing jobs we are about to discuss.
If a node restarts during the cutover window, the job still fires: Scheduler0 recovers overdue executions on startup as long as the next scheduled time has not yet passed. A "go live at 9 AM" job does not silently disappear because of a deploy at 8:59.
2. Intraday budget pacing
Pacing is the canonical AdTech scheduling problem. You have a daily budget, you want to spend it smoothly (or front-loaded, or evenly distributed across dayparts), and you have to recalculate bid multipliers every few minutes based on actual spend. Miss too many ticks and you either underdeliver and lose the budget, or overdeliver and eat the overage.
This is the textbook use case for @every:
curl -X POST "https://api.scheduler0.com/v1/jobs" \
-H "Content-Type: application/json" \
-H "X-API-Key: $KEY" -H "X-Secret-Key: $SECRET" -H "X-Account-ID: $ACCT" \
-d '[{
"projectId": 42,
"executorId": 17,
"spec": "@every 1m",
"data": "{\"task\":\"pace_budgets\",\"shard\":\"us-east\"}",
"retryMax": 3,
"createdBy": "pacing-svc"
}]'
A minute-cadence pacing job is mundane in isolation. What matters is what happens when it fails. With a generic cron, a failed tick is a silent gap. With Scheduler0, the tick is retried up to retryMax times, and each retry attempt carries the same SHA-256 unique id derived from the job, project, last execution timestamp, and next scheduled time. Your pacing endpoint can use that id as a dedupe key — so an in-flight retry plus a recovered execution from a node restart will not double-debit the budget ledger.
For platforms running thousands of campaigns, shard the pacing work across executors (one job per region, per vertical, or per priority tier) instead of trying to compute everything in a single tick. The cluster will load-balance dispatch across peers, and the analytics endpoint gives you per-minute execution counts you can correlate with pacing accuracy.
3. Daypart bid adjustments
Daypart logic — "bid 1.2x between 7 AM and 9 AM, 0.8x between 1 AM and 4 AM" — is conceptually a step function over a 24-hour clock. The naive implementation is to evaluate it on every bid request, which is fine until you discover your bidder is now doing daypart math at 200k QPS for no good reason.
A cleaner pattern is to push the resolved multipliers into a hot cache on a schedule and let the bidder read them as a flat lookup. Each transition gets its own job:
curl -X POST "https://api.scheduler0.com/v1/jobs" \
-H "Content-Type: application/json" \
-H "X-API-Key: $KEY" -H "X-Secret-Key: $SECRET" -H "X-Account-ID: $ACCT" \
-d '[
{
"projectId": 42, "executorId": 19,
"spec": "0 0 7 * * *",
"data": "{\"campaignId\":\"cmp_8821\",\"multiplier\":1.2}",
"timezone": "America/New_York",
"retryMax": 3, "createdBy": "daypart-svc"
},
{
"projectId": 42, "executorId": 19,
"spec": "0 0 9 * * *",
"data": "{\"campaignId\":\"cmp_8821\",\"multiplier\":1.0}",
"timezone": "America/New_York",
"retryMax": 3, "createdBy": "daypart-svc"
},
{
"projectId": 42, "executorId": 19,
"spec": "0 0 1 * * *",
"data": "{\"campaignId\":\"cmp_8821\",\"multiplier\":0.8}",
"timezone": "America/New_York",
"retryMax": 3, "createdBy": "daypart-svc"
}
]'
The scheduler now owns the time logic and the bidder owns the bidding. When a daypart strategy changes you mutate jobs, not bidder code.
4. Audience and segment rebuilds
CDPs, lookalikes, and contextual segments need to be recomputed continuously. The cadence depends on the segment — a "cart abandoners in the last 30 minutes" segment rebuilds every few minutes; an "annual high-value customers" segment is a nightly batch.
Both shapes are jobs; what differs is the executor target. For the high-frequency segment, point the executor at a small webhook that triggers a stream job. For the nightly batch, point it at a Lambda or Cloud Function that orchestrates a heavier pipeline:
curl -X POST "https://api.scheduler0.com/v1/executors" \
-H "Content-Type: application/json" \
-H "X-API-Key: $KEY" -H "X-Secret-Key: $SECRET" -H "X-Account-ID: $ACCT" \
-d '{
"projectId": 42,
"name": "audience-rebuild-lambda",
"type": "aws_lambda",
"config": {
"region": "us-east-1",
"functionName": "rebuild-audience-segments"
},
"createdBy": "audience-svc"
}'
Because Scheduler0 dispatches over HTTPS and supports webhook plus AWS/Azure/GCP function executors, you can fan a single job out to whichever cloud the segment owner lives on without standing up a new scheduler per provider.
5. Conversion postbacks (the retry-and-idempotency story)
Of all AdTech workloads, postbacks are the one where retries earn their salary. A conversion fires, you owe one HTTP call to every partner pixel, and partners will absolutely complain — and sometimes dispute attribution — if the call is missed or duplicated.
You usually do not schedule postbacks as a recurring job; you schedule a one-shot delivery in response to an event. But the same retry-and-idempotency machinery applies. Create the job with a near-future spec, a generous retryMax, and a stable correlation id in the payload:
curl -X POST "https://api.scheduler0.com/v1/jobs" \
-H "Content-Type: application/json" \
-H "X-API-Key: $KEY" -H "X-Secret-Key: $SECRET" -H "X-Account-ID: $ACCT" \
-d '[{
"projectId": 42,
"executorId": 23,
"spec": "@every 1s",
"data": "{\"postback_id\":\"pb_98a1\",\"partner\":\"partnerX\",\"conversion_id\":\"conv_771\"}",
"retryMax": 10,
"createdBy": "postback-svc"
}]'
Your partner endpoint dedupes on postback_id. If the partner returns 5xx, Scheduler0 retries up to ten times with the same fingerprint, and a node restart in the middle of the retry loop does not produce an eleventh attempt. After a successful delivery, the postback service deletes the job. The net effect is exactly-once delivery semantics without you building the queue.
(For platforms that fire millions of postbacks a day, a queue is still the right primitive for the delivery. Scheduler0's role is the upstream scheduler — delayed delivery, retry orchestration, and per-tenant SLOs that your engineering team should not be reinventing.)
6. Reporting and reconciliation
Hourly reports between ad server, DSP, and SSPs are the unglamorous backbone of ad operations. Every hour a half-dozen pipelines pull, normalize, and diff impressions, clicks, spend, and viewability. Discrepancies above a threshold page someone.
This is a fan-out of jobs that need to run in a strict order, in many timezones, with retries on the inevitable partner API hiccups:
curl -X POST "https://api.scheduler0.com/v1/jobs" \
-H "Content-Type: application/json" \
-H "X-API-Key: $KEY" -H "X-Secret-Key: $SECRET" -H "X-Account-ID: $ACCT" \
-d '[
{
"projectId": 42, "executorId": 31,
"spec": "0 5 * * * *",
"data": "{\"task\":\"pull_dsp_report\",\"partner\":\"dsp_a\"}",
"timezone": "UTC",
"retryMax": 5, "createdBy": "recon-svc"
},
{
"projectId": 42, "executorId": 31,
"spec": "0 15 * * * *",
"data": "{\"task\":\"pull_ssp_report\",\"partner\":\"ssp_b\"}",
"timezone": "UTC",
"retryMax": 5, "createdBy": "recon-svc"
},
{
"projectId": 42, "executorId": 31,
"spec": "0 30 * * * *",
"data": "{\"task\":\"reconcile\"}",
"timezone": "UTC",
"retryMax": 3, "createdBy": "recon-svc"
}
]'
The pulls run at :05 and :15, the reconciliation runs at :30. If a partner is slow and the :05 job retries through :15, the :30 job still queues — and either picks up the late data or surfaces a discrepancy alert, which is the behavior you want. The /v1/job-executions/totals and /v1/job-executions/analytics endpoints feed dashboards that show recon pass rates over time, which is far more actionable than a wall of cron logs.
7. Programmatic deal activation
Private marketplace deals and programmatic guaranteed campaigns have crisp start and end times that often differ from a buyer's standard flight schedule (a flash sale window, a sponsorship around a live event). Each deal becomes a pair of jobs that flip its eligibility flag in the bidder's deal store. Because deals are tenant-scoped, use one Scheduler0 project per publisher or per agency and the deal jobs inherit project-level isolation, credentials, and analytics for free.
8. Letting media planners schedule things in plain English
The last use case is the one that quietly changes how an ad platform feels. Media planners do not want to write cron expressions for a "promote this creative every Friday at 6 PM ET for the next four weeks" task. The /v1/prompt endpoint converts natural language into a job spec your application can store directly:
curl -X POST "https://api.scheduler0.com/v1/prompt" \
-H "Content-Type: application/json" \
-H "X-API-Key: $KEY" -H "X-Secret-Key: $SECRET" -H "X-Account-ID: $ACCT" \
-d '{
"prompt": "Promote creative crv_5512 every Friday at 6 PM for the next 4 weeks, and pause it Sunday night",
"timezone": "America/New_York"
}'
The response is a structured set of jobs with generated cron expressions you can show in the UI before committing. Combined with per-project credentials, this is what makes "let agencies schedule their own campaigns in our self-serve" a feature you can actually ship.
How it fits together
Here is the shape of an AdTech stack with Scheduler0 sitting where the clock work lives:
Scheduler0 (Raft, HA)
+-----------------------------------------+
| campaign flights pacing ticks |
| daypart transitions postback fan-out |
| audience rebuilds recon pulls |
| |
| per-job timezone, retryMax, |
| SHA-256 idempotency, analytics |
+------+-------------+--------------+-----+
| | |
webhook URL AWS Lambda Azure / GCP Fn
| | |
v v v
+------------+ +-----------+ +--------------+
| bidder | | audience | | reporting |
| pacing svc | | rebuilder | | reconciler |
| postback | | feature | | SFTP / API |
| dispatcher | | store | | adapters |
+-----+------+ +-----+-----+ +------+-------+
| | |
+-------+------+--------------+
v
+-------------------------+
| Postgres / warehouse |
| Redis / hot caches |
| S3 / data lake |
+-------------------------+
The point of the diagram is not that there are many boxes — it is that the scheduler is not in the data path of a bid request. It is the control plane that makes sure the data and config the bid path consumes is fresh, paced, and correct.
Operational notes
A few things worth doing on day one if you are running AdTech workloads through Scheduler0:
- One project per advertiser, agency, or business line. This gives you isolated credentials, analytics, and AI settings, and it keeps "this advertiser is misbehaving" containable.
- Pick
retryMaxbased on the partner SLA, not a default. Postbacks usually want the upper end (10+). Recon pulls want fewer (5) so you do not mask a partner outage as a long retry. Pacing wants 3 so a stuck tick fails fast and the next tick takes over. - Use the analytics endpoint for SLOs. "Pacing executions per minute, success rate" is a real SLO. The
/v1/job-executions/analyticsand/v1/job-executions/totalsendpoints are the easiest way to feed it without scraping logs. - Treat the prompt endpoint as a UX primitive. Even internal tooling benefits from "describe the schedule in English" rather than "fill in this cron field". It will reduce the volume of one-off scheduling tickets your platform team takes.
- Self-host when latency to your executors matters. A Scheduler0 cluster co-located with your bidder region keeps dispatch latency low; the managed service is fine for most reporting and audience workloads.
Closing
AdTech is not unique in needing a scheduler. It is unique in how many kinds of scheduling it needs in the same product — global timezones, sub-minute precision, multi-cloud fan-out, retry semantics that mean money, and a customer surface that includes non-engineers. The reason ad platforms keep building bespoke scheduling layers is that generic cron does not give you those properties together.
Scheduler0 does. The full surface — jobs, executors, the prompt API, analytics, self-hosting — is in the documentation and the API reference. If you are starting an ad platform, lean on it from day one. If you are running one already, the highest-leverage place to start is usually pacing and postbacks — those two workloads alone will pay for the migration in saved overspend and recovered conversions.
