Oblien
Tutorial

How to Let Your Users Run Code Safely Inside Your App

Add safe code execution to your product - users write and run code in sandboxed environments that can't touch your infrastructure.

Oblien Team profile picture
Oblien Team
1 min read

How to Let Your Users Run Code Safely Inside Your App

Every developer tool eventually needs a "Run Code" button. Code playgrounds, AI assistants, educational platforms, no-code builders, internal tools - they all need to execute user-written code somewhere.

The problem is obvious: user code is untrusted. It could be an infinite loop, a crypto miner, or an attempt to access your backend. You need to run it, but you need to run it safely.

This guide explains how to build a secure code execution feature for your product.


The Naive Approach (Don't Do This)

User's code → your server runs it → hope for the best

This is how most MVPs start, and it works until it doesn't. Here's what goes wrong:

  • A user writes while True: pass and your server stops responding
  • A user runs os.system('rm -rf /') and deletes your data
  • A user runs curl to your internal API and gets admin access
  • A user mines cryptocurrency on your EC2 instance

Even with Docker containers, you're sharing a kernel. A container escape - and there are new ones discovered every year - gives the attacker access to your host and every other user's container.


What "Safe" Actually Means

For user code execution, you need:

RequirementWhy
Process isolationUser code can't see or affect other users
Resource limitsCan't consume infinite CPU, memory, or disk
Network controlCan't reach your internal services or other users
Time limitsCan't run forever
Filesystem isolationCan't read your application data
Automatic cleanupNo leftover processes or files after execution

The only way to guarantee all of these is hardware-level isolation - running each execution in its own virtual machine.


The Architecture

Here's how it works in production:

User clicks "Run" in your app


┌─────────────────┐
│  Your Backend    │
│  (API server)    │
└────────┬────────┘
         │ Creates a fresh workspace

┌─────────────────────────┐
│  Sandbox Workspace       │
│  (isolated microVM)      │
│                           │
│  - Own Linux kernel       │
│  - No internet access     │
│  - 30-second timeout      │
│  - 256 MB memory cap      │
│  - Auto-destroys on exit  │
└─────────────────────────┘


    Return output to user

Each code execution gets a fresh microVM. Even if the code is malicious, it can only affect its own disposable environment. Nothing persists. Nothing leaks.


Building It: The Simple Version

Your backend receives the user's code and language, creates a workspace, runs the code, and returns the result.

The flow:

  1. User submits code (e.g., Python)
  2. Your API creates a temporary workspace with:
    • No internet access (air-gapped)
    • 2-minute TTL (auto-destroy if something hangs)
    • remove_on_exit: true (clean up when code finishes)
    • Resource caps (1 CPU, 256 MB RAM)
  3. Execute the code inside the workspace
  4. Capture stdout, stderr, and exit code
  5. Return to the user
  6. Workspace self-destructs

The workspace boots in ~130ms, the code runs, and the whole thing is cleaned up. Zero manual cleanup required.


Handling Different Languages

Each language can use a different base image:

LanguageImageRun Command
Pythonpython-3.13python3 script.py
JavaScriptnode-22node script.js
Gogo-1.22go run main.go
Rustrust-1rustc main.rs && ./main
Bashubuntu-24bash script.sh

All images come pre-installed with common tools. The user's code is written to a file in the workspace, then executed.


Security Deep Dive

No network = no exfiltration

With allow_internet: false and empty ingress, the sandbox is completely network-dark. The user's code can't:

  • Call external APIs
  • Scan your internal network
  • Download malware
  • Send data to webhooks

No persistence = no attack surface

With remove_on_exit: true, the workspace is destroyed the moment the code finishes. The encrypted disk is wiped. There's nothing to exploit later.

Resource caps = no abuse

Hard limits on CPU and memory mean a crypto miner gets 1 CPU core for 30 seconds before the sandbox self-destructs. Not exactly profitable.

Kernel isolation = no escape

Unlike Docker (shared kernel, 300+ syscalls), each sandbox has its own Linux kernel. The VM boundary is enforced by hardware (KVM). There are zero known escapes from Firecracker microVMs in production.


Making It Fast: Pre-Warming

If ~130ms boot time isn't fast enough for your UX, you can pre-warm sandboxes:

  1. Create a pool of idle workspaces ahead of time
  2. When a user runs code, grab one from the pool
  3. Execute the code and destroy the workspace
  4. Replenish the pool in the background

This gives you near-instant code execution - the user clicks "Run" and sees output in under a second.


Adding Package Support

Users want to import libraries. Two approaches:

Approach 1: Pre-installed packages

Build custom images with popular packages pre-installed:

  • Python: numpy, pandas, requests, flask, fastapi
  • Node.js: express, lodash, axios, react

Approach 2: Install on demand

Allow a setup step before execution:

# User provides:
# packages: ["numpy", "pandas"]
# code: "import pandas as pd; print(pd.__version__)"

# Your backend runs:
# 1. pip install numpy pandas
# 2. python3 script.py

This is slower (network needed for install) but more flexible. You can use egress rules to only allow connections to PyPI/npm registry and nothing else.


Streaming Output

For long-running code, stream output in real-time instead of waiting for completion.

Your backend starts the execution with streaming enabled, then forwards the output to the user's browser over a WebSocket or Server-Sent Events connection. Each chunk of stdout/stderr is sent as it happens.

This gives users the feeling of a real terminal - they see output line by line, not all at once after 30 seconds of waiting.


Real-World Examples

Code playground (like Replit)

Users write code in a browser editor and click Run. Each execution creates a fresh sandbox. For persistent projects, use permanent workspaces that the user can come back to.

AI assistant with code execution

Your chatbot generates code and runs it to verify the output before showing it to the user. The AI never sees other users' code because each execution is isolated.

Educational platform

Students submit assignments that run in sandboxes. The teacher sees the output. No student can access another student's work or the grading system.

Internal tool builder

Employees write automation scripts in a web UI. Each script runs in a sandbox so a buggy script can't take down your internal infrastructure.


The Comparison

ApproachBoot TimeIsolationCostCleanup
Run on your serverInstantNoneFree (until hacked)Manual
Docker container~200msKernel-sharedLowManual
AWS Lambda~100-500msFirecracker VMPer-invocationAutomatic
Oblien workspace~130msHardware (KVM)Per-secondAutomatic

Oblien gives you Lambda-grade isolation with full control over the environment - any language, any package, any runtime, persistent filesystem, SSH access, and real-time streaming.


Summary

Safe user code execution requires:

  1. Hardware isolation - each execution in its own VM
  2. Network lockdown - no internet, no internal access
  3. Time limits - automatic cleanup if code hangs
  4. Resource caps - no CPU/memory abuse
  5. Auto-destroy - nothing persists after execution
  6. Fast boot - users shouldn't wait more than a second

MicroVMs make this practical. You get real Linux environments that boot in 130ms, run anything, and self-destruct when done.

Stop worrying about what your users' code might do. Give it a sandbox and let it run.

Learn moreHow to Run Untrusted Code Safely | Oblien Documentation