A small language for work that is not.

A gen1-to-gen2 integrator migration needed a backfill of 1.3 billion transaction records. The first version, in Python, was a nightmare to get through a run and was abandoned. The rewrite in Ecko went through the whole set without a single failure.

curl -fsSL ecko.sh/install | sh

Errors are values

import std.json

rows = [r"""{"id": 1, "cents": 1250}""", r"""{"id": 2, "cents": """, r"""{"id": 3, "cents": 990}"""]

mut total = 0
mut rejected = []
for line in rows {
    try {
        row = json.decode(line)
        total = total + row.cents  # checked: an overflow is an error, never a wrap
    } catch (e) {
        # A malformed row is set aside; anything else stops the job, loudly.
        match get(e, "kind") {
            "parse" => {
                rejected = push(rejected, line)
            }
            _ => error(e)
        }
    }
}
print("{total} cents, {len(rejected)} rejected")

                

Try other examples in the playground

Why Ecko?

The case comes from production, not a benchmark. What follows are the properties that made it possible.

A gen1-to-gen2 integrator migration needed a backfill of 1.3 billion transaction records. The first version, written in Python, was a nightmare to get through a run and was abandoned. The rewrite in Ecko went through the whole set without a single failure. Read the full case →

No warm-up tax

Cold start · hello world to first byte out
  • ecko

    2.1ms

  • python

    8.4ms

  • node

    17.1ms

  • ruby

    44.5ms

Median of 100 runs after 10 warmups. Ecko v0.40.0, python 3.12.3, node 24.4.0, ruby 3.2.3, on an AMD Ryzen 9 5950X under Linux.

Against AOT-compiled languages the gap is smaller. But they ask for a build step and a linker before you've written a line. Ecko skips both.

A bytecode compiler and a stack VM, with no tier that has to get hot. The thousandth run costs what the first one did. CLIs, serverless handlers and dev reloads are up before your finger leaves the key.

  • No garbage-collection pauses

    Memory is freed when the last reference goes away. Nothing stops the process to sweep, so a request at the tail isn't waiting on a collector pass.

    fn handle(body) {
        n = len(body)
        n
    }
    # body drops at return - no GC pause later
    

    reference counted · freed at scope exit

  • Concurrency without the ceremony

    Parallel work is something you write, not something you configure. Each worker gets its own snapshot, so a batch job can fan out across a dataset with no shared state to get wrong.

    amounts = [1250, 900, 3400]
    fees = pmap(amounts, fn(a) a * 3 / 100)
    

    channel() · async fn · await · pmap()

  • Checked before it runs

    A generated program is checked before its first side effect. An undefined name or a wrong argument count stops the run instead of letting it half-finish. ai[T] validates the reply against the type you declared, and with no API key it answers from a deterministic offline mock.

    type Lead = Hot { company: String, seats: Int }
        | Demo
    
    lead = ai[Lead] "Acme wants 40 seats"
    

    ai[T] · checked against your types · mock mode offline

The language server ships with it.

A language server ships inside the binary: ecko lsp. Diagnostics, completion, hover and go-to-definition in any editor that speaks LSP. First-party extensions for VS Code, Cursor and JetBrains; everything else points at the same server.

Setting up IDEs that support LSPcommand ecko · argument lsp · files *.ecko
// Zed - settings.json
{
  "lsp": {
    "ecko": { "binary": { "path": "ecko", "arguments": ["lsp"] } }
  },
  "languages": {
    "Ecko": { "language_servers": ["ecko"] }
  }
}

// JetBrains, without the plugin: LSP4IJ, command ecko, argument lsp

Keep ecko on your PATH - or give the absolute path. Highlighting works without it; smart editing waits for the server. Full editor setup →

Install it in one command.

The same guarantees apply whether you're moving a billion rows or serving the next request. One binary, four platforms, nothing else to install. If it turns out not to be for you, it is one file to delete.

curl -fsSL ecko.sh/install | sh

Homebrew: brew install ecko-lang/tap/ecko · binaries and checksums →