By default, both schedules use the
storage-bunny queue. You can change the schedule of each task in the global stream options only.
Pick a trigger
A task does nothing until something runs it. Pick one of these:
You can use more than one trigger. Status sync is safe to run at the same time from two triggers: a status write changes a document only while its stored status is still
processing. Inside one server process, the jobs runner, the endpoint and the CLI do not start a task while the same task is still running. They skip the run and log a [bunny:tasks] warning. The endpoint and the CLI then return { "hasMore": false, "skipped": true }. The exported functions do not skip.
When a task is on and no jobs.autoRun entry runs its queue, the plugin logs a warning at startup. Set tasks.secret to turn the warning off.
Payload jobs runner
The plugin registers each task as a Payload job task with aschedule. Payload queues a job when the schedule is due and runs it when something processes the storage-bunny queue.
Long-running server. Add an autoRun entry for the queue:
payload.config.ts
autoRun does not work there, because no process stays alive. Call Payload’s own endpoint /api/payload-jobs/run?queue=storage-bunny from a cron. It queues the due jobs and runs them. Let the cron call it with a secret:
payload.config.ts
vercel.json
Authorization: Bearer <CRON_SECRET> when the CRON_SECRET environment variable is set.
Plugin endpoint
Settasks.secret to add GET and POST /api/storage-bunny/tasks/:task. It runs one task right away, without Payload jobs, and returns its counts. Without tasks.secret, the endpoint does not exist.
payload.config.ts
cleanup returns deletedCount, errorCount and hasMore.
Vercel Cron. Use the same value for CRON_SECRET and tasks.secret, because Vercel sends CRON_SECRET as the bearer token:
payload.config.ts
vercel.json
When hasMore is true
hasMore: true means there is more work left, for example because the run reached its time budget or more items were waiting than one run takes.
status-sync loads the documents in pages of limit and keeps going until it has checked them all. It stops early and returns "hasMore": true when:
- the run reached its time budget of 25 seconds;
- Bunny answered
429 Too Many Requests.
cleanup loads stale upload sessions 100 at a time per library. When sessions of a page fail, it skips them and loads the next page in the same run. Otherwise it stops after one page. It also loads at most 100 sessions of libraries that are no longer configured. It returns "hasMore": true when the run reached its time budget of 25 seconds, or when more sessions were waiting than it loaded, but only if the run removed at least one session in any library. A run that removed no session returns false, for example when every session failed or Bunny did not answer in time. So a loop that calls it until hasMore is false always ends.
Neither task starts a document or an upload session with less than 2 seconds left. A Bunny call that the run deadline cuts off does not count in failed or errorCount.
The next scheduled run continues. To finish now, call the endpoint again until hasMore is false. Wait between calls, so a 429 has time to clear. This loop also stops on an error response and after 20 calls:
CLI
The plugin adds thebunny:task command to Payload’s CLI when a task is on:
1 when any video or session failed. See bunny:task.
Call the functions from your own code
The package exports the functions behind the tasks:req defaults to a new local request. limit defaults to stream.status.sync.limit, and maxDurationMs defaults to 25000. Both functions throw when the plugin is not in the config or the task is off.
A Next.js route with your own authentication:
app/api/cron/bunny/route.ts
payload run:
scripts/sync-stream-status.ts
Status sync
The plugin stores the encoding state of each video inbunnyData.stream.status. The webhook and the media preview update it. Status sync is a third way. Turn it on when the library has no webhook, or when a missed webhook must not leave a video as processing in the list and the filters.
payload.config.ts
payload.config.ts
A collection can set
status: { sync: true }, { sync: false } or { sync: { limit, maxAge } }. In a partial stream override, it inherits the other values from the global stream.status.sync. With full stream options for its own library, it inherits nothing, and status sync is off unless you set it. status: true keeps the global sync, and status: false turns the status and its sync off for that collection.
How a run works:
- For each collection with status sync, it finds two kinds of documents, last saved first,
limitat a time, until none are left or the run stops: documents whose stored status isprocessingand that were saved in the lastmaxAgeseconds, and documents with a Stream video but no stored status, at any age. Videos uploaded before the status existed have no status, so the sync fills it for them. - It asks the Bunny API for each video, four at a time.
- When the state differs from the stored one, it stores the new state. A video with no stored status gets
processingwhile Bunny still encodes it. The write happens only if the stored status has not changed since the query and the document still points at the same video, so it never overwrites a newer state from the webhook or the state of a replaced video. It keeps the stored MP4resolutions. When the video becomesready, Bunny has finished encoding it,mp4Fallbackis on and the document has noresolutionsyet, it fetches them and stores them in the same write. If that fetch fails or Bunny returns no MP4 resolutions yet, it does not storeready. The document stays or becomesprocessing, and a later run tries again while the document was saved in the lastmaxAgeseconds. If Bunny answers that fetch with429, the run stops as described below and the document keeps its stored status for the next run.updatedcounts only the writes that changed a document. - A video that Bunny no longer has becomes
failed, but only when the library itself still answers. Other errors count infailed, and the document keeps its stored status for the next run. So a video with no status is checked again on every run until Bunny answers for it. A call that the run deadline cuts off does not count infailed. - In a collection with
versions.drafts, it then does the same for the latest version of each document, with the same filters, page size and limits. TheupdatedAtof the version counts formaxAge. It skips a version whose document is in the trash. A version with the same video as its main document does not need a Bunny call, because step 1 already checks that video. When the main document has another state, the sync copies the state and theresolutionsof the main document into the version. This copy counts inupdatedbut not inchecked. After a copy toreadyorfailed, the version no longer matches, and a read withdraft: truedoes not show an oldprocessingstate. A version with another video than its main document is checked like a document. So a video that only a draft has also gets its state. The writes change onlybunnyData.streamof the version and keep its other fields and itsupdatedAt.checked,updatedandfailedcount documents and versions together.
updatedAt. So a video with no status that gets processing and was last saved more than maxAge seconds ago is not checked again by status sync. The webhook or the media preview updates it.
Status sync skips a collection with a [bunny:tasks] warning when Payload does not have it or when it has timestamps: false, because the sync needs updatedAt.
Like webhook writes, these writes go straight to the database. They do not run your collection hooks, do not create versions and do not change updatedAt. So maxAge counts from the last time someone saved the document, for example when they replaced the video, not from a status write.
Turn the status off
Withstream.status: false, the plugin does not store the status, so there is nothing to sync. It also removes the bunnyStream admin field and the status endpoint. The preview shows the player right away. Nothing shows that a video is still encoding.