Skip to main content
Every request produces two kinds of data: the JSON payloads (the input you sent and the output the model returned, which power your dashboard history) and the media files the model generated, hosted on the ModelRunner CDN. Each has its own default retention and its own control. The 180-day payload default applies to requests created on or after this policy took effect. Older requests are kept until you delete them — nothing was removed retroactively.

Generated media

Set an expiration on the files a request produces with the X-Modelrunner-Object-Lifecycle-Preference header:
Needs modelrunner-ai 0.5.0 or newer, where lifecycle, store_io and a raw headers={...} escape hatch all arrived together — on run, submit, subscribe and stream, sync and async. Prefer 0.5.1 or newer: before that, expires_in="never" sent a 100-year duration, roughly twenty times the maximum in the table above, instead of the null that means no expiration.expires_in takes an integer number of seconds or one of 1h, 1d, 7d, 30d, 1y, never. Use "never" for no expiration — it sends the null in the table above rather than a large number. None is not a valid value. Leaving store_io unset keeps whatever your account default is.On 0.4.x and earlier there is no supported way to send these headersheaders= did not exist, and passing it raises TypeError: SyncClient.subscribe() got an unexpected keyword argument 'headers'. Upgrade, send the request over raw HTTP as in the cURL example, or set an account-wide default in the dashboard.
The countdown starts when the request finishes, not when you submit it — so a 60-second expiration on a model that takes two minutes still gives you a full minute of file life after the output exists.
Expired files are permanently deleted and cannot be recovered. Download anything you need to keep before it expires.
The same header works on file uploads, setting an expiration on the file you upload — send it on POST /storage/upload/initiate. There the countdown starts at upload time, since the bytes are landing immediately. One exception is worth knowing: an uploaded file that you then use as a request input stops expiring. Another request may reference the same upload, so we stop treating it as disposable. The same applies to a file you favorite, tag, or use as a model, wrapper or collection image. Delete those explicitly when you are done with them.
For a multipart upload, the preference rides on POST /storage/upload/complete instead of the initiate — that is the call that creates the file’s row.Both clients handle that for you, so a large upload keeps its expiration: pass lifecycle to upload, upload_file or upload_image on modelrunner-ai 0.6.0 or newer, or to storage.upload() on the JavaScript client 1.3.0 or newer. Both switch to multipart above 90 MB. On earlier versions the preference did not survive a large upload — the JavaScript client accepted the option and dropped it, and Python had no upload option at all — so upgrade, or drive the raw HTTP flow and set the header on the complete call yourself.

Access controls are not supported

CDN URLs are unguessable but public to anyone holding the link. There is no per-user access-control list, and an initial_acl field in the lifecycle header is rejected with a 400 rather than ignored — a silently-dropped ACL would leave you believing files are private when they are readable. Treat the URL itself as the secret, and use a short expiration for sensitive output.

Request payloads

Inputs and outputs are stored for 180 days, which is what makes your dashboard history work. To keep them out of storage entirely, send X-Modelrunner-Store-IO: 0:
This is not “never written”. We hold the output briefly while we move media onto the CDN, build thumbnails and calculate what to charge you — usage-priced models are billed from the output itself. Payloads are removed shortly after the request settles, once your result has been handed over and any webhook body has been rendered. Budget for a window of roughly 15 minutes, not zero.Your media is unaffected: files stay for as long as the expiration above allows.
Because the payload is gone afterwards, a request submitted this way is only readable once in practice — from the response to your own poll, or from a webhook delivery. Capture what you need at that point.

Deleting a request’s data

You can also append /payloads to the response_url the submit returned. This removes the input and output JSON and deletes the media files that request generated:
X-Idempotency-Key is optional; repeating the same delete is safe either way.
Deletion is permanent. The files return 403 immediately afterwards and cannot be restored.
A delete returns 409 in three cases, each protecting something:
  • The request has not finished. Its payload does not exist yet in final form.
  • It has not been charged yet. Usage-priced models compute the bill from the output, so removing it first would produce a wrong charge.
  • It is published as an example on a model or wrapper page. Detach it first.

Reading a request whose payloads are gone

The request record survives a purge — only the payloads are emptied. input and output come back as {}, and a payloadsPurgedAt timestamp tells you why.
Key your handling off payloadsPurgedAt, not off an empty output — {} is also what a failed generation looks like. See the billingStatus warning on the webhooks page for the same distinction.

Setting a default for your account

Rather than sending headers on every call, set a default under Settings → Account in the dashboard: payload retention, a media expiration, and whether to store payloads at all. It applies to every request on the account. A header on an individual request always wins over the account default — including "expiration_duration_seconds": null, which is how you exempt one request from an account-wide media expiration.