TL;DR, Quick Answer
7 min readMastodon documents scheduled_at as needing to be at least 5 minutes in the future, and the source backs it with MINIMUM_OFFSET = 5.minutes in ScheduledStatus. Anything nearer returns HTTP 422 with a validation error. A timestamp in the past behaves differently: PostStatusService drops it and publishes the status immediately. Two further limits, 300 scheduled statuses total and 25 per day, appear only in the source.
What is the minimum scheduled_at offset on the Mastodon API?
Every Mastodon API scheduled_at value has to land more than 5 minutes ahead of the server's clock, or POST /api/v1/statuses rejects the request with HTTP 422. The form-data documentation for that endpoint states it in one line: "Datetime at which to schedule a status. Providing this parameter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future."
The update endpoint repeats the rule in slightly different words. PUT /api/v1/scheduled_statuses/:id documents its own scheduled_at as "Datetime at which the status will be published. Must be at least 5 minutes into the future", so moving an existing scheduled post nearer than five minutes fails the same way creating one does.
The number is not a documentation convention. It is a constant in the model, MINIMUM_OFFSET = 5.minutes.freeze in app/models/scheduled_status.rb, and the validation that uses it reads:
def validate_future_date
errors.add(:scheduled_at, I18n.t('scheduled_statuses.too_soon')) if scheduled_at.present? && scheduled_at <= Time.now.utc + MINIMUM_OFFSET
endThe comparison is <=, so a timestamp exactly 5 minutes out fails. "At least 5 minutes" in the docs means strictly more than 5 minutes in the code.
What does the API return when the time is too near?
A 422 with a validation message in the error field. Mastodon's API error handling turns the Rails exception straight into JSON:
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
render json: { error: e.to_s }, status: 422
endThat concern lives in app/controllers/concerns/api/error_handling.rb and is included by Api::BaseController, so the body is the exception's own string rather than a machine-readable code. There is no error code, no field list and no retry-after hint; parsing it means matching on English text.
Why does the documented error string differ from the one servers send?
Because the locale string changed and the docs example did not. The scheduled_statuses page shows this 422 body for the update endpoint:
{
"error": "Validation failed: Scheduled at The scheduled date must be in the future"
}The current config/locales/en.yml on the mastodon/mastodon main branch carries a shorter phrase:
scheduled_statuses:
over_daily_limit: You have exceeded the limit of %{limit} scheduled posts for today
over_total_limit: You have exceeded the limit of %{limit} scheduled posts
too_soon: date must be in the futureRails prefixes the humanized attribute name, so a current server produces "Validation failed: Scheduled at date must be in the future". The documented example still carries the older "The scheduled date must be in the future" wording.
| Source | String |
|---|---|
| docs.joinmastodon.org, scheduled_statuses 422 example | "Validation failed: Scheduled at The scheduled date must be in the future" |
config/locales/en.yml, main branch | "Validation failed: Scheduled at date must be in the future" |
Any client matching the documented sentence exactly will miss the error on a current server. Match on the 422 status and the presence of scheduled_at in your own request instead of on the sentence.

What happens if scheduled_at is in the past?
The post goes out immediately, and no error is raised. This is the behaviour the documentation never mentions, and it lives in PostStatusService:
@scheduled_at = @options[:scheduled_at]&.to_datetime
@scheduled_at = nil if scheduled_in_the_past?with
def scheduled_in_the_past?
@scheduled_at.present? && @scheduled_at <= Time.now.utc
endSetting @scheduled_at to nil makes scheduled? false, which routes the request through process_status! rather than schedule_status!. The response is a Status entity, not a ScheduledStatus, so a client that assumes scheduled_at always yields a ScheduledStatus will read a missing id for a post that is already public.
Three outcomes, one parameter:
AdaptlyPost
Start Your 7-Day Free Trial
All-platform analytics
Social Inbox
AI-powered assistant
scheduled_at value | Result | Response |
|---|---|---|
| In the past, or exactly now | Published immediately | Status |
| Within 5 minutes of now | Rejected | 422, validation error |
| More than 5 minutes ahead | Queued | ScheduledStatus |
The middle band is the trap. A retry loop that pushes a failed send "a couple of minutes later" walks into the 422; one that falls back to "post it now" by sending a past timestamp gets a live post instead of the error it expected.
- Lands inside the 5-minute band
- Server responds with 422
- Sends a past timestamp
- PostStatusService drops scheduled_at
- Status publishes immediately
How many scheduled statuses can one account hold?
Two more limits sit next to the offset in the same model, and neither appears in the API documentation:
TOTAL_LIMIT = 300
DAILY_LIMIT = 25
MINIMUM_OFFSET = 5.minutes.freezeTOTAL_LIMIT caps the number of scheduled statuses an account can have queued at once, and the message is "You have exceeded the limit of 300 scheduled posts". DAILY_LIMIT caps how many can share a single calendar date, validated with scheduled_at::date = ?::date against the database, and the message is "You have exceeded the limit of 25 scheduled posts for today".
Both arrive as the same 422 shape, with the message on base rather than on scheduled_at. A queue that loads a month of posts in one run will hit the 25-per-day wall long before the 300 total, and nothing on docs.joinmastodon.org warns about either.
What format does scheduled_at take?
An RFC 3339 datetime, which Mastodon documents separately as its datetime format: [YYYY]-[MM]-[DD]T[hh]:[mm]:[ss].[s][TZD], where the timezone designator is either Z for UTC or an offset such as +02:00. The docs are explicit that the separator is an uppercase T and that Z is always uppercase.
The validation compares against Time.now.utc on the server, not on the client. Client clock drift therefore eats directly into the five-minute margin, which is a good reason to schedule at six or seven minutes out rather than at five minutes and one second. The same failure mode shows up whenever a queue trusts local time, as it does across Mastodon and Bluesky and every other API that validates against its own clock.

How do you change or cancel a scheduled status?
Three endpoints manage the queue after creation. GET /api/v1/scheduled_statuses lists them, defaulting to 20 results with a maximum of 40. PUT /api/v1/scheduled_statuses/:id takes a new scheduled_at, subject to the same 5-minute rule. DELETE /api/v1/scheduled_statuses/:id cancels one and returns an empty object.
Changing the text is a different operation. Mastodon notes that PUT /api/v1/statuses/:id edits a published status, and that "To edit the scheduled_at attribute of a ScheduledStatus to change the publication date, use the scheduled status endpoint." The content of a queued post lives in the params object of the ScheduledStatus entity and the update endpoint accepts only the date, so changing wording means deleting and recreating.
A queued status that no longer exists returns 404 with {"error": "Record not found"}, which is also what a scheduled status belonging to another account returns.
What does the 5-minute floor mean for a posting queue?
It sets the shortest useful gap between "decide to post" and "post". Anything under five minutes has to be sent as an immediate status rather than a scheduled one, so a scheduler needs a branch, not a single code path: below the threshold, drop scheduled_at and publish; above it, queue and store the returned ScheduledStatus id.
Two other numbers belong in the same planning pass. Mastodon's default post length is 500 characters, covered in the Mastodon character limit, and per-server rate limiting sits on top of all of it, in the same way Bluesky publishes its own API rate limits separately from its post rules. Building the calendar around the platform floor rather than around a tool's own preferences is the general habit behind scheduling social media posts reliably.
One caveat covers every number above. The constants come from the mastodon/mastodon main branch, and a server running a fork or an older release can carry different values, so read the 422 body rather than assuming 5, 25 and 300 hold everywhere.
Frequently asked questions
Does scheduled_at accept a time exactly 5 minutes from now?
No. The validation compares with <=, so scheduled_at <= Time.now.utc + 5.minutes fails. The documented phrase "at least 5 minutes in the future" means strictly more than five minutes once you read the source.
What HTTP status does Mastodon return for a scheduled_at that is too soon?
- Mastodon rescues
ActiveRecord::RecordInvalidand renders{ error: e.to_s }with status 422, so the body is a plain English validation sentence rather than a structured error object.
Which Mastodon version added scheduled_at?
2.7.0. The version history on the POST /api/v1/statuses documentation lists "2.7.0 - scheduled_at added", and the three scheduled_statuses endpoints were added in the same release.
AdaptlyPost
Start Your 7-Day Free Trial
All-platform analytics
Social Inbox
AI-powered assistant
Does a scheduled post return a Status or a ScheduledStatus?
A ScheduledStatus, whenever scheduled_at is accepted. Mastodon states that the endpoint "Returns: Status. When scheduled_at is present, ScheduledStatus is returned instead." A past timestamp is the exception, because the server drops the parameter and publishes immediately, returning a Status.
Can you schedule more than 25 Mastodon posts for the same day?
Not on a default server. DAILY_LIMIT = 25 in app/models/scheduled_status.rb counts existing scheduled statuses sharing the same date and rejects the twenty-sixth with "You have exceeded the limit of 25 scheduled posts for today". A separate TOTAL_LIMIT = 300 caps the whole queue. Neither number appears in the API documentation.
Where is the 5-minute minimum documented?
On the statuses API methods page at docs.joinmastodon.org/methods/statuses/, in the scheduled_at form-data parameter, and again on docs.joinmastodon.org/methods/scheduled_statuses/ for the update endpoint. The constant behind it is MINIMUM_OFFSET = 5.minutes.freeze in app/models/scheduled_status.rb in the mastodon/mastodon repository.
How many results does GET /api/v1/scheduled_statuses return by default?
Twenty. The endpoint defaults to 20 scheduled statuses per page and caps at 40, so an account sitting near the 300-total limit needs several requests to page through its whole queue.
What happens if you try to fetch a canceled scheduled post?
The server returns 404 with {"error": "Record not found"}. That's the same response a scheduled status belonging to another account returns, so a 404 here doesn't tell you whether the post never existed, was already canceled, or simply isn't yours.
Can you edit the text of a scheduled Mastodon post?
Editing the text means deleting the scheduled post and creating a new one, not modifying it in place. The update endpoint only accepts a new scheduled_at, and the content of a queued post lives in the params object of the ScheduledStatus entity.
How far in advance should you actually schedule a Mastodon post?
More than five minutes, but six or seven minutes is safer than five minutes and one second. The validation runs against the server's clock rather than the client's, so any drift in local time eats directly into that margin.
Put this into practice with AdaptlyPost
Was This Article Helpful?
Let us know what you think!
See us more often in Google
One click marks AdaptlyPost as a preferred source, so our articles sit higher in your Top Stories, AI Mode, and AI Overviews.
Before you go...
AdaptlyPost
Schedule your content across all platforms
Manage all your social media accounts in one place with AdaptlyPost.
All-platform analytics
Social Inbox
AI-powered assistant
Related Glossary Terms


What Instagram's content_publishing_limit Endpoint Returns
Instagram's content_publishing_limit endpoint returns quota_usage plus a config block holding quota_total 50 and quota_duration 86400 seconds.


The Facebook Long Lived Access Token and Its 60-Day Clock
A Facebook long lived access token lasts about 60 days, and the Page token you derive from it has no expiry date at all. The exchange call and the caveats.


Reading the Instagram Container status_code Before You Publish
Every Instagram container status_code value Meta documents, the polling cadence it recommends, the 24-hour expiry window, and what to do on ERROR.
Related Articles


Why a LinkedIn Access Token Expires After 60 Days
Every LinkedIn access token runs 60 days and expires_in returns 5184000. Refresh token rules, what kills a token early, and how Meta's 60 days differ.


How the Threads API 250 Posts Per Day Limit Works
Meta's Threads API 250 posts per day limit is a 24-hour moving window on publishes. Carousels count once, and one endpoint reports what a profile has left.


How TikTok API chunk_size And total_chunk_count Must Add Up
TikTok API chunk_size has a 5 MB floor, a 64 MB ceiling, a 128 MB final chunk, and a total_chunk_count that must round down, not up.

