The craftcms/cloud Extension Package
The Yii2 module that adapts Craft to run on Cloud's serverless infrastructure. It auto-bootstraps when present, overrides Craft components for the Lambda environment, and surfaces a few Cloud-specific APIs (App::isEphemeral(), cloud.esi(...), the Cloud filesystem types).
Source & docs
- Source: https://github.com/craftcms/cloud-extension-yii2
- Composer:
craftcms/cloud(typeyii2-extension) - Docs: https://craftcms.com/docs/cloud/extension
- Plugin development guidance: https://craftcms.com/docs/cloud/plugin-development
This reference is verified against craftcms/cloud-extension-yii2@main on 2026-05-28 — the docs page is intentionally light, so most of what follows comes from reading the source directly.
Table of contents
- Common Pitfalls
- What the extension provides
- Cache, queue, and session wiring
- Ephemeral filesystem
cloud.esi(...)— Edge Side Includescloud/up— what runs during the Migrate phase- Console commands the extension adds
- Filesystem types
- Binary responses —
sendContentAsFile()auto-upload - What the extension does NOT provide
Common Pitfalls
- Installing
craftcms/cloudmanually and committing it. Usephp craft setup/cloud(or the equivalent project-config flow) so the extension's config files andcraft-cloud.yamlget scaffolded together. - Trying to
\craft\cloud\Module::getInstance()before the bootstrap completes. The module sets itself as Yii's instance early inbootstrap(), but module-scoped APIs aren't usable untilinit()finishes. Reach for them inCraft::$app->getResponse()-style callsites, not at plugin construction time. - Writing to disk without
App::isEphemeral()guards. Lambda's filesystem is ephemeral; writes outside Craft's path service don't persist across requests. See Ephemeral filesystem below. - Logging to files via custom Monolog channels. The extension routes
Craft::info/warning/error()to a Cloud-managed log target; bypassing that loses the output.
What the extension provides
The craft\cloud\Module class is a \yii\base\Module with BootstrapInterface. When the package is installed, Yii auto-bootstraps it. The module:
- Registers a Twig extension exposing
cloud.*template helpers (most notablycloud.esi(...)). - Registers Craft components:
staticCache(edge cache invalidation),urlSigner(presigned-URL generation for assets). - Reconfigures the
cache,queue, andsessioncomponents for the serverless environment (see Cache, queue, and session wiring below). - Hooks element events to enable Cloud-native image transforms via the
ImageTransformerandImageTransformBehaviorclasses. - Caps execution: 60 seconds for web requests, 890 seconds (15 minutes minus a 10s buffer) for CLI commands. These match the documented Cloud limits and ensure PHP times out before Lambda kills the process.
- Auto-wires filesystem types (
AssetsFs,BuildsFs,CpResourcesFs,StorageFs,TmpFs, etc.) so user assets, build artifacts, CP resources, and Craft's storage path all point to the right S3-backed locations. - Routes logs to Cloud's log target via
craft\log\MonologTarget.
Auto-bootstrap behavior
The module bootstraps differently for web vs console requests — controllerNamespace is set to craft\cloud\controllers (web) or craft\cloud\cli\controllers (console). Local development is detected automatically and the bootstrap is largely a no-op locally; you can leave the extension installed in your dev environment without it hijacking your local DDEV setup.
Cache, queue, and session wiring
The extension (via craft\cloud\AppConfig) reconfigures three core components for the serverless environment. The detail matters because it determines what your code is actually hitting — most importantly, the data cache is not always Redis.
| Component | On Cloud | Backing |
|---|---|---|
cache |
RedisCache when Redis/Valkey is provisioned, otherwise DbCache |
Redis/Valkey, else a MySQL cache table. Wrapped in a CascadeCache with an in-request ArrayCache tier. |
queue |
CraftQueue with an SqsQueue proxy (when useQueue is on) |
AWS SQS — not Redis. |
session |
DbSession when the PHP-session table exists, else Craft's default |
MySQL (setup/php-session-table creates the table during cloud/up). |
The cache: Redis/Valkey → DbCache fallback
The cache resolver checks CRAFT_CLOUD_CACHE_SRV (a DNS SRV record for the Valkey cluster) first, then the deprecated CRAFT_CLOUD_REDIS_URL. If either resolves, the primary cache is RedisCache. If neither is present — i.e. Redis/Valkey isn't provisioned for that environment — the primary cache falls back to craft\cache\DbCache, backed by a MySQL cache table. Lambda's ephemeral filesystem rules out FileCache entirely, so DB is the only fallback. This is why cloud/up runs setup/db-cache-table on every deploy — it guarantees the cache table exists before anything tries to use it.
The consequence to keep in mind: on a no-Redis environment, the cache is a shared MySQL table, and it becomes a contention surface under concurrency — especially during the Migrate phase of a deploy, when the old and new code versions are both live and both reading/writing it. See deploy-pipeline.md → "Never flush the whole cache from a migration" for the failure mode this creates.
Common misconception: "Cache, queue, and session are all on Redis on Cloud." Only the cache uses Redis/Valkey, and only when it's provisioned. The queue is SQS; sessions are DB-backed. Don't assume a Redis-backed mutex or queue exists.
Ephemeral filesystem
Cloud runs Craft on AWS Lambda — disk writes are not durable across requests. Craft sets the CRAFT_EPHEMERAL environment variable on Cloud so application code can detect the environment.
How to check
use craft\helpers\App;
if (App::isEphemeral()) {
// We're on Cloud (or any other environment that flags itself ephemeral).
// Don't write to disk except through the Path service.
}Path service for transient writes
When you do need a transient write (a temp file for processing, a scratch directory for an export, etc.), use Craft's Path service. On Cloud, these paths are backed by the appropriate filesystem types — temp files go to TmpFs, runtime storage goes to StorageFs, etc.
$tempPath = Craft::$app->getPath()->getTempPath();
$storagePath = Craft::$app->getPath()->getStoragePath();
$cachePath = Craft::$app->getPath()->getCachePath();Anything written outside these paths — file_put_contents('/var/log/myplugin.log', ...), mkdir(__DIR__ . '/cache'), etc. — is lost when Lambda recycles the container, which can happen between any two requests.
The pattern in plugin code
The skill-creator pattern is: gate every disk write on a single check at the top of the function, then use the Path service for the actual write.
public function exportToFile(string $filename): string
{
if (App::isEphemeral()) {
$path = Craft::$app->getPath()->getTempPath() . '/' . $filename;
} else {
$path = $this->_legacyLocalExportPath . '/' . $filename;
}
file_put_contents($path, $this->_buildExport());
return $path;
}For plugins that target Cloud as a first-class environment, drop the fallback branch — use the Path service unconditionally. The Path service works correctly on self-hosted Craft too.
cloud.esi(...) — Edge Side Includes
A Twig helper for embedding dynamic content inside an edge-cached page. The edge cache serves the surrounding HTML; the ESI fragment is rendered fresh per request via a signed subrequest.
{# Cached page with a dynamic island #}
{% expires in 1 hour %}
<header>{{ siteName }}</header>
<aside>
{# This fragment is re-rendered every request, even when the rest is cached #}
{{ cloud.esi('_partials/account-nav.twig') }}
</aside>
<main>{{ content|raw }}</main>
{# Passing scalar variables (IDs/handles only — no objects, no collections) #}
{{ cloud.esi('_partials/recommendations.twig', { sourceId: entry.id }) }}Signature
cloud.esi(template, variables = {}) — first arg is a Twig template path resolved like any include; second arg is an optional object of scalar variables.
Constraints
- Scalar variables only. Pass IDs or handles, re-fetch the full object inside the fragment.
- No parent context inheritance. The fragment runs as a fresh subrequest; variables must be passed explicitly.
text/htmlandtext/plainresponses only. ESI tags are only parsed in those response types.- No nesting. Don't put
cloud.esi(...)inside another ESI fragment.
For the full ESI surface (when to use vs avoid, behavior at the gateway, cookie-forwarding caveats), see caching-and-edge.md.
Mechanics under the hood
cloud.esi(...) dispatches to craft\cloud\Esi (the Twig variable) and the matching craft\cloud\controllers\EsiController (the subrequest handler). The helper emits a signed <esi:include> tag at the edge, which the gateway resolves by making a tamper-protected subrequest. You don't write a controller — point cloud.esi(...) at any internal Twig template and the controller handles dispatch.
cloud/up — what runs during the Migrate phase
php craft cloud/up is invoked automatically during the Migrate phase of a deploy. Verified from craftcms/cloud-extension-yii2/src/cli/controllers/UpController.php:
1. trigger EVENT_BEFORE_UP (cancelable — plugins can abort the deploy)
2. run craft setup/php-session-table (ensures PHP session table exists)
3. run craft setup/db-cache-table (ensures DB cache table exists)
4. if Craft is installed:
run craft up (see internal order below)
purge edge static cache
5. trigger EVENT_AFTER_UP (cancelable)Step 4's craft up is itself a sequence — and the order matters for what your migrations can rely on. From craftcms/cms UpController::actionIndex():
migrate/all --no-content— Craft + plugin migrations (the content track is deliberately skipped here).- Save/reset modified project config data.
project-config/apply— if there are pending YAML changes.migrate/up --track=content— content migrations, which therefore run after project config has been applied.clear-caches/compiled-templates.
So content migrations can assume YAML-defined schema (sections, fields, entry types) already exists, but plugin/Craft migrations (step 1) cannot. The full breakdown lives in the craftcms skill's migrations.md → "Execution order in craft up".
You don't run this command yourself in normal operation — it runs server-side during every deploy. The events are useful for plugins that need to hook deploys (e.g. invalidate a custom cache, post a deploy notification to Slack, recompute a derived index).
Hooking the deploy events from a plugin
use craft\cloud\cli\controllers\UpController;
use craft\events\CancelableEvent;
use yii\base\Event;
Event::on(
UpController::class,
UpController::EVENT_AFTER_UP,
function (CancelableEvent $event) {
// Runs once per successful deploy, after migrations + project config + cache purge.
MyPlugin::getInstance()->getDeployNotifier()->notifySlack();
}
);The EVENT_BEFORE_UP variant is cancelable — setting $event->isValid = false in the handler aborts the deploy. Use sparingly; a cancelled migrate phase leaves the old version live but the build artifact is wasted.
Console commands the extension adds
All under the cloud controller namespace (php craft cloud/<command>):
| Command | Purpose | Used by |
|---|---|---|
cloud/up |
Migrate phase entry point | Cloud's deploy pipeline (don't run manually) |
cloud/setup |
Initial extension wiring on a project | Developers, once per project (also accessible as setup/cloud) |
cloud/build |
Build phase entry point | Cloud's deploy pipeline |
cloud/asset-bundles |
Publishes asset bundles to the CDN | Build phase |
cloud/assets |
Asset operations | Internal |
cloud/queue |
Queue worker | Cloud's queue runner |
cloud/static-cache |
Static cache management | Manual cache operations |
cloud/info |
Diagnostics | Manual debugging via Console |
Most of these are infrastructure-only and you won't run them by hand. cloud/info and cloud/static-cache are the two worth knowing about manually — cloud/info reports environment details, and cloud/static-cache lets you purge or inspect the edge cache.
Filesystem types
The extension provides several craft\base\Fs implementations, all S3-backed via Flysystem. The one you'll configure as a normal Craft filesystem (in the CP) is AssetsFs. The others are internal:
| FS class | Purpose |
|---|---|
craft\cloud\fs\AssetsFs |
User-facing — for asset volumes. Configure as a normal filesystem in the CP. |
craft\cloud\fs\BuildArtifactsFs |
Build artifact storage |
craft\cloud\fs\BuildsFs |
Build metadata |
craft\cloud\fs\CpResourcesFs |
CP asset bundles published to the CDN |
craft\cloud\fs\StorageFs |
Craft's storage/ path |
craft\cloud\fs\TmpFs |
Temp paths |
See assets-and-transforms.md for how to configure AssetsFs for a Craft asset volume.
Binary responses — sendContentAsFile() auto-upload
Plugins that serve binary content (file exports, generated PDFs, etc.) via Craft::$app->getResponse()->sendContentAsFile(...) get an automatic Cloud-friendly treatment: the extension uploads the binary to S3 and returns a 302 redirect to a pre-signed URL. This sidesteps Lambda's response-size cap (6MB) and the 60-second request timeout.
You don't need to write Cloud-specific code for this — sendContentAsFile() works the same way you'd use it on self-hosted Craft. The extension intercepts and rewrites the response transparently.
What the extension does NOT provide
- Mail. No SMTP, no managed transactional email service. You configure your own mailer (Postmark, SES, SendGrid, Resend, etc.). See
limitations.md(Mail). - A log-tailing UI. Logs are routed to a Cloud-managed target, but the surfacing UI is the Console command runner's output history — there's no live tail view documented. See
limitations.md(Logs). - A
craft-cloudstandalone CLI binary. All Cloud-specific commands run throughphp craft cloud/*from inside the Craft application. There's no separate binary invendor/bin/or PATH.
Last verified against craftcms/cloud-extension-yii2 (composer.json, src/Module.php, src/cli/controllers/UpController.php, src/fs/, src/twig/) on 2026-05-28. The cache/queue/session wiring section was re-verified against src/AppConfig.php on the 3.x branch (the repo's current default branch) on 2026-06-18; the internal craft up order against craftcms/cms 5.x src/console/controllers/UpController.php on 2026-06-18.