Runtime

How to run Haskell code in a sandbox

Install GHC in a Linux microVM, from apt or GHCup, then run the file with runghc or compile it with ghc and run the binary.

On Runtime GHC comes from Ubuntu's archive in one command, and a compile that takes a while is billed only for the CPU it uses. Each sandbox is a Firecracker microVM running Ubuntu 24.04.5, where ghc, cabal-install and haskell-stack are apt packages. Runtime charges $0.025 per vCPU-hour of measured CPU and $0.0075 per GiB-hour of memory, so a 2 vCPU, 4 GiB sandbox costs $0.08 an hour at full load and $0.03125 while it waits (pricing).

What to install

Haskell is not in the default image. These are the options, read on 25 September 2026:

Tool Ubuntu 24.04 (noble) package Version on noble Use it for
GHC ghc 9.4.7 ghc and runghc
Cabal cabal-install 3.8.1.0 Packages from Hackage
Stack haskell-stack 2.9.3.1 Stackage snapshots
GHCup (not in apt) get-ghcup.haskell.org its own Newer GHC and switching versions

GHCup installs into ~/.ghcup/bin, which in a sandbox is /workspace/.ghcup/bin (GHCup). Its script runs without prompts when BOOTSTRAP_HASKELL_NONINTERACTIVE=1 is set.

Run a Haskell file

runghc interprets a file with no build step. It suits short programs and exercises; compile anything that loops heavily.

TypeScriptimport { Sandbox } from "withruntime";const program = `import Data.List (sortOn)import qualified Data.Map.Strict as Mmain :: IO ()main = do  let ws = words "the quick brown fox jumps over the lazy dog the end"      counts = M.toList (M.fromListWith (+) [(w, 1 :: Int) | w <- ws])  mapM_ print (take 3 (sortOn (negate . snd) counts))`;await using sbx = await Sandbox.create({ timeoutSeconds: 900, onLeaseEnd: "stop" });await sbx.exec("sudo apt-get update -q && sudo apt-get install -y -q ghc", {  check: true,  timeoutMs: 600_000,});await sbx.network.set({ internet: false });await sbx.files.write("/workspace/Main.hs", program);const run = await sbx.exec(["runghc", "Main.hs"], { timeoutMs: 120_000 });console.log(run.exitCode, run.timedOut, run.stdout);
Pythonfrom withruntime import Sandboxprogram = """import Data.List (sortOn)import qualified Data.Map.Strict as Mmain :: IO ()main = do  let ws = words "the quick brown fox jumps over the lazy dog the end"      counts = M.toList (M.fromListWith (+) [(w, 1 :: Int) | w <- ws])  mapM_ print (take 3 (sortOn (negate . snd) counts))"""with Sandbox.create(timeout_seconds=900, on_lease_end="stop") as sbx:    sbx.exec("sudo apt-get update -q && sudo apt-get install -y -q ghc", check=True, timeout_ms=600_000)    sbx.network.set(internet=False)    sbx.files.write("/workspace/Main.hs", program)    run = sbx.exec(["runghc", "Main.hs"], timeout_ms=120_000)    print(run.exit_code, run.timed_out, run.stdout)

Data.Map comes from the containers library, which ships with GHC, so nothing is fetched from Hackage.

Compile, then run

Compile with optimisation, then run the binary under its own time limit. A type error comes back from the compile step on stderr, with the file, line and column, and the run never starts.

TypeScriptimport { Sandbox } from "withruntime";await using sbx = await Sandbox.create({ image: "haskell", network: { internet: false } });await sbx.files.write(  "/workspace/Sum.hs",  "main :: IO ()\nmain = print (sum [x * x | x <- [1 .. 1000000 :: Int], even x])\n",);const build = await sbx.exec(["ghc", "-O2", "-rtsopts", "-o", "sum", "Sum.hs"], {  timeoutMs: 300_000,});if (build.exitCode !== 0) throw new Error(build.stderr);const run = await sbx.exec(["./sum", "+RTS", "-M512m", "-RTS"], { timeoutMs: 30_000 });console.log(run.stdout);

-rtsopts lets the binary take runtime options, and +RTS -M512m -RTS then caps its heap at 512 MB from inside GHC's runtime, on top of the sandbox's own memory size. This uses the haskell image built below.

Build a Cabal project

Cabal downloads the package index and dependencies from Hackage. Let it do that with the network on, then run the tests offline:

Pythonimport sysfrom withruntime import Sandboxwith Sandbox.create(image="haskell", disk_mib=8192, timeout_seconds=3600, on_lease_end="stop") as sbx:    sbx.files.upload("./parser", "/workspace/parser")    opts = {"cwd": "/workspace/parser", "timeout_ms": 1_800_000, "on_stdout": sys.stdout.write}    sbx.exec("cabal update && cabal build --only-dependencies --enable-tests", check=True, **opts)    sbx.network.set(internet=False)    tests = sbx.exec(["cabal", "test"], **opts)    sys.exit(tests.exit_code or 0)

Dependencies compile from source, which is the slow part of a first Haskell build. Do it once and snapshot the sandbox, and later runs start with the dependencies already built.

Start every sandbox with GHC

TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({ name: "haskell", recipe: { apt: ["ghc", "cabal-install"] } });await using sbx = await runtime.sandboxes.create({ image: "haskell" });console.log((await sbx.exec(["ghc", "--version"])).stdout);
Terminalruntime image build --apt ghc,cabal-install --name haskell

Building an image is free, and the free trial stores your first three images free (custom images).

Why run Haskell in a sandbox at all?

Types do not stop side effects. A Haskell program in IO can delete files, start processes and open sockets like any other, and Template Haskell runs code at compile time. A sandbox puts the compiler and the program in a microVM with its own kernel, with network, CPU, memory and cost enforced on the host (security). New accounts get 50 free sandbox hours, no card:

Terminalnpx withruntime login

See also grading student code, running untrusted LLM code and how to run Elixir code in a sandbox.

Sources

Checked 25 September 2026.

Facts on this page were checked on 25 September 2026.