No. 18 · JUN 2026 · 7 Min Read

Files Without S3, Cron Without EventBridge

Abstract

Storage, scheduling, and secrets are not services you provision per tenant. You compose them from cloud primitives, in userspace, on either cloud.

The series built a sandbox and a policy and said everything else is composed, not built: storage, scheduling, secrets, the surfaces a run operates through. Each is a capability you assemble from a primitive the cloud already sells, scoped by the policy, mounted into the run. Not a service you stand up per customer.

The reflex says otherwise. You need files, so you reach for S3 and a bucket policy. You need a job on a clock, so you reach for EventBridge and a rule. Each reach is a managed service, provisioned and wired from outside the run, and each one multiplied by every tenant is the per-app infrastructure the whole design was trying to avoid. None of that is necessary. Files do not have to live in S3. Cron does not have to live in EventBridge. Most of what a user-code platform needs is already a primitive, and the platform’s job is to compose primitives, not to operate infrastructure.

The Recipe

Every capability is assembled the same way. Find the primitive that fits. Scope it with the policy so a run can only ever name its own tenant’s slice. Mount it into the sandbox at execution time, as a thin client the user’s code calls without holding a credential of its own. Storage, scheduling, secrets, outbound access, each is that recipe with a different primitive in the first slot.

That sameness is what keeps the platform thin and the pieces interchangeable. Swap the storage primitive and nothing else moves. Run the same shape on a different cloud and the policy and the recipe are unchanged; only the primitives have different names. Files and cron are the two people most often think they cannot do without a heavyweight service, so they are the ones worth working out.

Files Without S3

Storage is the part the sandbox piece worked out as a filesystem, because a filesystem is what an agent authors against. The filesystem is an interface. What sits under it is a choice, and S3 is one option among several.

“Store the user’s data” is not a single need. It is a handful of them, with different access patterns. The program’s files and the durable tree the agent edits want an object store, S3 or Cloudflare R2, addressed by a per-tenant prefix. Small hot keys, a session, a counter, a feature flag, want a key-value store like Workers KV, not an object you fetch and parse. Relational queries over a tenant’s own records want a database, and Cloudflare D1 hands each tenant their own SQLite file, which is per-tenant isolation as a physical fact instead of a where-clause. Versioned authoring, where the agent commits and you diff its work against a baseline, maps cleanly to Cloudflare Artifacts, a Git-compatible repository per user with no documented repository-count limit. Artifacts is in closed beta, so using it today requires beta access.

Four primitives, one job. Each is addressed by a key or a path that carries the tenant, and the policy that governs the filesystem governs all of them: a run can only ever name its own. You did not build storage. You picked the primitive that matched the access pattern and scoped it. S3 was never the requirement. It was one answer to one of the patterns.

Cron Without EventBridge

Some user code runs on a clock. A nightly export, a sync every fifteen minutes, a reminder that fires once next Tuesday. The reflex is a central scheduler: EventBridge raises an event, the event invokes the code, and you provision a rule for every schedule. Multiply that by every tenant and every app and you are back to per-app infrastructure, managed from outside the thing it drives.

Scheduling can live in userspace instead, carried by the tenant’s own app. On Cloudflare the primitive is Durable Object Alarms. A Durable Object is a small, addressable, stateful piece of a tenant’s app, and it can set an alarm on itself: wake me at this time, run my alarm() handler. Delivery is at-least-once, so the handler must be idempotent. A thrown handler gets exponential-backoff retries, starting after two seconds, but Cloudflare caps automatic retries at six. Before those retries run out, a job that must keep retrying catches the error, sets a new alarm, and returns. There is one alarm per object, so a hundred thousand tenants can have a hundred thousand independent schedules with no central registry to provision. The schedule lives where the data lives. Cron Triggers cover the coarse, app-wide case. One Worker can have up to three, within an account-wide limit of five schedules on Free and 250 on Paid. Queues decouple the work a schedule emits from the schedule itself. No external scheduler needs a rule for every tenant.

AWS has the managed scheduler if you want it, EventBridge Scheduler, and it is a fine primitive when the schedule is yours rather than a tenant’s. For per-tenant cron the userspace move still applies, but the runtime sets the timer’s reach. A standard Lambda function invocation has a 15-minute timeout. Amazon Bedrock AgentCore Runtime is a different runtime: each session gets a dedicated microVM, and the same live compute can preserve context across invocations for up to eight hours. Its default idle timeout is 15 minutes. AgentCore terminates the microVM when it sits idle that long or reaches its configured lifetime. A later invocation can resume the logical session on new compute, but an in-memory timer does not survive the stop. Only a timer due before that compute stops can live in memory. Anything that may fire after idle or lifetime termination needs its next-fire time in durable storage and a control-plane wake-up to invoke the session again. The schedule remains tenant-owned state, and the wake-up becomes one more primitive you scope, like the files.

The Ones You Already Met

Two of these got their own pieces before this one named the pattern. Identity: when the code acts as the user, the scoped session it runs under is composed from STS and principal tags, mounted into the run, never held as a standing credential. Secrets: the broker is the secrets-and-identity primitive with a vault attached, minting a short-lived token the moment a run needs one. Outbound access is one too, the one most worth scoping, because a sandbox that can reach the open internet can also exfiltrate; you allow the hosts a run actually needs and deny the rest, at the same policy layer.

You meet them one concrete problem at a time, identity, then secrets, then storage, and then notice they are the same move with a different primitive in the first slot. One recipe, run as many times as the platform has capabilities.

Why It Stays Cheap

Per-tenant isolation used to be a luxury, because it meant per-tenant cost. A VM per customer sitting warm is a bill per customer sitting idle. That math is what pushed platforms toward the fat shared service to begin with.

Snapshotting changed the math. A suspended microVM is a snapshot in storage, billed at almost nothing, and it resumes on the next request in a fraction of a second. The primitives are pay-per-use, the same ones you already run for everything else: an object-store read, a KV get, an alarm that fires. So the cost of giving one tenant their own sandbox, their own subtree, their own schedule, their own scoped secrets, is near zero while that tenant is idle, and a tenant is idle almost all the time. The dominant lever is the idle window: suspend aggressively and the warm tail shrinks, at the price of an occasional sub-second resume on a cold request. You tune one number.

That is why the private workshop per customer is affordable now and was not a few years ago. The isolation is structural, the primitives are metered, and the idle case is free. None of it carries the standing cost it used to.

The Thin Platform

Stack it up and the platform you operate is small. A router that resolves a request to a tenant and an app. A policy engine that answers one question, may this principal touch this resource. A thin runtime that mounts the right primitives into a sandbox and tears it down. Everything with weight, the isolation boundary, the object store, the database, the scheduler, the secrets vault, the token exchange, is a primitive you rented and scoped.

You did not build a sandbox, a filesystem, a database, a scheduler, a queue, or a secrets manager. You composed them, behind one policy, on whichever cloud you run. The hard parts are in place and have names and SLAs. What is left is assembly, and the platform is mostly the glue between primitives.