Skip to main content
The plugin has two background tasks. Both are off by default. 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 a schedule. 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
Command line. Run the queue and handle the schedules:
Vercel and other serverless hosts. 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
Vercel sends Authorization: Bearer <CRON_SECRET> when the CRON_SECRET environment variable is set.

Plugin endpoint

Set tasks.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
crontab or any other cron service:

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 the bunny:task command to Payload’s CLI when a task is on:
It runs the task once, prints the counts and exits with code 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
A script, for example with payload run:
scripts/sync-stream-status.ts

Status sync

The plugin stores the encoding state of each video in bunnyData.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
Or change the defaults:
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:
  1. For each collection with status sync, it finds two kinds of documents, last saved first, limit at a time, until none are left or the run stops: documents whose stored status is processing and that were saved in the last maxAge seconds, 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.
  2. It asks the Bunny API for each video, four at a time.
  3. When the state differs from the stored one, it stores the new state. A video with no stored status gets processing while 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 MP4 resolutions. When the video becomes ready, Bunny has finished encoding it, mp4Fallback is on and the document has no resolutions yet, it fetches them and stores them in the same write. If that fetch fails or Bunny returns no MP4 resolutions yet, it does not store ready. The document stays or becomes processing, and a later run tries again while the document was saved in the last maxAge seconds. If Bunny answers that fetch with 429, the run stops as described below and the document keeps its stored status for the next run. updated counts only the writes that changed a document.
  4. A video that Bunny no longer has becomes failed, but only when the library itself still answers. Other errors count in failed, 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 in failed.
  5. 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. The updatedAt of the version counts for maxAge. 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 the resolutions of the main document into the version. This copy counts in updated but not in checked. After a copy to ready or failed, the version no longer matches, and a read with draft: true does not show an old processing state. 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 only bunnyData.stream of the version and keep its other fields and its updatedAt. checked, updated and failed count documents and versions together.
A status write does not change 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.
Bunny does not publish rate limits for the Stream API, so the plugin cannot stay under them for sure. Each run makes one API call per processing document or document with no status, plus one for the MP4 resolutions of a video that became ready while mp4Fallback is on, four at a time, for up to 25 seconds. When Bunny answers 429, the run stops and returns hasMore: true. If you see 429 often, lower maxAge, pass a shorter maxDurationMs or run the task less often. maxAge does not limit videos with no status: on a large library from before beta.2, the first runs fill their status over several runs, so pass a shorter maxDurationMs or run the task less often until they are done.

Turn the status off

With stream.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.

Troubleshooting