No. 18 · JUN 2026 · 6 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, wants Cloudflare Artifacts, a git repository per user at a scale where a million of them is ordinary.

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, and a handler that throws is retried with exponential backoff until it succeeds. There is one alarm per object and no limit on objects, so a hundred thousand tenants are 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 where three schedules on a Worker are enough; Queues carry jobs that run for minutes rather than seconds. None of it is an external scheduler reaching in.

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: a Lambda MicroVM lives up to eight hours, long enough to hold its own timer for a working session, and for anything longer the control plane records the next-fire time and relaunches the sandbox from its snapshot when it comes due. The tenant’s app owns its clock either way. The scheduler stopped being a service every job routes through and became 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.