Limitations
What does not run, what runs differently, and the platform limits behind each.
Measured properties of the runtime, listed so they are known before they are hit.
Unavailable Features
- Fibers are not available.
new Fiber()aborts the interpreter, because emscripten implements none of the context-switching calls it needs. The build rewrites core's five uses to a synchronous stand-in with the same surface. exit()in a request is not handled generically. The serve prints its JSON result after the kernel returns, andpib_runperforms no request shutdown, so shutdown handlers never fire. A request that reachesexitends the program before the JSON and answers 500 with "no JSON in the output", and anexitwhile an output buffer is open leaves the buffer open until the object recycles. Measured with SimpleSAMLphp's own redirect. SimpleSAMLphp works through a switch for itsexit; other code that exits does not have one, and the generic handler is planned for v1.1.- Shell commands run only through a router.
exec(),shell_exec(),proc_open()and the rest serve a fixed set of programs with platform equivalents (echo,date,file,zip,unzip,gzip,wget,curl, checksums and a few more). Pipes, redirection and any other program fail with a recorded degradation and not a fatal.sleep()parks the call and is capped per request bySLEEP_BUDGET_MS. - The interpreter loads 27 extensions: Core, PDO, Reflection, SPL, SimpleXML, Zend OPcache,
cfwpark, ctype, date, dom, filter, hash, json, lexbor, libxml, mbstring, pcre, pib, random,
session, standard, tokenizer, uri, vrzno, xml, yaml and zlib. OPcache is loaded and disabled by
default; see
OPCACHE_MODE.mbstringis the native extension;iconvis a PHP polyfill. There is nogdand nopdo_sqlite./phpreports the live list. extension_loaded()is true for a stand-in only when the stand-in covers the extension's whole API. The check is gated by a manifest of the extension's functions, constants and classes, and the shim answers false for any name outside it.sodiumanswers true: the stand-in covers the whole of ext-sodium and a conformance gate holds it to native libsodium, except that password hashing refuses work above a 64 MiB memory cap (see Modules).zstdandbrotlianswer false because their streaming API is not shimmed, andgdanswers false.
Behavior Differences
- A parked call made inside a generator ends the request without output. The park resumes a
chain across invocations, but a network read made in a generator's body finishes with state
doneand nothing printed. Strata'sCommitLog::walk()is one, so strata's cron stops silently at its dictionary stage and its compaction, scheduled drill and telemetry do not run. Measured 2026-11-02. The fix is in the park extension inphasm. - Six more extensions are stand-ins the driver installs at boot: curl, openssl, zip (including
ZipArchive), exif, fileinfo (includingfinfo) and xmlwriter. Each is parity-tested against the real extension, and what a stand-in does not implement is refused at the call and recorded as a degradation, never answered wrongly. The openssl stand-in covers AES and x509. curl_*works withoutext-curl. The functions are supplied over the same transport as the rest of outbound traffic, so an SDK that bundles its own curl transport runs unmodified.curl_version()reports0.0.0-drupflare-shim, and an option the shim does not understand is refused and not ignored. The options Stripe and elastica set are understood.- Passwords hash with bcrypt by default, and argon2id is available.
ARGON2=1switches the password service to argon2id at m=19456 KiB, t=2, p=1, computed on the host and not in PHP. Existing bcrypt hashes keep working and are upgraded at each account's next login. Hashes are written in PHP's own encoded form, so they verify on any PHP withext-argon2. - Image derivatives are produced in the Worker and not by
gd. The toolkit hands the work to@gmitch215/tinyimg, a wasm encoder that runs in the front worker and needs no zone, no binding and no monthly transform allowance. It covers scale, crop, rotate, desaturate and format conversion, and encodes JPEG, PNG and WebP. It does not encode AVIF: core asks the toolkit before applying an AVIF effect and falls through to the style'swebpfallback. Cloudflare Images is reachable withIMAGE_ENGINE=imageson a zone that wants AVIF. Styles at or below 480 px on the long edge are produced during the request; larger ones are produced on the alarm chain. A public image style URL is rewritten to the/cfw-img/delivery path. - Outbound HTTP completes inside the request that asks.
Drupal::httpClient()and a Symfony HTTP client park the PHP call while the Worker performs the fetch, and the same call resumes with the answer. Where a park is refused (a call made from inside an internal function such asarray_map), the request falls back to the deferred transport: a previous fetch's response is returned and no connection is opened. A URL the site can predict from its own installed projects is warmed on the alarm before Drupal asks for it. A URL it cannot predict is queued, and an idempotent request that deferred is drained and rendered once more inside the same visit, so the answer usually arrives on the first request. Request headers are carried across the queue and are part of the cache key, exceptUser-Agent, which is sent but not keyed. A non-idempotent request is never re-driven: a replayed POST is a different outcome. - A page that has been rendered before does not wait for a re-render. After a content change, a
request for a path with history is answered from the previous generation with
x-cfw-edge: STALEwhile the current one regenerates on the alarm chain. It is bounded two generations and 24 hours deep, never applies to a session-carrying response, and refuses a deny-list thatNEVER_STALEextends. The previous generation is kept in the KV page tier, which is on for both plans wheneverPAGE_KVis bound (PAGE_KV_ENABLED). A free site stops writing pages at its daily KV write budget. - Outbound TCP is either parked or declared, depending on which the module needs.
Drupal\drupflare\Network\CfwTcpdescribes a whole exchange and reads the answer on a later request over a queue, which suits syslog. A module that needs the answer inside the request that asked, which is what a cache read is, gets it from the park: the PHP call is suspended mid-frame, the Worker performs the socket read, and the same call resumes with the result.drupal/redisis verified on that path. The endpoint and its credentials come fromREDIS_URLandSYSLOG_URLand not from the calling code, and administrative Redis commands are refused. The Durable Object's own SQLite is still the recommended cache backend, because a parked get is a network round trip where SQLite is a local read. - Config import and queue drains run in steps.
POST /ops?op=cim&drive=1andop=queue-drain&drive=1run the first step in the request and leave the rest to the alarm, one step per firing.docs/configuration/index.mdhas the parameters. - Greek word-final sigma lowercases differently from native PHP, and
mb_strwidthunder-counts emoji. Neither affects Drupal core. - An object reset for memory costs a form submission a retry by hand. A GET or HEAD that meets a
reset is sent once more to the fresh instance and answers normally with
x-cfw-retried: reset. A POST is repeated only when the object never started it: it records an attempt id before the page runs, so a missing record means nothing was saved. A POST it had started gets a short "Try Again in a Moment" page withRetry-After: 2instead of Cloudflare's 1101, because it may already have saved. - A freshly deployed site is claimable until it is provisioned.
/firstrunanswers without a credential while the site has never been configured, because the owner token that/exporttakes is minted by that route and nowhere else; gating it would mean the only way to get your data out was to expose/sql,/restoreand/phpfirst. The window is the unprovisioned state only: once configured the route answers 409, and reconfiguring needs the token. It is the same model as Drupal's owninstall.php, and the window is signposted: an unclaimed site answers a browser navigation with the claim page instead of its front page. Claim the site as the first thing you do after deploying.
Platform Limits
- Case-insensitive matching is ASCII-only. Durable Object SQLite has no user-defined
collations, so Drupal's
NOCASE_UTF8becomes builtinNOCASE.Ünicodedoes not matchünicode. This affects username and email uniqueness and taxonomy matching. LIKE/GLOBpatterns cap at 50 bytes. A Views "contains" filter on a longer search string fails in the engine.- A statement caps at 100 bound parameters.
REGEXPis evaluated in PHP by the driver, as areMD5()andSUBSTRING_INDEX()in a select list. The engine has none of them, so the driver widens the statement and filters the rows itself: Views regex filters work, and on a large table they cost a scan.- Integers above 2^53 are read back through a second, casting query. The cursor returns doubles, so the driver detects a value a double cannot hold and re-reads that statement with the affected columns cast to text.
- A 64-bit integer does not survive a JSON round trip.
PHP_INT_SIZEis 8 andPHP_INT_MAXis 9,223,372,036,854,775,807, but a JSON number is a double, so a value above 2^53 handed across the host boundary comes back rounded. Cast it to a string first. The database path already does this. - A module install leaves the object with little room to do anything else. It is the most
expensive thing a site can do, and a booted interpreter already holds most of the object's
~195 MiB memory budget.
memory.growhas no inverse, so the heap never shrinks; a further growth is served at a smaller step and not refused. The install queues the pages it invalidated instead of re-rendering them, and the cache is cold for one visit. - A cold object rebuilds Drupal, and the heap image does not help. An evicted Durable Object has
no interpreter, so the next request that needs one pays a boot.
HEAP_IMAGEstores a heap so that boot can be replaced by a restore, and it is off by default: measured on two deployed workers, the restore costs more than the boot it replaces, and it consumes storage on every site as well.1opts in. What keeps a cold boot off the visitor's path is the page cache, the compiled plan, the stale-generation serve, and a resident object while a session is active. - Writes do not scale, and a replica pool does not change that. A lane refuses an authoritative
write and hands it back, so every write is still one object and one thread. What a pool scales is
authenticated reads; see Replica Lanes and
REPLICA_COUNT.