Deploying Turmeric Services to Cloudflare

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.


Prerequisites

Tool How to get it
Turmeric (tur) built from source tur run 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

The example service

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.


Approach A: Cloudflare Containers

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.

Step 1 -- write the Turmeric service

;; 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.

(load "stdlib/async_socket.tur")
(load "stdlib/scheduler.tur")
(load "stdlib/fiber.tur")
(load "stdlib/env.tur")

(defn cstr-length [s : cstr] : int
  ```c
  return (int64_t)strlen(s);
  ```)

(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 : Fd] : 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 : Fd 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 (env/get-raw "PORT")
        port     (if (= port-str 0) 8080 (cstr->parse-int (:: port-str :cstr)))
        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)

Owned-return note. http-ok assembles a fresh response with nested str-concat and returns it as cstr; handle-client sends it and drops it immediately, so the buffer is a "caller frees" result that leaks each request. For a handler that builds and holds response strings, an owned String (adopt the str-concat buffer, or accumulate with a StringBuilder) frees exactly once and borrows to cstr for async-socket-send. See strings-guide.md.

Test locally before containerizing:

tur run build
tur run service.tur
# in another terminal:
curl http://localhost:8080/
# Hello from Turmeric!

Step 2 -- write the Dockerfile

# 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/*

# Build tur itself. There is no tur in the image yet, so this is plain CMake
# rather than `tur run`.
WORKDIR /turmeric
COPY . .
RUN cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
    && cmake --build build -j

# Compile the service to a standalone binary
RUN build/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 and build tur, as in the builder stage above ...
COPY --from=builder /turmeric/build/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.

Step 3 -- write the Cloudflare Worker

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

Step 4 -- deploy

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.

Tradeoffs

Advantages

Disadvantages


Approach B: Interpreter-in-WASM 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 -- without long-running loops, raw sockets, or filesystem access.

Step 1 -- build the WASM module

From the Turmeric repository root:

tur run wasm

This produces:

build-wasm/wasm/turmeric.js
build-wasm/wasm/turmeric.wasm

Step 2 -- adapt the Emscripten output for Workers

Emscripten's default output targets browser environments. Cloudflare Workers run in a V8 isolate with no window, document, or XMLHttpRequest. The stock tur run wasm build already passes -sMODULARIZE=1 and -sEXPORT_NAME=TurmericModule; what it does not set is the target environment:

# In src/CMakeLists.txt (the tur_wasm target) or a custom build script,
# add to the emcc link command:
#   -sENVIRONMENT=worker

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.

Step 3 -- write the service handler in Turmeric

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.

Owned-return note. handle-request returns cstr because turi_wasm_eval marshals the result back to the JS host as a C string -- that host boundary is a legitimate keep-cstr seam. If the handler grows internal string-building steps beyond this one str-concat, build those as owned String and convert to cstr only at the return. See strings-guide.md.

Step 4 -- write the Worker

// 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' }
    });
  }
};

Step 5 -- wrangler.toml

name = "my-turmeric-wasm-service"
main = "worker.js"
compatibility_date = "2024-01-01"

[build]
command = "tur run wasm && cp build-wasm/wasm/turmeric.js . && cp build-wasm/wasm/turmeric.wasm ."

[[rules]]
type = "CompiledWasm"
globs = ["**/*.wasm"]

[[rules]]
type = "ESModule"
globs = ["**/*.js"]

Tradeoffs

Advantages

Disadvantages


Approach C: AOT WASM via emit-c

How 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.

Step 1 -- write a pure handler

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)))

cstr at the seam, String inside. tur_handle's signature is the WASM/host C ABI (char *tur_handle(...)), so its return type genuinely must be cstr -- that boundary is a legitimate keep-cstr site. The internal builders are the owned-String cases: greet assembles a fresh "Hello, name!" with str-concat (a "caller frees" buffer that leaks here). Build such internal results as String and lower to cstr only at the exported seam ((string/to-cstr (greet ...)), or string/adopt-cstr the str-concat buffer). See strings-guide.md.

Step 2 -- compile to C

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.

Step 3 -- write a C shim and compile with Emscripten

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.

Step 4 -- write the Worker

// 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' }
    });
  }
};

Step 5 -- wrangler.toml

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"]

Tradeoffs

Advantages

Disadvantages


Summary

                     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.