# From Lua to V8

*2026-02-06 — Tech — By Kim*

> Exploring the transition from Lua to V8 for game scripting, the challenges of embedding JavaScript engines, and how building isolated execution contexts accidentally created a streaming-first serverless platform.
If you've done any game development you've probably touched Lua at some point. It's everywhere — Love2D, Defold, Roblox, modding scene, embedded scripting in countless engines.
It's light, it's fast, it embeds beautifully.

On the other side of the engineering spectrum, outside of the creative world of games, we have... IT and software land.
I spend a lot of time there. Mostly as a [consultant](https://linkedin.com/in/kim-aarnseth) these days.

And on the software-side we're not seeing that much Lua. We have the language of the web, our friend — JavaScript!

So why not JavaScript instead of Lua for game scripting?
There are a lot of web developers and [bun.sh](https://bun.sh/) is great. And there are like 1000000 npm packages — what could go wrong?

### What about V8?

So I started looking at V8. Google's JavaScript engine. The thing that powers all the things — Chrome, Node, Deno, Cloudflare Workers.
The obvious concern is size and overhead.

**Lua is tiny** — there are even a couple of nice header-only libraries.

**V8 is not** — it's like a gigantic pile of code that you have to compile and link into your application. Or spend hours building from source.

But what you get is a powerful JavaScript engine that can be embedded in your application.

```c
// some javascript to run
const char* src = "let hp = 100; hp -= 25; hp;";

// create an isolated V8 instance (think: one sandbox)
v8::Isolate* isolate = v8::Isolate::New(params);

// create an execution context (global scope for the script)
v8::Local<v8::Context> context = v8::Context::New(isolate);

// compile and run
v8::Local<v8::String> source = v8::String::NewFromUtf8(isolate, src).ToLocalChecked();
v8::Local<v8::Script> script = v8::Script::Compile(context, source).ToLocalChecked();
v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();

// read the result as a number
double hp = result->NumberValue(context).FromJust();
printf("HP: %.0f\n", hp); // "HP: 75"

// useful? idk maybe
// can just write this in c directly and not include a gigantic v8 library
int hp = 100;
hp -= 25;
printf("HP: %d\n", hp); // "HP: 75"

// but think of the adventure..
```

Do you really need sandbox isolation in a game engine that runs on V8? Maybe. It's pretty nice.
Is it faster than Lua? Maybe, go try it yourself.
You do get all of the Google-powered <Abbr explanation="Just-In-Time compilation. No compile just go dude.">JIT</Abbr> compilation and JS magic sauce that's been cooking for years.

Wish it wasn't so painful to embed and live with.

### The Deno connection

How do other people live with V8?

Enter Deno. Because our buddies at [Anthropic](https://bun.com/blog/bun-joins-anthropic) are [zigging it up with JavaScriptCore](https://bun.com/blog/how-bun-supports-v8-apis-without-using-v8-part-1).

Deno's `deno_core` and `rusty_v8` crates give you a Rust-native way to bootstrap V8, create isolated contexts, and inject your own functions. You define ops in Rust, expose them to JavaScript, and suddenly you have a scripting layer where the hot path runs in Rust but the logic is written in JS.

Deno acknowledges the pain and keeps their [V8 bindings alive](https://github.com/denoland/rusty_v8).


```rust
use anyhow::Result;
use function_runner::create_js_runtime;

#[tokio::main]
async fn main() -> Result<()> {
    // create a V8 runtime (isolated sandbox)
    let mut runtime = create_js_runtime();

    // some javascript to run
    let code = r#"
        const res = await fetch("https://httpbin.org/json");
        const data = await res.json();
        console.log(data);
    "#;

    // load and run it
    let module_id = runtime
        .load_main_es_module_from_code(&"file:///main.js".parse()?, code)
        .await?;
    runtime.mod_evaluate(module_id).await?;
    runtime.run_event_loop(Default::default()).await?;

    Ok(())
}
```

Maybe Deno + [Bevy](https://bevy.org/) could be a thing.

I started prototyping this for a game use. Script sandboxes, hot-reloading, exposing game state to JS contexts. It worked well. Really well.


### Pain the the middle

So js running all fine, but the real pain is production grade javascript and typescript.
Deno unlocks typescript support. It is with `nodejs_compat` and other js runtimes like bun and deno is where the good javascript support is.

V8 out of the box can't reallt handle a ```console.log``` or a ```setTimeout``` this is js function you have to bridge yourself in your native layer.

Ideally you just want all that for free - deno_core gives you a part of that but not all. Look deep into the horros of [supabase](https://github.com/supabase/supabase) to find out.


### Wait — this is a serverless runtime

What I had built was essentially isolated JavaScript execution contexts sharing a single V8 instance, with Rust handling the heavy lifting underneath. Each script sandbox was independent, had its own scope, couldn't interfere with others.

**That's a serverless function.**

If I pair each sandbox to an <Abbr explanation="HTTP - Hypertext Transfer Protocol, internetz based.">HTTP</Abbr> endpoint...
I thought for a while and felt like the key differentiator to other FaaS is really good streaming support.
So I worked on that for a bit. The main challenge was managing the streaming pool of <Abbr explanation="TCP - Transmission Control Protocol, sendy things data.">TCP</Abbr> connections and working with async/await in Rust.

So now I had a <Abbr explanation="Function as a Service, no server over here (it is elsewhere).">FaaS</Abbr> platform. One where <Abbr explanation="Server-Sent Events, sendy data.">SSE</Abbr> streaming isn't bolted on as an afterthought but is the default mode of operation.

Yielding a server-sent event stream was easy:

```js
export default async function streaming(request: Request) {
    const encoder = new TextEncoder();

    async function* sseGenerator() {
        const tokens = [
            "Hello ",
            "Worlds",
            " Worlds.",
            " Ah, living in the multiverse."
        ];

        for (const token of tokens) {
            yield encoder.encode(token);
            // simulate your wallet getting spent on tokens
            await new Promise((res) => setTimeout(res, 30 + Math.random() * 20));
        }
    }

    return new Response(sseGenerator(), {
        status: 200,
        headers: {
            "Content-Type": "text/event-stream",
            "Cache-Control": "no-cache",
            Connection: "keep-alive",
        },
    });
}
```

So that's how [Tiderun](/news/tiderun-streaming-faas) happened.

## Questions & Answers



*Questions about the article, answered by the developer.*



**1. Be honest — did you actually ship anything with V8 embedded in a game, or did the serverless rabbit hole eat the whole game scripting idea alive?**

Not in a production released gold-standard game yet, but once compiled its quite manageable so it might still be a good idea.

**2. V8's overhead versus Lua is massive — at what point during prototyping did you stop pretending that trade-off was acceptable for a game engine?**

For a compiled game-engine I think it could still be a good idea. But the best thing is to not use any of them. 

**3. You mention Deno + Bevy could be a thing — why isn't it already, what's actually blocking that from working today?**

Well go ahead and create that plugin for me.

**4. What's the 'look deep into the horrors of Supabase' story — what did you find in there that made you realize bridging JS properly is way harder than it looks?**

The constant node_compact glue to get modern Javascript features to work.  Its a loooot of duct-tape.

**5. Why is streaming the killer feature for Tiderun specifically — isn't every major cloud provider already decent at SSE?**

SSE itself is a great standard popularized by the LLM chats, but the killer feature is first class support for full streaming inside a FaaS architecture, tranditinal FaaS is you call the thing it respond, the end. Now you have longer running FaaS that will compute on the way and generate. 

**6. You're running a game studio that now also has a serverless platform — does Tiderun actually power anything in Lunar Soil or your other games, or are these two completely separate lives?**

They are not melded together, The focus with Lunar Soil is to make the game. It does have a small part of player messages that are using a FaaS architecture but again I don't have any hardware or rent hardware right now to deploy Tiderun on for a production setting where I could use it in Lunar Soil. Maybe that change later post launch of the game. 


---

*Canonical URL: https://morgondag.io/news/from-lua-to-v8*