Skip to content
Runtime

How to run Haskell code in a sandbox

Install GHC in a Linux microVM, either from apt or with 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. A long compile is billed only for the CPU it uses. Each sandbox is a Firecracker microVM that runs Ubuntu 24.04.5. In that image, 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. 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. They were read on 26 October 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. In a sandbox, that path 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 any program with heavy loops.

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. That library ships with GHC, so nothing is fetched from Hackage.

Compile, then run

Compile with optimization, then run the binary under its own time limit. The compile step writes a type error to stderr, with the file, line and column. The run then 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. +RTS -M512m -RTS then caps its heap at 512 MB 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 while the network is 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. That is the slow part of a first Haskell build. Do this once, then snapshot the sandbox. 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 images build --apt ghc,cabal-install --name haskell

Building an image is free. An account without credit stores its first three images at no charge (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 program. Template Haskell runs code at compile time. A sandbox puts the compiler and the program in a microVM with its own kernel. Network, CPU, memory and cost are enforced on the host (security). A new account gets 100 free hours of a 1 GB machine in its first 30 days. No payment card is needed:

Terminalnpx withruntime login

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

Sources

Checked 26 October 2026.

Facts on this page were checked on 26 October 2026.

Related.

100 free hoursin your first 30 days.

of a 1 GB machine

  • No credit card
  • Four sandboxes at once, 2 vCPU and 4 GiB each
  • Then prepaid credit from $10, no plan fee
Start free, no card
Start free, no card