Two clocks
DripDeck publishes videos at 6pm without needing to be awake at 6pm. The trick is letting the platform hold the second clock.
DripDeck drip-feeds videos to YouTube and TikTok on a schedule you set once. The obvious way to build that is a job that wakes at 6pm and uploads.
It does not do that, and the reason is the most useful thing in the codebase.
The problem with one clock
A scheduler that uploads at the publish moment has to be alive at the publish moment. That single requirement drags in everything else:
- The machine has to be awake at 6pm. Laptops sleep.
- The upload has to finish by 6pm, so really it has to start at 5:52, except a large file on a bad connection might need until 5:40.
- If the network is down between 5:40 and 6:00, the slot is missed. Not delayed — missed, because 6pm has passed and the reason it was 6pm is gone.
- Retrying after 6pm publishes at 6:11, which is a different decision from the one the user made.
Every one of those is a real failure and they all share a cause: the moment of publication and the moment of work were forced to be the same moment.
Uploading and publishing are two different events. Insisting they happen together is what makes the schedule fragile.
Let the platform hold the second clock
YouTube accepts an upload with privacyStatus: private and a publishAt
timestamp. It stores the video and flips it public itself, at the specified
instant, on Google's infrastructure.
So DripDeck uploads early — hours early, whenever it happens to be running and the network is good — and hands the publish time to YouTube:
await youtube.videos.insert({
part: ["snippet", "status"],
requestBody: {
snippet: { title, description, tags },
status: {
privacyStatus: "private",
// The second clock. YouTube owns this moment, not us.
publishAt: slot.publishAt.toISOString(),
selfDeclaredMadeForKids: false,
},
},
media: { body: createReadStream(file) },
})
The uploading machine can now be off at 6pm. It can be off from 6:01pm until tomorrow. The video goes live on time because the entity publishing it is a datacentre that does not sleep.
What was a hard real-time constraint became a deadline with hours of slack, and a deadline with slack is a completely different engineering problem. Retries become ordinary. A failed upload at 2pm can be retried at 3pm and nothing is lost.
The part that makes it self-healing
Two clocks fixes the machine being asleep at 6pm. It does not fix the machine being off all day.
So the scheduler does not ask "what should I do now?" It asks "what should have happened by now that has not?":
// Not: find slots due in the next N minutes.
// Instead: find every slot that is unfulfilled, including ones in the past.
const pending = await db.query(
`select * from slots
where status = 'scheduled'
and publish_at < now() + interval '48 hours'
order by publish_at asc`,
)
for (const slot of pending) {
if (slot.publishAt < new Date()) {
// The moment passed while we were off. The user asked for a cadence,
// not for this specific minute — so reassign to the next free slot
// rather than publishing late or dropping it.
await reassignToNextOpenSlot(slot)
continue
}
await uploadEarly(slot)
}
The difference is entirely in the where clause. A query bounded to the next
few minutes silently drops everything it was not awake for. A query over
everything unfulfilled discovers the gap on the next run and repairs it.
That is what "it heals itself" means in practice. Not an elaborate recovery
subsystem — a query that looks backwards as well as forwards, and a state
machine where the resting state is scheduled rather than probably fine.
The distinction that took a rewrite to see
There are two kinds of missed schedule and they need opposite handling:
A missed moment. The 6pm slot passed while the machine was off. The user wanted a video that evening. Publishing it at 9am the next day is worse than moving it to the next evening slot. Reassign.
A missed upload. The 6pm slot has not arrived yet, but the upload that was meant to happen at 2pm failed. Nothing is lost. Retry, now.
The first version treated both as "failed job, retry with backoff", which produced videos published at 3am — technically a successful retry, and exactly not what anyone asked for. The fix was not better retry logic; it was noticing that a schedule expresses an intent about when people watch, and a retry policy that ignores that intent is optimising the wrong thing.
Where else this applies
The pattern generalises well past video:
- Scheduled email. Hand the send time to the provider rather than holding it.
- Payments. Most processors accept a future-dated charge.
- Content publishing. A CMS with a publish timestamp is the same trick.
Whenever an external system will hold a timestamp for you, giving it the deadline converts a hard real-time requirement into a soft one. That trade is almost always worth taking, because the platform's clock has better uptime than yours and it is free.
The general rule underneath: do the work early, and let the moment be somebody else's responsibility.
DripDeck is one of ours. More in why our crawler is deliberately slow, the case studies, and agentic workflows.