all posts

Modal vs Daytona for AI Agent Code Execution

Ajay Kumar··9 min read

"Modal vs Daytona" gets searched at a very specific moment: you're building an agent, it needs to run code that isn't yours, and both products came up in the same evening of reading. They look adjacent. Both are cloud compute, both hand you a Python SDK, both promise you won't be running servers. But they aren't really competitors. Modal is built around a function you wrote. Daytona is built around a machine your agent works inside. Nearly every argument about which one is "better" turns out to be an argument about which of those two shapes your workload actually has.

I'm Ajay, I built PandaStack — this post is about choosing between Modal and Daytona by workload shape rather than by feature scoreboard, and about the third shape neither of them is centred on. I'll be specific about PandaStack because I can measure it, and deliberately qualitative about the other two. I'm not going to publish a benchmark of somebody else's platform or quote a price that will be stale before this post is a month old.

Everything here about Modal and Daytona is descriptive, not benchmarked, and reflects their public positioning as of writing. Pricing, isolation details, and limits are the fastest-moving pages in any compute platform's docs. Check their current docs before you make an architecture decision — this moves fast, and a blog post is not a source of truth about someone else's product.

What each one is actually for

Modal's centre of gravity is Python-first serverless compute. You decorate a function, describe the container image in the same Python file, and the platform takes it from there: build the image, ship the code, run it — once, or ten thousand times in parallel. The mental model is that infrastructure disappears behind the decorator. It carries batteries for the workloads that motivated it: GPU attachment, long-running batch and ML jobs, scheduled work, wide fan-out over large inputs. If your unit of work is "this function that I wrote, run it on hardware I refuse to think about," Modal was designed for exactly that sentence.

The thing worth noticing is the direction of the abstraction. Modal wants your code to come to it. That's a genuine strength — image-as-code means no separate Dockerfile quietly drifting out of sync with your dependencies, and no cluster to babysit. It's also the constraint. The unit is an invocation: it starts, it runs, it returns, it goes away. Durable state is a deliberate add-on, not the default.

Daytona: an environment your agent works inside

Daytona's centre of gravity is provisioning environments — sandboxes and dev environments for agents and developers, created quickly through an SDK. The unit isn't a function call; it's a workspace. Something with a shell, a filesystem, and a lifetime. An agent connects to it, clones a repo into it, runs a build, reads a stack trace, edits a file, runs the build again. The environment is the product, not a side effect of running the product.

That shape matters more than it sounds. A coding agent doesn't do one thing. It does forty things in a row, and each one depends on the previous one's side effects on disk. If your workload is "an agent needs a computer for a while," a workspace API fits the grain of the problem and a function API spends its life fighting it — you end up rebuilding a filesystem, a working directory, and a session out of object storage and hope.

The axes that actually decide it

Here's the honest comparison. I'm keeping the competitor columns qualitative on purpose: the shapes are stable, the specifics aren't, and I'd rather be right in six months than precise today.

  • Unit of work — Modal: an invocation of a function you wrote. Daytona: an environment that exists for a while and accumulates state. microVM sandbox API: a whole machine created per task and thrown away.
  • Language surface — Modal: Python-centric by design; the decorator model is the API. Daytona: SDK-driven, but what you get is a shell, so the language surface is whatever you install into it. microVM sandbox API: also a shell — the SDKs wrap exec, so the guest runs any language you can install.
  • Statefulness — Modal: ephemeral per invocation; durable state is explicit (volumes, object storage). Daytona: long-lived workspace with a filesystem that survives across steps. microVM sandbox API: your call — ephemeral by TTL, persistent, or snapshotted now and restored days later.
  • Isolation model — Modal and Daytona: both document their own sandboxing approach and both treat untrusted-code isolation as a real requirement; read their current security docs rather than trusting any blog's summary, including this one. microVM sandbox API: PandaStack gives every sandbox its own Firecracker microVM with its own guest kernel behind a hardware virtualization boundary. Whatever you pick, make the vendor tell you exactly where the kernel boundary sits.
  • Cold start — Modal: optimised hard around function start, with warm-container reuse doing much of the work. Daytona: optimised around fast sandbox creation, which is the core pitch of a workspace platform. microVM sandbox API: PandaStack restores a baked snapshot on every create — p50 179ms end to end, p99 around 203ms, of which the restore step itself is roughly 49ms.
  • Scale to zero — Modal: no invocations means no compute charge; the abstraction genuinely has nothing running between calls. Daytona: means the workspace is stopped or destroyed, and an idle workspace is a decision you own. microVM sandbox API: means the VM is gone and its snapshot is a file in object storage, so the next create pays a restore rather than a boot.
  • GPU story — Modal: first-class and well developed; for many teams it's the reason they chose it. Daytona: environments for code work, not a GPU inference platform. microVM sandbox API: PandaStack is CPU sandboxing — if you need GPUs, stop reading this column.
  • Self-host and open source — Modal: managed service. Daytona: has an open-source story; confirm the current licence and what self-hosting actually covers before you plan around it. microVM sandbox API: PandaStack is Apache-2.0 and runs on your own KVM hosts if you want it to.
  • Billing shape — Modal: metered around invocations and the resources they consume. Daytona: shaped around environment runtime. microVM sandbox API: shaped around how long the VM lives, so a four-second task costs four seconds. Every one of these has changed at least once; read the pricing page, not this list.

"Fast" depends entirely on what you're timing

Cold start is the most misread number in this category, because the three platforms aren't timing the same event. A function platform's cold start is "how long until my code's first line runs," and a warm container makes it nearly free. A workspace platform's create time is "how long until an agent can type into a shell." A microVM API's create time is "how long until a fresh guest kernel with its own network stack is accepting connections." Comparing those head-to-head is like comparing the time to open a text editor against the time to boot a laptop.

What you should actually compare is the amortised cost across your real loop. If your agent does one 200ms thing per user request, start latency dominates everything. If it runs a 90-second build, nothing about start time matters and you should be optimising your dependency cache instead. If it runs thirty small steps that each depend on the previous step's files, you want an environment that persists — because any per-step start cost gets multiplied by thirty, and because rebuilding state between steps is where the bugs live.

The useful question isn't "whose cold start is lowest." It's "how many times does my loop pay it?" A 3-second start paid once per session is invisible. A 300ms start paid per tool call, forty calls per task, is your entire latency budget.

The third shape: a machine cheap enough to create per task

Between "a function that vanishes" and "an environment you keep" there's a third option, and it only becomes viable if creating a machine is genuinely cheap: a hardware-isolated microVM you create for one task, use, and destroy. No pool, no reuse, no residue.

That's the shape PandaStack is built in. There is no warm pool of idle VMs waiting around — every create restores a baked Firecracker snapshot through pre-allocated networking. p50 is 179ms end to end, p99 around 203ms, with the snapshot restore step itself roughly 49ms. Only the very first spawn of a template does a real boot, around 3 seconds; after that the snapshot exists and every subsequent create is a restore. Each host pre-allocates 16,384 /30 subnets so network setup never lands in the hot path.

This changes the economics of isolation, which is the actual point. When a machine costs a fifth of a second, you stop reusing them — and reuse is where most sandbox security bugs actually live: the leftover file in /tmp, the cached credential, the background process that outlived its task. "Destroy it" is a far easier invariant to hold than "clean it thoroughly." Cleanup code is the least-run and least-tested path in any system, and it is asked to be perfect.

Fork: branching an environment that's already running

The capability neither a function platform nor a workspace platform typically exposes is fork. Snapshot a warmed sandbox — dependencies installed, repo cloned, test suite already run once — and then create N independent copies of that exact state, memory and disk together. Same-host forks land in 400-750ms; cross-host is 1.2-3.5s because the memory image has to travel. Memory pages are shared copy-on-write until something writes, and rootfs clones are reflinks, so the tenth fork is not the tenth copy of your toolchain.

A function platform structurally can't do this: there's no long-lived state to branch from. A workspace platform can usually clone a template, but cloning a template is not the same operation as branching a machine that is mid-execution with a warm process tree and a populated page cache. For best-of-N agent sampling, speculative refactors, or exploring several fix candidates against the same failing test, that difference is the entire feature.

The agent-needs-a-machine pattern, in code

Here's the shape in the PandaStack Python SDK. It's deliberately boring — create, write, exec, read, kill. The interesting part is the finally block and the TTL.

from pandastack import Sandbox

def run_agent_task(task: str, files: dict[str, str]) -> str:
    """Give the agent a real machine for the length of one task, then burn it."""
    sbx = Sandbox.create(
        template="code-interpreter",
        ttl_seconds=900,                    # dead-man's switch, not a nicety
        metadata={"task": task[:64]},
    )
    try:
        # The agent's working set goes INTO the guest. The host never executes it.
        for path, src in files.items():
            sbx.filesystem.write(f"/work/{path}", src)

        install = sbx.exec(
            "cd /work && pip install -q -r requirements.txt",
            timeout_seconds=300,
        )
        if install.exit_code != 0:
            return f"install failed: {install.stderr[-1000:]}"

        run = sbx.exec(
            "cd /work && python3 solve.py --task-file task.txt",
            timeout_seconds=600,
        )
        print("exit:", run.exit_code, run.stdout[-500:])

        # Only the artifact crosses back to the trusted side.
        return sbx.filesystem.read("/work/out/answer.md")
    finally:
        sbx.kill()   # files, processes, network namespace: all gone together

Note ttl_seconds. It's there because your orchestrator will eventually crash between create and kill, and when it does, the VM still needs to die. Never rely on your own cleanup path being reached — it's the code path that runs least often and gets tested least, and it is somehow always the one holding the bill.

Best-of-N with snapshot and fork

Now the operation you can't express on a function platform at all. Warm one machine, freeze it, and branch it eight ways. Each branch is a real independent microVM, not a thread and not a shared workspace with eight agents elbowing each other in the same working directory.

from concurrent.futures import ThreadPoolExecutor
from pandastack import Sandbox

N = 8   # eight independent attempts at the same failing test

# 1. Warm ONE machine: deps installed, repo cloned, suite already run once (red).
base = Sandbox.create(template="code-interpreter", ttl_seconds=1800)
base.exec("git clone --depth 1 https://github.com/acme/service /work/repo", timeout_seconds=300)
base.exec("cd /work/repo && pip install -q -e '.[dev]'", timeout_seconds=900)
base.exec("cd /work/repo && pytest -x tests/test_billing.py", timeout_seconds=600)

# 2. Freeze it. Memory and disk, exactly as it stands right now.
snap = base.snapshot()
base.kill()

# 3. Branch. Same-host forks are 400-750ms and share memory copy-on-write,
#    so eight attempts cost one dependency install, not eight.
def attempt(i: int) -> dict:
    fork = snap.fork(ttl_seconds=1800)
    try:
        fork.filesystem.write("/work/repo/patch.diff", candidate_patch(i))
        fork.exec("cd /work/repo && git apply patch.diff", timeout_seconds=60)
        r = fork.exec("cd /work/repo && pytest -q tests/", timeout_seconds=900)
        return {"candidate": i, "passed": r.exit_code == 0, "log": r.stdout[-2000:]}
    finally:
        fork.kill()   # a bad patch poisons one fork, never the base

with ThreadPoolExecutor(max_workers=N) as pool:
    results = list(pool.map(attempt, range(N)))

winner = next((r for r in results if r["passed"]), None)
print("winner:", winner["candidate"] if winner else "none of 8")

The economics here are the reason it's interesting. Eight candidate patches would normally mean eight clones and eight dependency installs — minutes of wall clock and eight copies of node_modules or site-packages. With snapshot and fork you pay that once, then pay sub-second per branch, and each branch still has a hardware isolation boundary around whatever the model decided to run.

When PandaStack is the wrong answer

Being useful here means saying where we lose, plainly:

  • GPU inference at scale — we don't do GPUs. Modal does, seriously and well. If your unit of work is a model forward pass, this comparison ended two sections ago.
  • "I just want to deploy a Python function and never think about a machine" — that is Modal's exact sentence, and a microVM sandbox API answers it by handing you a machine. Wrong tool, on purpose.
  • Python-native fan-out where the container image is the product — image-as-code is a real ergonomic win, and reimplementing it on top of a generic sandbox API is work you shouldn't volunteer for.
  • One long-lived dev environment per human developer — that's a workspace product's home turf. The per-task-disposable model is solving a problem you don't have, and you'll fight the TTL forever.
  • Trusted first-party code with no isolation requirement — if you wrote it, you reviewed it, and it only touches your own data, a hardware boundary per task is a cost with no matching benefit.

I'd rather you pick correctly the first time than churn off us in month three because the shape never fit.

Pick one

  1. Pick Modal if the unit of work is a function you wrote, it's Python, and you want infrastructure to vanish — especially if there's a GPU or a large batch job involved.
  2. Pick Daytona if the unit of work is "an agent, or a developer, needs a computer for a while" and a workspace-shaped API with a persistent filesystem matches how your loop actually runs.
  3. Pick a microVM sandbox API like PandaStack if you're executing code you don't trust, you want a hardware kernel boundary per task, you'd rather create a fresh machine than clean a reused one, or you need to fork a warmed environment to explore several branches at once.
  4. Pick two. This is a genuinely common and correct answer: a function platform for the GPU and batch tier, a sandbox API for the untrusted per-task execution tier. They are not competing for the same request, and pretending they are is how you end up with one tool doing two jobs badly.

If you want the one-on-one detail instead of the shape argument, /blog/pandastack-vs-modal and /blog/pandastack-vs-daytona go head-to-head with each of them directly, and /blog/daytona-vs-e2b covers the workspace-versus-sandbox-API axis from the other side of the table.

Frequently asked questions

Is Modal or Daytona better for AI agent code execution?

It depends on what your agent does between steps. Modal is strongest when the unit of work is a self-contained Python function you wrote and want run at scale without managing machines — its image-as-code model and GPU support are real advantages there. Daytona is strongest when the unit of work is an environment: a shell and a filesystem an agent works in across many dependent steps. A coding agent that clones, builds, reads errors, edits, and rebuilds fits the environment shape. A one-shot transform or an inference call fits the function shape. Verify current capabilities in each vendor's docs before deciding.

Can I use Modal and a sandbox API together?

Yes, and it's a common architecture. The two handle different tiers of the same product. Modal or a similar serverless platform handles GPU inference, scheduled batch jobs, and heavy Python fan-out where you own and trust the code. A microVM sandbox API handles the untrusted tier: running commands and code your model generated, per task, behind a hardware isolation boundary. They aren't competing for the same request, so splitting by trust level and by workload shape usually costs less and fails more predictably than forcing one platform to do both.

How does a microVM sandbox differ from a serverless function for running untrusted code?

The boundary and the lifetime differ. A serverless function is optimised for invocation start, and platforms typically reuse warm execution environments across calls, which means isolation depends on how thoroughly that reuse is scrubbed. A microVM sandbox creates a fresh guest with its own kernel behind hardware virtualization, runs one task, and is destroyed — reuse never happens, so leftover files, cached credentials, and orphaned processes aren't a class of bug you have to defend against. On PandaStack that costs p50 179ms per create, because every create is a snapshot restore rather than a boot.

What is fork and why can't a function platform do it?

Fork branches a running environment. You snapshot a warmed sandbox — dependencies installed, repo cloned, process tree warm — and then create N independent copies of that exact memory and disk state. Same-host forks take 400-750ms and cross-host 1.2-3.5s, with memory pages shared copy-on-write until something writes. A function platform can't offer it because there's no durable in-flight state to branch from; the invocation is the unit. It's the operation that makes best-of-N agent sampling cheap: you pay for one dependency install and then sub-second per candidate.

When should I not use PandaStack?

When you need GPUs — we're CPU sandboxing, and Modal's GPU story is genuinely better suited. When your goal is literally "deploy a Python function and never think about a machine," because a microVM API answers that request with a machine. When the container image is the product and image-as-code ergonomics matter more than the isolation boundary. And when you want one long-lived environment per human developer rather than a disposable machine per task — that's a dev-environment product's home turf, and you'll spend your time fighting TTLs instead of shipping.

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.