This guide covers three ways to deploy a Turmeric service on Cloudflare infrastructure. They differ in complexity, performance, and what Cloudflare product they target:
| Approach | Product | Complexity | Works today |
|---|---|---|---|
| A. Cloudflare Containers | Containers + Workers | Low | Yes |
| B. Interpreter-in-WASM | Workers | Medium | With adaptation |
C. AOT WASM via emit-c |
Workers | High | Requires build tooling |
Recommendation: start with Approach A. It uses the native tur binary inside a
Docker container and requires no changes to your Turmeric code. Approaches B and C are
better fits once you need true edge distribution or sub-millisecond cold starts.
| Tool | How to get it |
|---|---|
Turmeric (tur) built from source |
just build |
| Docker | https://docs.docker.com/get-started/get-docker/ |
| Cloudflare account (paid, Workers Paid plan) | https://cloudflare.com |
| Wrangler CLI | npm install -g wrangler |
All three approaches use the same logical service: a minimal HTTP server that responds to any request with a plain-text greeting. The implementation differs per approach.
How it works: the native tur binary runs inside a Docker container. Cloudflare
spins up the container on demand and a thin Cloudflare Worker routes incoming HTTP
requests to it.
;; service.tur -- minimal HTTP service for Approach A (Cloudflare Containers)
;;
;; Listens on $PORT (default 8080), accepts connections concurrently using the
;; cooperative fiber scheduler, and returns a fixed HTTP 200 response.
(import "stdlib/async_socket.tur")
(import "stdlib/scheduler.tur")
(import "stdlib/fiber.tur")
(import "stdlib/args.tur")
(import "stdlib/env.tur")
(defn http-ok [body :cstr len :int] :cstr
(str-concat "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\nContent-Length: "
(str-concat (int->str len)
(str-concat "\r\n\r\n" body))))
(defn handle-client [fd :int] :nil
(async-socket-recv fd 4096)
(let [body "Hello from Turmeric!\n"
resp (http-ok body 21)]
(async-socket-send fd resp (cstr-length resp)))
(async-socket-close fd))
(defn accept-loop [listen-fd :int sched :ptr<void>] :nil
(let [client-fd (async-socket-accept listen-fd)
f (fiber-new (fn [] :nil (handle-client client-fd)) 0)]
(scheduler-spawn sched f))
(accept-loop listen-fd sched))
(defn main [] :int
(let [port-str (getenv "PORT")
port (if port-str (cstr->parse-int port-str) 8080)
sched (scheduler-new)
fd (async-socket-listen port 128)
accepter (fiber-new (fn [] :nil (accept-loop fd sched)) 0)]
(println (str-concat "Listening on :" (int->str port)))
(scheduler-spawn sched accepter)
(scheduler-run-to-completion sched))
0)
Test locally before containerizing:
just build
tur run service.tur
# in another terminal:
curl http://localhost:8080/
# Hello from Turmeric!
# Build stage: compile the service to a native binary
FROM ubuntu:22.04 AS builder
RUN apt-get update && apt-get install -y \
build-essential cmake git python3 \
&& rm -rf /var/lib/apt/lists/*
# Install 'just'
RUN curl --proto '=https' --tlsv1.2 -sSf \
https://github.com/casey/just/releases/latest/download/just-x86_64-unknown-linux-musl.tar.gz \
| tar -xz -C /usr/local/bin just
WORKDIR /turmeric
COPY . .
RUN just release
# Compile the service to a standalone binary
RUN build-release/tur emit-c --output-dir /tmp/svc service.tur \
&& cd /tmp/svc \
&& gcc -O2 -o service service.c -lpthread
# Runtime stage: minimal image with only the compiled binary
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y libpthread-stubs0-dev \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /tmp/svc/service /usr/local/bin/service
EXPOSE 8080
ENV PORT=8080
CMD ["service"]
Alternative (simpler, larger image): skip emit-c and ship the tur interpreter
alongside the source:
FROM ubuntu:22.04
# ... install build deps, just build ...
COPY --from=builder /turmeric/build-release/tur /usr/local/bin/tur
COPY service.tur /app/service.tur
EXPOSE 8080
ENV PORT=8080
CMD ["tur", "run", "/app/service.tur"]
The interpreter-based image is bigger (~30 MB vs ~2 MB) but requires no emit-c
compilation step.
Create a new project directory:
my-service/
Dockerfile
service.tur
worker.js
wrangler.toml
worker.js -- the Worker proxies requests to the container:
export default {
async fetch(request, env) {
// env.SERVICE is bound to the Container in wrangler.toml
return env.SERVICE.fetch(request);
}
};
wrangler.toml:
name = "my-turmeric-service"
main = "worker.js"
compatibility_date = "2024-01-01"
[[containers]]
name = "SERVICE"
image = "./Dockerfile"
max_instances = 5
[[containers.port]]
name = "http"
target = 8080
wrangler login
wrangler deploy
Wrangler builds the Docker image, pushes it to Cloudflare's registry, and deploys the Worker. The Worker URL is printed on success.
Advantages
- Works today with no code changes to your Turmeric program
- Full POSIX environment: all stdlib modules work, including async_socket.tur,
fs.tur, concurrent.tur, threads, and effects
- No WASM porting or build flag changes needed
- Container scaling is handled by Cloudflare automatically
Disadvantages - Cloudflare Containers is a paid product (Workers Paid plan required) - Cold starts are measured in seconds (container boot), not milliseconds - Not available at every Cloudflare PoP -- containers run in a limited set of locations vs. the full global Worker edge network - Larger attack surface than a sandboxed Worker
How it works: the Turmeric WASM module (the interpreter itself, compiled by
Emscripten) is bundled inside a Cloudflare Worker. Each request calls turi_wasm_eval
with a Turmeric expression and returns the result.
This approach is best for programs that can express their request handling as a pure Turmeric expression: no long-running loops, no raw sockets, no filesystem access.
From the Turmeric repository root:
just wasm
This produces:
build-wasm/wasm/turmeric.js
build-wasm/wasm/turmeric.wasm
Emscripten's default output targets browser environments. Cloudflare Workers run in
a V8 isolate with no window, document, or XMLHttpRequest. You need to rebuild
with environment flags:
# In CMakeLists.txt or a custom build script, add to the emcc link command:
# -sENVIRONMENT=worker
# -sMODULARIZE=1
# -sEXPORT_NAME=TurmericModule
# --no-entry
Or patch the existing output to remove browser-specific checks. The exact changes
depend on the Emscripten version; see src/web/wasm_glue.c for the exported API.
Rather than a server loop, write a pure request handler. This runs inside the interpreter on each incoming request:
;; handler.tur -- evaluated once per Worker request via turi_wasm_eval
;;
;; The Worker calls:
;; turi_wasm_eval("(handle-request request-body)")
;; after loading the stdlib definitions below.
(defn handle-request [body :cstr] :cstr
(str-concat "Hello from Turmeric! You sent: " body))
The Worker loads these definitions at startup (cold) and calls handle-request on
each incoming request.
// worker.js
import TurmericModule from './turmeric.js';
let tur = null;
async function initTurmeric() {
if (tur) return tur;
tur = await TurmericModule();
// Load the handler definitions once at startup
const src = await fetch('./handler.tur').then(r => r.text());
const initResult = tur.ccall('turi_wasm_eval', 'string', ['string'], [src]);
if (initResult?.startsWith('error:')) {
throw new Error('Turmeric init failed: ' + initResult);
}
return tur;
}
export default {
async fetch(request) {
const module = await initTurmeric();
const body = await request.text();
// Escape body for safe embedding in a Turmeric string literal
const escaped = body.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const expr = `(handle-request "${escaped}")`;
const result = module.ccall('turi_wasm_eval', 'string', ['string'], [expr]);
return new Response(result, {
headers: { 'Content-Type': 'text/plain' }
});
}
};
name = "my-turmeric-wasm-service"
main = "worker.js"
compatibility_date = "2024-01-01"
[build]
command = "just wasm && cp build-wasm/wasm/turmeric.js . && cp build-wasm/wasm/turmeric.wasm ."
[[rules]]
type = "CompiledWasm"
globs = ["**/*.wasm"]
[[rules]]
type = "ESModule"
globs = ["**/*.js"]
Advantages - True edge execution: runs at every Cloudflare PoP worldwide - No container overhead: isolate cold starts are measured in milliseconds - No Docker or infrastructure to manage - Free tier eligible (within Worker CPU/memory limits)
Disadvantages
- The full Turmeric interpreter ships with every Worker (~3 MB WASM) -- counts
against the 1 MB compressed Worker size limit, which may require a paid plan
- Cloudflare Workers have a 10 ms CPU time limit per request on the free tier
(50 ms on paid). Interpreter startup + Turmeric eval must fit within that budget.
- Emscripten output requires adaptation (Worker environment flags, no browser globals)
- POSIX I/O is not available: async_socket.tur, fs.tur, and threading do not
work inside a Worker isolate. Handler code must be pure computation.
- WASM memory is limited to 128 MB in Workers
emit-cHow it works: the Turmeric compiler emits C from your source file (tur emit-c).
Emscripten then compiles that C to a minimal, self-contained WASM module with no
interpreter overhead. The Worker imports the compiled WASM and calls the exported
handler directly.
This gives the best runtime performance but requires the most build tooling.
Because POSIX sockets are not available in WASM, the handler must accept the request as a parameter and return the response as a string. The Worker provides the HTTP layer.
;; handler.tur -- compiled to WASM via emit-c + emcc
;;
;; Exports a single C function:
;; char *tur_handle(const char *method, const char *path, const char *body)
;; which the Worker calls directly via WASM imports.
;;
;; All logic must be pure computation -- no sockets, threads, or filesystem.
(import "stdlib/str.tur")
(defn greet [name :cstr] :cstr
(str-concat "Hello, " (str-concat name "!")))
(defn tur-handle [method :cstr path :cstr body :cstr] :cstr
(greet (if (= (cstr-length body) 0) "world" body)))
tur emit-c handler.tur > handler.c
Inspect handler.c to find the mangled name of tur_handle; it will be something
like tur__handler__tur_handle. You will export this function by its mangled name
in the next step.
Create shim.c to export a stable symbol:
/* shim.c -- stable export wrapping the Turmeric-generated symbol */
#include <stdint.h>
#include <emscripten.h>
/* Forward declaration of the Turmeric-generated function */
const char *tur__handler__tur_handle(const char *method,
const char *path,
const char *body);
EMSCRIPTEN_KEEPALIVE
const char *tur_handle(const char *method, const char *path, const char *body) {
return tur__handler__tur_handle(method, path, body);
}
Compile both with emcc:
emcc -O2 \
-sENVIRONMENT=worker \
-sEXPORTED_FUNCTIONS='["_tur_handle","_malloc","_free"]' \
-sEXPORTED_RUNTIME_METHODS='["ccall","cwrap","UTF8ToString","stringToUTF8","lengthBytesUTF8"]' \
-sMODULARIZE=1 \
-sEXPORT_NAME=HandlerModule \
--no-entry \
handler.c shim.c \
-o handler.js
This produces handler.js and handler.wasm.
// worker.js
import HandlerModule from './handler.js';
let mod = null;
async function getModule() {
if (mod) return mod;
mod = await HandlerModule();
return mod;
}
export default {
async fetch(request) {
const m = await getModule();
const handle = m.cwrap('tur_handle', 'string', ['string', 'string', 'string']);
const method = request.method;
const path = new URL(request.url).pathname;
const body = await request.text();
const result = handle(method, path, body);
return new Response(result, {
headers: { 'Content-Type': 'text/plain' }
});
}
};
name = "my-turmeric-aot-service"
main = "worker.js"
compatibility_date = "2024-01-01"
[build]
command = "tur emit-c handler.tur > handler.c && emcc -O2 -sENVIRONMENT=worker -sMODULARIZE=1 -sEXPORT_NAME=HandlerModule --no-entry -sEXPORTED_FUNCTIONS='[\"_tur_handle\",\"_malloc\",\"_free\"]' handler.c shim.c -o handler.js"
[[rules]]
type = "CompiledWasm"
globs = ["**/*.wasm"]
Advantages - No interpreter overhead: the Worker runs compiled Turmeric code directly - Smallest WASM payload: only the code your program actually uses is compiled in - Fastest request-time CPU budget: no eval loop, just direct C function calls - True edge distribution at every Cloudflare PoP
Disadvantages
- Requires Emscripten SDK installed in the build environment
- The mangled symbol name from emit-c is not stable across compiler versions --
the shim approach handles this but requires a rebuild when the Turmeric version
changes
- POSIX I/O still not available: same restrictions as Approach B (pure computation
only, no sockets, threads, or filesystem)
- Effects that depend on the Turmeric runtime (algebraic effects, fibers) may not
be available without linking in the full runtime support library
- Build pipeline is more complex (two compilers: tur then emcc)
Containers (A) Interp-WASM (B) AOT WASM (C)
Cold start seconds ms ms
Request CPU budget generous 10-50 ms 10-50 ms
Global edge limited PoPs all PoPs all PoPs
Full stdlib yes no (pure only) no (pure only)
Docker required yes no no
Emscripten required no yes (adapted) yes
Build complexity low medium high
Free tier eligible no yes (if <1 MB) yes (if <1 MB)
Works today yes with adaptation requires tooling
Choose A when you need the full Turmeric stdlib (sockets, threads, effects, filesystem) or want the simplest possible path to production.
Choose B when your service is pure computation and you want global edge distribution without container infrastructure.
Choose C when B's interpreter overhead is too large (either for payload size or CPU budget) and your service is already expressed as a pure function.