Back to Explore
GuideVibeFix 编辑部Updated Nov 7, 2026

Set a Time and Mean It: Scheduled Publishing for Vibe Projects

Scheduled publishing looks like five words — send it when the time comes — and turns out to be a chain of engineering decisions. This guide covers the schedule data model and six-state machine, the three scheduler designs, the timezone traps, calendar views and conflict rules, retries and failure handling, and a 13-item launch checklist.

Real photograph: a paper calendar on a wooden desk with colourful push pins marking dates.

First, a fictional teaching example

Xiaoman built a small tool called "Morning Letterbox": users write an issue and set it to go out to subscribers at 7:30 in the morning. In its first week, a user wrote Monday's letter at 2 a.m., scheduled it for Monday 7:30, and went to bed reassured. At 7:30 on Monday, nothing happened. At 8:00 the user asked in the group chat: where is my letter? Xiaoman checked the backend and found the issue still sitting in "scheduled" status — the scheduler had never run that night, because the server had restarted in the early hours and the scheduled jobs, which lived in memory, had vanished with the process.

This example is fictional: the name, the product, and the events are all made up to make a point. But the problem it points at is real. Scheduled publishing looks like five words — "send it when the time comes" — and turns out to be a whole chain of engineering decisions. Where the time is stored, who triggers the send, what happens when it fails, how a user takes it back: every link in the chain has a trap in it. This guide walks through those decisions one by one.

Real photograph: a paper calendar on a wooden desk with colourful push pins marking dates.

Scope first: what this guide covers, and what it does not

Scheduled publishing is easily confused with several neighbouring features, so let us draw the boundaries before anything else, or you will build the wrong thing halfway through.

First, draft autosave is not scheduled publishing. Autosave solves "don't lose the draft"; scheduled publishing solves "send it on time". The two can share a table, but they should not share a status field, or a draft that is both "saved" and "scheduled" will leave you unable to say which step it is actually in when something goes wrong.

Second, timezone handling is not scheduled publishing itself. Timezones are the part of scheduled publishing most likely to blow up, and this guide gives them their own section — but relative-time display and the rest of timezone presentation belong elsewhere, so this guide only covers the parts scheduled publishing itself must handle.

Third, background cron jobs are not scheduled publishing either. Cleaning up expired data and running nightly statistics are jobs you run for yourself; scheduled publishing runs jobs on behalf of users, and when one fails the user has to be told, the send has to be recoverable, and the schedule has to be cancellable. The bar is completely different.

Fourth, lifecycle email sequences are not scheduled publishing. A sequence is about "what to send on day N of a user's journey"; scheduled publishing is about "a time the user picked with their own hands". In the first case the system computes the time; in the second the user owns it, including the right to change it, cancel it, or send early.

With the boundaries drawn, this guide is about one thing only: a user picks a publish time, the system sends the content on time, nothing goes wrong in between, and if something does go wrong, there is an explanation.

Whose problem does scheduled publishing actually solve?

Before writing any code, ask: why do your users need scheduled publishing? The answer is usually three kinds of people, with three completely different ways of using it.

The first is the content creator. Inspiration strikes at midnight; they write it, and they want to sleep, but their readers only read in the morning. Without scheduled publishing they either stay up waiting to hit send, or drag themselves up in the morning to send it — both are stupid. For them, scheduled publishing buys sleep: the certainty of "once it's written, I can let it go".

The second is the person doing operations. They plan a week of content: an update announcement on Monday, a tutorial on Wednesday, an event preview on Friday. For them, scheduling is a visible work plan, and the point is not "send it on time" but "see at a glance what goes out this week and what is still missing". These users need a calendar view more than anything; a lone time picker is not enough.

The third is the ordinary user setting a time for themselves: a birthday greeting, an anniversary reminder, a message to a group at a set hour. They use it a few times a year, but every single time it must not fail. For them, scheduled publishing buys "I won't forget", and reliability matters ten times more than fancy features.

Three kinds of people, three acceptance criteria. Creators want "on time", operators want "visible at a glance", ordinary users want "don't mess it up". Build for only one of them and the other two will come to you with complaints sooner or later. So decide before you build: which kind of person is your product mainly for, and how will you look after the other two? Making that judgement up front is far cheaper than reworking it later.

The data model: the minimum fields one scheduled publish needs

The most common mistake in scheduled publishing is treating it as a single time field. Someone adds a publish_at column to the content table, flips a status when the time arrives, and thinks the job is done. That works on day one and breaks by day three: what happens when the user wants to cancel? How do you retry a failed send? How do you explain the same piece going out twice?

The right approach is to model "one scheduled publish" as its own record. Content is one thing; a schedule is another. One piece of content can have several schedule records in its history: first scheduled for Monday 7:30, then the user moves it to Tuesday 7:30, the first record is voided, the second takes effect. A schedule record needs at least these fields: the content it belongs to, the target publish time, the current status, the creation time, the time of the most recent send attempt, the failure count, and the next retry time.

The state machine is the part most worth spending time on. I suggest exactly six states: draft, scheduled, sending, sent, failed, and cancelled. Do not add more; every extra state adds branches, and every branch is a place for bugs to live. The key rule is that every transition has a clearly identified actor: users may only schedule, reschedule, or cancel; the system may only start sending, mark success, mark failure, or retry. A user must not be able to set a status directly to "sent", and the system must not be able to set a status directly to "cancelled". Write that permission boundary into the code, not just into the docs.

For time fields, there is exactly one standard: store UTC timestamps. Do not store "the user's local time as a string", do not store "text with a timezone", do not store "server local time". Every comparison, sort, and trigger decision uses UTC; conversion to the user's timezone happens only at display time. This looks like a trivial detail, but it is the premise for the whole timezone discussion later, so lock it in here.

There is one more field beginners almost always forget: the idempotency key. Sending must be safe to retry: no matter how many times the scheduler triggers the same piece of content under the same schedule, it goes out exactly once. The key can be the schedule record's id or a separately generated string; what matters is that the send endpoint checks the key before doing anything, and if it has already been sent, it returns success without sending a second copy. Retrying without an idempotency key is just scheduling duplicate-send accidents for yourself.

Real photograph: a paper calendar on a wooden desk with colourful push pins marking dates.

How to build a scheduler you can trust

With the data model settled, the next question is: when the time comes, who triggers the send? There are three common routes, and which one you pick depends on your scale and budget. Pick wrong and you either waste money or get woken up at night.

The first route is polling on a timer: a job runs every minute, finds records that are "scheduled and past their publish time", and sends them one by one. This is the simplest approach and the one I recommend starting with. A one-minute cron plus a status field can carry scheduling for thousands of users. Its drawback is minute-level precision — if your scenario demands second-level punctuality, this is not enough. But honestly, for almost all content publishing, minute-level precision is plenty; no user can tell the difference between a send at 7:30:00 and one at 7:30:40.

The second route is a delayed queue: when a schedule is created, a "process this at time T" message goes into a queue, and the queue hands it back when the time arrives. The precision is high and it scales well, but you have one more service to maintain, and when a schedule is cancelled or rescheduled you must remember to void the old message in the queue, or the old message will still trigger a send when its time comes. The trap in the queue approach is not sending — it is cancelling.

The third route is an external scheduling service: hand the triggering over to a cloud provider's timer service, which calls one of your endpoints when the time comes. This is the most hands-off option, but it puts a lifeline in someone else's hands, and every trigger needs proper authentication, because an endpoint anyone can poke is a problem waiting to happen.

Whichever route you take, three things are non-negotiable. First, lock the send. The same schedule record must only ever be processed by one worker at a time, or two processes will both find it "due", both send it, and the user will receive two copies. The lock can be a database row lock or a distributed lock; what matters is that "find it" and "mark it as sending" are one atomic operation with no gap in between.

Second, never keep scheduled jobs in memory. That is exactly the mistake in the fictional example at the top: setTimeout, in-process queues, in-memory to-do lists — a process restart wipes them all out. Schedule records must be persisted, and the scheduler must restore from the database every time it starts. Do not trust any state that lives inside a process.

Third, the scheduler itself must be observable. How many records each poll found, how many went out, how many failed, how long it took — record all of it. Without those numbers, the morning a user says "my content never went out", you will not even know where to start looking. Scheduler logs are not for your own amusement; they are preparation for the postmortem that will come one day.

Timezones are where it goes wrong most easily

Timezone bugs have a particular character: everything works during development, and then they erupt in a cluster three months after launch. That is because they only appear under specific conditions — a user travelling across timezones, a daylight saving switch, a user moving house and changing their timezone setting. You cannot catch them in normal testing, and when they hit, they hit hard.

The first rule is already set: store UTC, display in the user's local timezone. Everyone must follow it, including you when you write your own admin tools. In the admin schedule list, what shows should be "the local time the user originally picked", not the UTC time, or customer support and the user on the phone will be talking about two different times.

The second rule: when a user picks a time, make it unmistakable which timezone that time is in. The best approach is to write it right next to the time picker, for example "9 November 2026, 07:30 (Beijing time)". If the user is travelling and the system shows their device timezone, write the timezone name out too. The worst approach is to show a bare time and leave the user to guess which timezone it belongs to.

The third rule: be extra careful on the two daylight saving switch days each year. Every spring and autumn there is one 23-hour day and one 25-hour day. A "2:30 a.m." a user picks on a switch day may not exist at all, or may occur twice. The fix: when the time does not exist, roll forward to the nearest time that does exist and tell the user clearly; when it occurs twice, default to the first occurrence and tell the user. Never handle it silently — silent handling takes the decision out of the user's hands.

The fourth rule: what happens to existing schedules when a user changes their timezone setting? My recommendation: once a schedule is created, lock in the UTC time it corresponds to. Changing the timezone only changes the display, never the send moment of an already-fixed schedule. If the user wants it recalculated under the new timezone, let them reschedule it themselves. Write this rule in the interface, or users who change timezone and see their schedule "move" will assume it is your bug.

Schedule conflicts and the calendar view: what heavy schedulers need most

Someone who schedules one piece a week can live with a time picker. Someone who schedules ten pieces a week will find a time picker a disaster. They need to see the whole week laid out: two pieces on Monday, Wednesday empty, three pieces colliding on Friday. At that point a calendar view is not a nice extra — it is a core need.

A calendar view has to answer three questions. First, what goes out this week and at what time, visible at a glance. Second, which slots are already full and which are still free. Third, when rescheduling, can the user drag the item to its new slot instead of deleting it and starting over? Drag-to-reschedule looks like a small feature, but for an operator doing it dozens of times a day, saving ten seconds each time frees up half an hour a day.

Whether to block schedule conflicts is a product judgement call. My suggestion is to handle it on two levels. Level one is hard conflicts: two pieces in the same minute, on the same channel, to the same audience. Block those outright and tell the user "there is already a piece at this time", so they pick another time or merge the content. Level two is soft conflicts: five pieces in one day, or three pieces a day for three days running. Do not block those — just warn, for example "you already have 4 pieces scheduled today; readers may find that a lot", and leave the decision with the user. Hard blocks prevent accidents; soft warnings prevent fatigue. The force is different for a reason.

Frequency caps deserve their own rule too. Many products have been burned here: a user schedules fifty pieces at once, five a day, and a week later subscribers unsubscribe in bulk. It is not malice — the user simply does not know the consequences. The product should speak up when scheduling passes a threshold, for example warning when a week holds more than seven pieces that "this is above your historical average frequency". The warning does not restrict the user; it stops them from hurting themselves by accident.

One more detail: can already-scheduled content still be edited? Yes — but it depends on what is being edited. Titles, body text, and images can change any time; the schedule stays put. Changing the publish time is a reschedule: it goes through the reschedule flow, the old schedule is voided, and a new one takes effect. Changing the audience — say from "all subscribers" to "paying subscribers only" — should come with a clear statement that the audience for this send has changed, ideally with a second confirmation. The worst failure is a user editing content without knowing the schedule will still send the old version, or changing a time without knowing the old schedule record is still sitting in the queue.

When it fails: retries, notifications, and fallbacks

The most revealing test of a product is not how punctual it is on a normal day, but how it behaves when it fails. Sends fail for all kinds of reasons: the email provider hiccups, the SMS gateway times out, the target platform rate-limits the API, your own server restarts at the exact moment of sending. Failure will happen. The question is whether you are ready for it.

Set the retry policy before launch, not after the first failure. My suggestion: retry five minutes after the first failure, half an hour after the second, two hours after the third — and after three failures, stop and hand it to a human. Increasing intervals keep you from piling pressure onto a downstream service that is already struggling. Capping the number of retries keeps one failed schedule from endlessly consuming the scheduler's resources.

Retries must distinguish between failure types. A transient failure — a network timeout, a 500 from the provider — is worth retrying. A permanent failure — a recipient address that does not exist, content rejected by the platform, an account whose permissions were revoked — will still fail on the hundredth try, so stop immediately and go straight to the failure flow. Telling the two apart requires the send endpoint to return a clear error code, not a generic "failed".

When something fails, tell the user — in words they can understand. Do not write "Send failed: error code 500". Write "The morning letter scheduled for Monday 7:30 did not go out, because the email provider was unreachable at the time. We have already retried three times. Your content is still in the drafts folder — you can send it manually now, or schedule a new time." Users do not need technical details, but they need three things: what happened, where their content is now, and what they can do next.

Fallbacks need two layers. The first is inside the system: failed schedules land on a to-do list that an operator reviews daily, sending manually or contacting the user. The second is outside the product: if the whole scheduler goes down, an alert must reach the person on duty directly — not wait until users start asking in the group chat where their content is. Set the alert threshold sensibly: one failure does not need to wake anyone at night, but more than ten failures in an hour means someone must act immediately.

Advice for vibe coding: how to build this feature without it blowing up

Scheduled publishing suits vibe coding well: the requirements are clear, the boundaries are sharp, and an AI is unlikely to wander off track. But there are a few places where AI will happily dig a hole for you, and you need to guard those yourself.

The first hole is that AI loves setTimeout for timing. Tell it "send it when the time comes" and it will very likely write a setTimeout or an in-memory task queue. It runs fine in development; deploy it to a server, the process restarts, and everything is gone. Write it into the requirements in stone: schedules are persisted, the scheduler restores from the database, no in-process state.

The second hole is that AI easily gets time comparison wrong. Comparing times as strings, comparing local time directly against UTC, forgetting daylight saving — none of these look wrong in code review, but they will erupt together on some morning after launch. When you test, cover three scenarios specifically: a cross-timezone user scheduling, scheduling around a daylight saving switch, and whether an old schedule still fires on time after the user changes timezone.

The third hole is that AI-written retry logic often has no cap. Fail, retry, fail again, retry again, an infinite loop that eventually takes down the downstream service. The requirements must state the retry count and the increasing intervals, and demand that transient and permanent failures be distinguished before retrying.

My suggestion is to split scheduled publishing into three acceptance milestones. Milestone one is "can schedule, can send, can cancel" — get the main flow working first. Milestone two is "failures can retry, can notify, and the status is visible" — fill in the failure flows. Milestone three is the operations experience: the calendar view, drag-to-reschedule, and frequency warnings. Do not try to do it all at once. Ship milestone one, run it for two weeks, gather real feedback, and then move on — far cheaper than building everything and then reworking it.

Launch checklist

Finally, a checklist you can tick off directly. Confirm each item before launch; fill whatever is missing, and do not launch on a feeling.

One, schedule records live in their own table, the state machine has exactly six states, and transition permissions are written in code. Two, times are stored in UTC, displayed in the user's timezone, and the timezone is named next to the time picker. Three, the send endpoint has an idempotency key, so triggering the same schedule twice still sends once. Four, the scheduler polls every minute or equivalent, the send is locked, and "find it" plus "mark as sending" is one atomic operation. Five, schedules are persisted, and the scheduler restores from the database after a process restart. Six, the retry policy is fixed: three attempts maximum, increasing intervals, transient and permanent failures distinguished. Seven, after a failed send the user can see the status in the product and receives a notification in plain language. Eight, the daylight saving switch, user timezone change, and cross-timezone scheduling scenarios have all been tested. Nine, the calendar view shows the whole week's layout, hard conflicts are blocked, soft conflicts warn. Ten, the scheduler is monitored: the count, success count, and failure count of every poll are recorded, and failures past a threshold trigger an alert. Eleven, the cancel and reschedule flows work, and a voided schedule's old queued message can no longer trigger a send. Twelve, there is a manual re-send entry point, and operators can see the pending list in the backend. Thirteen, the docs are written: where users see schedule status, what to do when something fails, and who to ask for help.

Thirteen items looks like a lot, but every one of them maps to at least one real incident. Most of the time, users never notice scheduled publishing at all — they only remember you when it breaks. Build it solid, and it guards your product's reputation while you sleep.

Views 0Comments 0

Comments (0)

ME
0/1000
Loading comments...
Browse projectsPublish your project

Related articles

Real photograph: two hands exchanging a red gift card with a gold bow and the words 'GIFT CARD'.
Guide
Give Someone a Subscription: Gift Cards and Redemption Codes for Vibe Projects

A gift card is not a coupon with a different name: the giver pays first, the recipient spends later, and the money is a liability until it is spent. This guide covers the three forms, the five-step purchase flow, redemption code design against brute force, the balance ledger, fraud prevention, refunds and support, and a 14-item launch checklist.

Payments & MonetizationIndie DevelopmentProduct Strategy
Real photograph of a woman resting her head on her hand at a small table, looking exhausted in front of a laptop
Guide
Don't Want to Cancel — Can I Just Pause? Subscription Pause and Resume for Vibe Projects

A user asks to pause for two months — and the only button you have is cancel. This guide splits pause, downgrade, and cancel into three choices, explains Stripe's two pause modes and the three invoice behaviors, covers read-only access and data retention during the pause, walks through the three things that break at resume time, and ends with a save funnel and a 12-item launch checklist.

Payments & MonetizationBackend EngineeringIndie Development
Real photograph of a man reading email on a laptop, the inbox on screen showing a stack of unread messages
Guide
Signed Up and Then Silence? A Lifecycle Email Sequence That Actually Keeps Users

Users sign up and then hear nothing for 30 days — and quietly leave. This guide builds the lifecycle email sequence that fills that gap: a welcome and activation run for days 0, 1, 3, and 7, three behavior-triggered habit emails after the aha moment, at 80% of quota, and after 14 days asleep, plus win-back execution, frequency caps, unsubscribe rules, and how to measure it with activation conversion instead of open rates.

Creator EconomyIndie DevelopmentProduct Strategy