What wirl does not do yet
What wirl cannot do today, so you find out here and not at your first deploy.
- JavaScript or TypeScript only. Server-side apps run as Workers: no Python, no other language, no processes, and no disk (Node's built-ins work, but what
node:fswrites is gone by the next request). - No packages with native code (
sharp,better-sqlite3,bcrypt): the server module is bundled from pure JavaScript, 10 MB at most. Ordinary npm packages work. - A deploy's bundle has size limits. wirl takes a bundle of at most 100 MB, which may unpack to at most 300 MB (counted as the archive's bytes, so a little more than the files' own sizes), with at most 5,000 files in at most 10,000 folders and 50 MB a file; the bundled server module is at most 10 MB. A hard link counts as a whole copy of the file it names. Past any of them the deploy is refused before anything is stored, and the message says which: leave out what the app does not serve, or list it in
.wirlignore. A bundlewirl deploydid not build is also refused for a path that appears twice, a hard link to anything but a file before it, more than 64 KB in one tar extended header or 4 MB in all of them, a file given two different sizes in its headers, or one size for every file in a global header. - 2 uploads at once from one person, and room for 4 large deploys. app.wirl.dev takes at most 2 deploys and previews at once from one person, whatever credential each comes with. Once its body is in, a deploy needs room for the most one can hold (450 MiB: the bundle, its files, and the largest file once more), and there is room for 4; it keeps that room until its files are counted, then what they come to until they are stored, then only its bundle and its migrations, so slow deploys and slow migrations leave room for others. One person's deploys hold at most 550 MiB of that room at once (one largest deploy, and one largest bundle beside it): a deploy that would pass it waits behind their own, and holds up nobody else. A deploy without room waits its turn, in order, for up to 60 seconds. Past any of these an upload is refused with
503,"error": "too_many_uploads"(one person's) or"uploads_busy", and aRetry-Afterof 30 seconds, before anything is ratified or deployed. - At most about 401 MiB of uploads arriving at once, about 100 MiB of them one person's. Counted as the bytes arrive, so a slow sender holds only what it has sent; one person's uploads arriving together hold at most one largest upload (the 100 MiB bundle beside its wirl.json), a quarter of the whole. Past either, the upload is refused part-way with
503,"error": "too_many_uploads"(one person's share) or"uploads_busy"(the whole), and aRetry-Afterof 30 seconds, nothing ratified. - At most 2 pulls at once to one person, and 4 in all.
wirl pullandwirl_pullask app.wirl.dev for an app's source, and it is read from storage and sent file by file, so one pull holds one file at a time (50 MB at most, and about three times that while storage reads it) and a small compressor, never the whole archive, and writes nothing to disk. Past 2 under way for one person, or 4 on the control plane, a pull is refused before any file is read, with503,"error": "too_many_pulls"(one person's) or"pulls_busy", and aRetry-Afterof 30 seconds, and the refusal is not recorded as a pull. A pull is cut 10 minutes after it began, however it is being read, and a client that takes none of what is waiting to be sent for about 2 minutes is dropped. A pull cut either way ends short of a whole archive, sowirl pullandwirl_pullsay the pull was cut, write nothing to the folder, and fail instead of finishing; run it again. Who may pull is decided when the pull begins. - An upload that sends nothing for 30 seconds is stopped with
408and"error": "upload_stalled", before anything is ratified or deployed. Any request to app.wirl.dev must arrive whole within 5 minutes, so a 100 MB bundle needs about 3 Mbit/s. - Other requests to app.wirl.dev's API and dashboard carry at most 1 MiB. A route that reads a body past it, JSON, a form or anything else, refuses the request with
413and"error": "too_large"(in plain text outside/api/, and as a JSON-RPC error on/mcp); only a deploy's and a preview's upload are larger. - 30 seconds of CPU per request, schedule run or flow step, which wirl does not raise for an app's Worker.
- 10,000 outgoing calls per request, schedule run or flow step, with six open at once (more queue). That is Cloudflare's default for a Worker's subrequests, which wirl does not change for an app's Worker. wirl adds its own to each outgoing call: the vendor has 60 seconds to start answering, and an answer is at most 25 MB (one that says up front it is larger is refused with a
502before any of it is read). Every answer except an event stream (next point) is read whole before your app gets it, so for those the 60 seconds cover the whole answer, and one that grows past 25 MB is refused with a502too. - Database queries per request are bounded in practice by time. Cloudflare's D1 limits page lists 1,000 queries per Worker invocation on paid plans. Measured on wirl on 2026-10-07, one request ran 2,000
env.DBqueries in a row and every one succeeded, at about 35 ms each (900 in 31.8 seconds, 1,001 in 35.1 seconds, 2,000 in 71.4 seconds); a run of 5,000 was still going when the caller stopped waiting. Each query is a round trip, and a batch is one call, so write many rows withenv.DB.batch([...])rather than a loop of single queries. - A vendor's event stream reaches your app as it arrives. A
2xxanswer sent astext/event-streamorapplication/x-ndjson(server-sent events from Anthropic, OpenAI, Mistral, Groq and Gemini'sstreamGenerateContentwithalt=sse, and Ollama's newline-delimited JSON) is handed to your app piece by piece, so a model's reply can be passed on to the visitor as it is written. An answer with any other status is read whole, whatever its type. A streamed answer has 60 seconds to start, then 13 minutes for the rest, and is cut once it passes 25 MB. It is also cut when nothing arrives on it for about two minutes. That bound is Cloudflare's, not wirl's: the call from your app out to wirl is closed after about 125 seconds with nothing on it (measured; Cloudflare does not document it). wirl's own, when nothing arrives on it for 4 minutes, is the one that applies underwirl dev. Either also happens when your app stops reading the stream for that long, because the vendor then waits for your app. An app may have 20 streamed answers open at once: the next call that would stream is refused with429and"error": "busy", and since only the vendor's answer says it is a stream, that call has already reached the vendor by then. How a cut looks to your app depends on where it happens, and the status arrived long before, so it cannot say so. When wirl cuts a stream (past 25 MB, or at 13 minutes), the end your app reads can look as if the vendor had finished, as it did on a deployed app before wirl wrote the event below. When the call goes quiet for about two minutes, your app's read fails with a network error. Before it ends an event stream (text/event-stream) that it has cut itself, wirl writes one last event. It starts with two line ends, the first ending a half-written line and the second dispatching the half-written event as far as it got, thenevent: erroranddata: {"type":"error","error":{"type":"api_error","message":"wirl cut this stream: it reached wirl's limit of 25 MiB for one answer."}}. The message names the bound that fired: the 25 MiB (the 25 MB above), 13 minutes after the vendor began answering, 14 minutes after wirl began passing it on (when nothing is reading the stream), or, underwirl dev, 4 minutes with nothing on the stream. It has the form Anthropic's API sends for an error in a stream. When the cut falls between events, Anthropic's and OpenAI's SDKs read it as an error from the API and throw it (checked against their public source in October 2026); a size cut usually falls inside an event, and then they throw a JSONSyntaxErroron the half-written event first. Either way the read fails. In a browser anEventSourcereceives it inaddEventListener('error', ...)as a message, apart from the plainerrorevent it gets when the connection fails: a handler written for connection failures runs for it too, so tell them apart byevent.data, and callclose()on this one, or the browser reconnects and your app asks the vendor again. wirl counts the event inside the 25 MiB, so an event stream is cut once it passes 25 MiB less 512 bytes. It waits up to 5 seconds for the event to leave the server before it ends the stream, which it then does as it did before, so a client that reads more slowly than that can still lose the event. The event is not written for newline-delimited JSON, for a compressed stream (one that declares acontent-encodingother thanidentity), or when the vendor finishes or breaks off, your app has gone, Cloudflare cuts the call (above), or an update ends the stream. So do not take the end of the stream for the end of the answer, and do not rely on the event alone: look for the vendor's own last event (Anthropic'smessage_stop, OpenAI'sdata: [DONE]), and treat a stream that ends without it as cut. Underwirl dev, every cut ends the read with an error. An update to wirl or to Cloudflare's runtime can also end a long stream early, so a reply worth keeping should be one your app can ask for again. Server-sent events can arrive split across reads, and several can arrive in one read: keep what you have read, split it on line ends, and treat a blank line as the end of an event, rather than taking each read as one event. - What your app sends on an outgoing call is at most 100 MB. wirl's control plane refuses a larger body with a
413and"error": "body_too_large", and nothing of it reaches the vendor. How that answer reaches a deployed app, through the call from your app out to wirl, has not been measured yet: the app may see a502or a closed connection instead. The body is held whole on its way out, by a Worker with 128 MB of memory, so a smaller one may still fail; wirl has not measured where. - The bodies of an app's outgoing calls held at once are at most 128 MiB, those of every app one owner has together at most 256 MiB, and all apps' together at most 512 MiB. They are counted as the bytes arrive, from a call's first byte until wirl has answered it, or, when the answer is streamed, until the stream has ended, so a slow sender counts only what it has sent, and a call that will be refused for its grant counts too. An app's owner is the person who owns it, while they are still a member of the app's organisation, read from wirl's database before the call's body is read and remembered for 30 seconds, so an app that is transferred, or an owner who leaves the organisation, reaches calls that begin more than 30 seconds later; a call already under way is given back under the owner it began with. The apps of one owner count together in however many organisations they are, so one person's apps can hold at most half of the whole. A call whose app has no such owner (it is gone altogether, or its owner has left its organisation) is counted in its app's share and the whole's only, and
wirl dev, which runs one app, counts no owner's share. When that read cannot get a database connection, because none comes free in time or the database refuses, the call is answered as any request to app.wirl.dev is that cannot, a plain-text503with aRetry-Afterof 5 seconds that says nothing was changed, before any of its body is read and with nothing sent to the vendor. Past any of the three, the call is stopped part-way with a503,"error": "app_bodies_full"(the app's own, its previews' calls with them),"owner_bodies_full"(its owner's apps') or"bodies_full"(all apps'), and aRetry-Afterof 10 seconds, and nothing of it reaches the vendor. A body that sends nothing for 30 seconds, or has not all arrived after 120 seconds, is stopped part-way too, with a408, noRetry-After, and"error": "body_stalled"or"body_too_slow", and nothing of it reaches the vendor. How these answers reach a deployed app has not been measured yet: as with the413above, the app may see a502or a closed connection instead. Like the limits under One control plane (below), they are counted in that one instance's memory. - The answers an app's outgoing calls hold at once are at most 64 MiB, those of every app one owner has together at most 128 MiB, and all apps' together at most 256 MiB. Every answer but an event stream is read whole before your app gets it (above), so wirl counts its bytes as they arrive from the vendor, until wirl's server has taken the last of them to send to your app, or your app has gone. A call, but a
HEAD, whose answer carries nothing, is sent only while its app's answers, its owner's apps' answers, and all apps' answers leave room for one more answer at the cap, 25 MiB: with less, it is refused with a503,"error": "app_no_answer_room"(the app's own, its previews' calls with them),"owner_no_answer_room"(its owner's apps') or"no_answer_room"(all apps'), and aRetry-Afterof 10 seconds, and nothing of it reaches the vendor, so it is safe to send again. Answers arriving at the same time can still take that room first: an answer that would take its app's answers, its owner's apps', or all apps', past the most is then stopped part-way with a502,"error": "app_answers_full","owner_answers_full"or"answers_full", and noRetry-After, and none of it reaches your app. That refusal comes after the vendor has received the call and may have acted on it, which it may bill for: send it again only if repeating it is safe, and see Connections for SDKs that send a call again by themselves. An answer that wirl's server has not taken to its last byte 120 seconds after handing it back, because your app reads it slowly or not at all, is cut there, deployed and underwirl devalike: your app's read fails, rather than ending as if the answer were whole. An event stream holds a little at a time, and is not counted. An app's owner is counted as for the bodies, above. Like the limits under One control plane (below), they are counted in that one instance's memory. - Responses stream; requests do not, and there are no WebSockets. A response is handed on as your app sends it, so server-sent events and large downloads your app produces itself work, deployed and under
wirl devalike. An answer your app relays from another service streams the same way when the vendor sends it as an event stream (see above); any other answer from an outgoing call is read whole first, so passing it on sends it all at once. Two kinds of response are still read whole: an HTML page that carries the wirl badge, because the badge goes before the page's closing tag (at most 25 MB). Turn the badge off in the app's Settings, or send aContent-Security-Policyheader of your own, and that page streams too; a preview never carries the badge. The other kind is a preview's own HTML page, which carries its preview marker by the same rule — a preview page you gave your ownContent-Security-Policy, or that is not a 200, streams too. AContent-Lengthheader your app sets is not passed on: Cloudflare announces the length of a body whose length it knows (a string, a buffer) and sends anything else in chunks, so a number your app got wrong cannot leave a visitor waiting. Everything a private or org app answers goes out withCache-Control: private, no-store; on a public app, sendCache-Control: no-cacheon a stream yourself, so that nothing between you and the visitor holds it back to cache it. If your stream throws part-way, the visitor's response simply ends early: the status has already gone, so there is no error page. A2xxevent stream or NDJSON answer (text/event-stream,application/x-ndjson) with noContent-Lengthof yours still counts towards rolling a broken version back automatically, once, as a5xxanswer does, unless it breaks 100 seconds or more after the last piece of it wirl received, its headers included: that is how Cloudflare cutting a quiet stream looks, so such a break is not counted. A break while your stream is still sending is counted, a Cloudflare restart included; if your app relays a vendor's stream and keeps sending heartbeats of its own, end your stream cleanly when the vendor's call is cut, or that break counts too. A visitor who leaves part-way does not count either. Any other body that breaks part-way after its status has gone does not count: one with aContent-Lengthyou set, and a stream of any other type, such as a chat answer the AI SDK streams astext/plainor a CSV export, are handed on exactly as your app answers them, so that a body whose length Cloudflare knows keeps it. The exception, with your stack, is in the logs. A request's body is read whole before your app is called. A WebSocket upgrade is not passed through. - A signed-in visit lasts 12 minutes, and renews itself while the page is in use. Once a visit is 8 minutes old, the next request the page makes to its own app asks app.wirl.dev whether the person may still open it, and if they may, the visit starts again. Neither the page nor your app has to do anything. So a live feed or a dashboard that refreshes keeps working for as long as it makes a request at least every 16 minutes; an
EventSourcereconnecting after a stream ended counts. AnEventSourcewhose stream your app keeps open needs nothing either: once the visit it opened on is 8 minutes old, wirl ends that stream cleanly between two events, and the browser reconnects by itself a few seconds later, which renews the visit (give your events anid:, and the reconnect'sLast-Event-IDheader says where to carry on). wirl knows anEventSourceby what the browser sends for one, aGETwhoseAcceptheader is exactlytext/event-stream, so afetchthat sends exactly that is treated the same way; aPOST, or anAcceptthat lists other types too, is never ended. Only the page's own requests renew a visit (itsfetchandEventSourcecalls to the same app, a page load, or a client outside a browser), never a request from another app's page. Removing someone's access still takes effect within 13 minutes: their next renewal is refused and sends them to sign in, which no longer lets them in. Signing out of app.wirl.dev, or signing in to it again in the same browser, stops renewals the same way, and no visit renews past 14 days after the sign-in that began it. The exception to the 13 minutes is a preview's visit link the person printed before their access was removed, since opening one asks app.wirl.dev nothing: whoever opens it is let in as them until 16 minutes after it was printed (its fifteen, and the minute of clock skew every check allows), and keeps the pass it gives for up to 13 minutes (its 12, and that minute again), so that preview can be opened as them for up to 29 minutes after the link was printed. A visit that does run out (the page made no request for longer than that, it came from a preview's visit link, whose visit lasts 12 minutes and never renews, or app.wirl.dev could not be reached to renew it) sends the next request to sign in, on app.wirl.dev. A page load follows that and comes straight back signed in. A request a page makes by itself does not: afetchfails, and anEventSourcecannot get through. So a page meant to stay open should still reload itself when such a request fails, rather than retrying it for ever. Any other stream that is already open, such as one afetchreads, is not cut when the visit runs out. - One control plane. The rate limits it keeps (the verify step for signed public paths, the reports of what a
nonepath's ceiling refused (ten an app a minute read, one written), sharing and inviting, and a flow's starts a second and the sends a second to one wait, under Flows below), an org's calls to D1 for its migrations, each person's listings of them and the count of applies of migrations under way (under Migrations below), the count of an app's open streamed answers, the uploads under way (2 a person, about 401 MiB arriving and about 100 MiB of it a person's, room for 4 large deploys), the pulls under way (2 a person, 4 in all), and the auto-rollback health count live in memory on a single instance. Running more than one would split them. The ceiling onnonepublic paths is not one of these: it is counted at the edge. - Schedules have one-minute granularity; the clock ticks every 30 seconds, so a run can start up to 30 seconds after its minute. See Schedules.
- No email triggers. An app cannot be invoked by an incoming email.
- No blob storage. Beyond
env.DB, an app has nowhere to put files, so a file goes inenv.DB. Cloudflare publishes D1's limit as 2,000,000 bytes in one string, BLOB or row: design to that, and store a larger file in chunks, one row each. What deployed D1 did when wirl measured it on 2026-10-07, which is not a promise: it stored a single value of 8,000,000 bytes, refused one of 9,000,000 withD1_ERROR: string or blob too big: SQLITE_TOOBIG, and failed one of 20,000,000 sooner, withSerialized RPC arguments or return values are limited to 32MiB, a limit on what one call toenv.DBsends or gets back. A batch is one call, so write a file of up to a few MB in oneenv.DB.batch([...]), and it is whole or absent: 8,000,000 bytes is the most one call has been measured to carry. A larger file goes in several batches of a few chunks each, then a last write that marks it complete (a row in afilestable, say), and whatever reads files ignores one not yet marked. Read a file back a chunk or a few at a time.wirl devrefuses a single bound value over 2,000,000 bytes, so a single value over the published size fails there first. It does not add up a row, a value grown in SQL (data = data || ?), or what one call sends or gets back, so a row of several large values, a value appended to, or a batch or a read that carries many chunks at once can still pass there and fail deployed. A BLOB bound as aUint8Arraycame back from.first()and.all()as a plain array of numbers, not aUint8Arrayor anArrayBuffer, and the 20,000,000-byte failure suggests an array of numbers is sent at about twice the bytes it holds, or more.wirl devhands a BLOB back the same way, as a plain array of numbers, from.first(),.all(),.raw()and.run().new Uint8Array(row.data)gives the bytes, and binding either it orrow.dataitself stores the same BLOB again.wirl devalso refuses two values D1's bind takes: a view other than aUint8ArrayorUint8ClampedArray(aDataViewor another typed array), which D1 binds as its elements rather than its bytes, and an array of numbers holding a fraction. What D1 stores for either has not been measured. - A request's body has no cap of wirl's own, except on a public path. wirl sets no limit on the body of any request to your app but a public path's (below), so an upload is bounded only by Cloudflare's own limits: Cloudflare publishes 100 MB for a request body on its Free and Pro plans, and more for Business and Enterprise, and 128 MB of memory for a Worker, which matters because a request's body is read whole (above). wirl has not measured where an upload fails.
- No organisation-level form to add a connection. Add one from an app's Connections page, or with
wirl connections create. - Undeclared hosts are refused. An outbound call to a host with no declared connection never leaves wirl.
- A connection with no key yet refuses every call, until someone adds the key.
- Sharing and inviting are metered. Sharing an app with, or inviting to an org, somebody who has no wirl account yet sends them an email, so it is limited: 50 such invitations a day per person and 200 a day per organisation. Past either, the share is refused with a message that says which limit was met, and the first refusal in an hour shows in Activity as
invitation.refused. Separately, no address is sent more than 5 invitation emails a day, whoever sends them: past that the share still happens and waits for their first sign-in, but no email goes, and the answer says so (mailed: falsefrom the API) so you can tell them yourself; those shares are counted too, 50 a day per person, apart from the ones that send mail. Sharing with somebody who already has an account sends nothing and counts against none of those. Every share or invitation of either kind is limited to 60 in ten minutes per person. - A public path's body is capped at 1 MB, a verified one is rate-limited to 600 requests a minute per app, and a
nonepath to 120 a minute per sender address and 600 a minute per app (counted per Cloudflare location, approximate by design). See Public paths. - An app holds at most 5 previews at once, each gone 14 days after it was last deployed unless redeployed under the same name. A preview's own database starts with its tables from
migrations/and no rows but the ones those files insert, and is never seeded from production. See Previews.
Flows#
What a flow may do, and what happens past each limit. An app's previews together draw on pools of their own, apart from its live runs', so a flow tried out on a preview never uses up the live app's steps a day, steps at once or unfinished runs. Of the limits below, two count live and preview together: an org's unfinished runs, and its share of wirl's starts a second (Starts, below). One above does too: the 20 streamed answers an app may have open at once count a preview's with the live app's, so a preview's step that reads a model's stream holds one of them while it does.
- A step is one request.
step_timeoutis 5 minutes unless the flow says otherwise, at least 30 seconds and at most 15 minutes. wirl cuts a step off at itsstep_timeout, 60 seconds before Cloudflare would give up on it, and refuses the step's outgoing calls from 5 seconds after that. A step with less than 5 seconds of its time left is not sent, and wirl tries it again. - An answer is at most 1 MiB (1,048,576 bytes) of JSON, and what wirl keeps of a step, your answer with its own record of the step, at most 1,044,480 bytes, so an answer within a few KiB of 1 MiB can still be refused: keep well under it. Past either, the run fails at once, saying to keep rows in
env.DB. An answer, a run's input and an event's body nest at most 100 levels of objects and arrays. - A run's input and an event's body are each at most 64 KiB. A finished run's output is kept up to 64 KiB; past that the run still finishes, and its
outputreadsnullwithoutputTruncated: true. - A step that fails (it throws, runs out of time, or answers
5xx) is tried againretriestimes: 5 unless the flow says otherwise, at most 10. The tries wait 10 seconds, 1 minute, 10 minutes, 1 hour and 6 hours, and 6 hours before each try after those. ARetry-Afterthe step answers with is honoured, held between 10 seconds and 6 hours, and after a step that ran out of time the next try waits at least 90 seconds, since the first may still be running. - A sleep, a wait's timeout and a
retrydelay are each at least 30 seconds and at most 30 days. A step may answerretryat most 30 times; one more fails the run. - Steps in a run. Every try of a step counts, and so does every wait for an event, for a slot (below) or for the day's pause to end; a sleep does not. Cloudflare's default is 10,000; wirl stops a run at 9,900 and says why: end it, and start another that carries on from its state.
- What a run keeps. Cloudflare keeps at most a gigabyte for one run. wirl stops a run before its answers and the events its waits heard come to that, and says why: keep rows and messages in
env.DB, and ids instateand in event bodies. - Unfinished runs (every run that is not done, failed or cancelled): 10,000 for an app's live runs, 1,000 for all of its previews together, and 50,000 for an org, which counts both. A start past one is refused with
409, saying which. - Steps at once: 50 for an app's live runs, and 10 for all of its previews together. A step past them waits its turn, trying again after 15 seconds and then twice as long each time, up to 10 minutes, each wait up to a tenth longer; each of those waits counts as a step of the run, and toward the day's steps.
- Starts: 10 a second for an app's live runs and 10 a second for its previews together. Across everyone, wirl takes at most 21 starts a second today, shared so that no org holds more than about half: an org that has started more than 10 in the current second, live and preview together, gets no more in it, so one org holds at most 11. Past any of these a start is refused with
429and aRetry-Afterin seconds: wait that long, then start it again. The refusal says which it met:"error": "too_fast_app"for the app's, and"too_fast_all"for wirl's, an org past its share of it included. A start that repeats a flow's key is answered with the run it found and counts toward none of these. - Steps a day: 50,000 for an app's live runs and 10,000 for all of its previews together, counted apart, by UTC day. Each try of a step counts one, whatever came of it, and so does each wait for a slot or for the day's pause to end, and each
failedcall; a sleep, a wait for an event and thecancelledcall count none. Past either, the runs it counts pause, never fail, until 00:00 UTC, and the app's owner is told once that day. - Events: at most 10 a run has taken and not yet delivered; at most 10 sends a second to one wait, of the event it waits for; a body of at most 64 KiB; a name of 1 to 100 letters, digits, dashes or underscores. A wait takes no send in the 5 seconds before its timeout.
- The
cancelledcall has 30 seconds. - A failed run can be retried from 4 minutes after it failed until 3 days after, while the run is still kept (below). The 4 minutes are there so that nothing the failed go sent can still land in the new one.
- Flows: at most 20 an app, each named with 1 to 40 lowercase letters, digits or dashes.
--fast, on a preview's runs only: every sleep,untilandretrydelay becomes 5 seconds, every wait's timeout 60 seconds, and every rung of the retry ladder 10 seconds, aRetry-Afteron a5xxanswer included. The 90 seconds after a step that ran out of time stays, as do the 4 minutes before a failed run can be retried.- What is kept: wirl has Cloudflare keep a finished run 1 day and a failed one 3 days. wirl keeps a run's record, with its steps and events, for 30 days after it ends, and a failed run's input for 3 days. The input of a run that finished or was cancelled is dropped when it ends.
Migrations#
How much the files in migrations/ may hold, and how wirl applies them.
- The folder holds at most 200 files and 2 MiB (2,097,152 bytes) in all, counted before any file is read. A file holds at most 500 statements, and a statement at most 100,000 bytes of UTF-8, D1's own limit.
- Each request to D1 has 30 seconds, Cloudflare's limit, for everything in it. On a database already on migrations, wirl sends one file a request, so one file's statements share the 30 seconds; on a new database it sends as many files together as fit in 500 statements and 4,000,000 bytes, wirl's own statements included and counted as the request carries them. A file is never split: one that does not fit beside others goes alone, and one too large to go even alone is refused before anything is sent. A request that runs past 30 seconds is cut off by Cloudflare with nothing of it applied, and the deploy fails.
- An apply sends no more files once it has been applying for 2 minutes, and never cuts off a request it has sent; the deploy then fails, naming what applied and how to apply the rest.
- At once: one apply at a time touches a database, at most 2 of an org's databases are applied to at once, at most 2 applies run for one person, whatever the org, and at most 16 on one control plane. Of the org's 2, one app takes at most 1 at a time, its live database and its previews' together. A deploy that brings no file its database has not applied does not wait for the org's 2, the app's 1 or the person's 2: it reads the database's record instead and goes on with nothing applied, unless another apply holds that database before it is done reading, an earlier apply's answer there is still to be settled, or the read cannot be made: at most 2 of an org's such reads run at once. A deploy or adopt turned away by any of these applies nothing, and answers
409with aRetry-After: when the database's hold ends at the latest, or a minute. An apply whose answer from D1 was lost holds its database for up to 10 minutes, so the next one starts only once D1 has finished or dropped that request. That hold is its database's alone: it counts toward neither the org's 2 nor its app's 1. - Calls: an org's applies and adopts are held to 300 calls to D1 in five minutes, a quarter of what Cloudflare allows wirl's account. So are the calls of a deploy with nothing new to apply that does not wait (above): its lookup of the database, and each read twice, for the second try a read may take. When the budget cannot take them, the deploy is turned away as it would have been. An apply or adopt that would start too close to that is refused before it starts, with
429and aRetry-Afterof a minute, and nothing is applied. One already under way sends no request that would pass it: the deploy fails there, naming the files that applied before it, and says to deploy again in a few minutes.wirl migrationstakes nothing from it: each person may list 20 times in five minutes, and the next is refused with429and aRetry-Afteruntil the oldest of those is five minutes old. - A failed statement's message, D1's own words about it, reaches the person deploying cut to 2,000 characters; wirl keeps it nowhere.