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 haskellBuilding 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 loginSee also grading student code, running untrusted LLM code and how to run Elixir code in a sandbox.
Sources
Checked 26 October 2026.
- Ubuntu 24.04 (noble) packages: ghc, cabal-install, haskell-stack
- GHCup installation and its bootstrap script (
BOOTSTRAP_HASKELL_NONINTERACTIVE)
Facts on this page were checked on 26 October 2026.