VLearnVibium

Sync vs Async in Vibium: How It Works Under the Hood

Sync vs async in Vibium under the hood: both clients call the same Go binary over a local WebSocket, so blocking and awaiting share one automation engine.

By Pramod Dutta··4 min read·Verified with Vibium 26.2
▶ Animated overview · made with Remotion

Under the hood, Vibium's sync and async APIs are two thin wrappers around one engine: both send identical JSON commands to the Go binary over a local WebSocket, and the binary does all the real work. The sync client blocks until the binary replies; the async client returns an awaitable that resolves on the same reply. Crucially, the heavy lifting — element finding, actionability checks, and the auto-wait polling loop — happens server-side in the Go binary, not in your Python or JavaScript. That's why a synchronous vibe.find("#save").click() and an asynchronous await page.find("#save").click() behave identically: same checks, same default 30-second timeout, same timing. The difference is purely how your code reads and composes, never what the automation can do. If you just want to pick one for a script, read Vibium sync vs async API; this page is about the machinery beneath that choice.

How can two APIs share one engine?

They share an engine because neither client contains the automation logic — it lives in the single Go binary. Both the sync and async clients translate your calls into the same JSON-over-WebSocket commands and ship them to the binary, which talks to the browser via WebDriver BiDi.

sync client  ─┐
              ├──► same JSON over local WebSocket ──► Go binary ──► browser (BiDi)
async client ─┘

The only real difference is what each client does while it waits for the reply. Sync parks the current thread until the response arrives. Async hands control back to the event loop and resolves a coroutine or promise when it does. The bytes on the wire are the same.

What does the sync path do while it waits?

The sync client sends a command and blocks the calling thread until the binary answers. Your code reads top to bottom with no await:

from vibium import browser_sync as browser
 
vibe = browser.launch()
vibe.go("https://example.com")
 
# This line blocks until the Go binary reports the click is done
vibe.find("#save").click()
 
vibe.quit()

While that click() line is "stuck," the Go binary is busy: it's running the actionability polling loop, waiting for #save to become visible, stable, enabled, and able to receive the click. The Python thread is idle, waiting for one WebSocket reply.

What does the async path do differently?

The async client sends the identical command but returns an awaitable, so the event loop can run other work while the browser does its thing. You call it from inside an async function:

import asyncio
from vibium.async_api import browser
 
async def main():
    bro = await browser.launch()
    page = await bro.page()
    await page.go("https://example.com")
    await page.find("#save").click()   # same Go-side checks as sync
    await bro.close()
 
asyncio.run(main())

The await page.find("#save").click() triggers the exact same server-side actionability loop as the sync version. The difference is that your event loop isn't blocked — it can service other coroutines while the WebSocket reply is pending.

Why doesn't async make a single script faster?

Async doesn't speed up a linear script because each browser command still has to wait for the browser. If your script is just "go here, click this, read that" in order, async simply rephrases the same sequential waits with await in front of them — the browser is the bottleneck, not the client. Async pays off when you have other awaitable work to overlap, such as calling an API, querying a database, or coordinating with an async web framework, while a browser command is in flight.

AspectSyncAsync
Calling styleBlocking, top-to-bottomawait inside an event loop
Where work happensGo binaryGo binary (identical)
Auto-waiting / timeoutServer-side, 30s defaultServer-side, 30s default
Best forScripts, learning, linear flowsExisting async codebases, concurrent work

Why put the logic in the binary instead of the client?

Vibium keeps actionability and waiting in the Go binary so every client behaves identically and stays trivially thin. The logic is written once, not re-implemented in Python, JavaScript, and any future client, which rules out subtle per-language timing differences. Polling also happens close to the browser over a local WebSocket rather than across extra client round-trips, so it's fast. The client's job shrinks to: send a command, wait for success or a timeout error. That design is exactly why sync and async can be different costumes over the same body — and why you can choose between them purely for code style. See the find command for the client-facing surface of that engine.

Next steps

Frequently asked questions

How does Vibium implement both a sync and an async API?

Both clients are thin wrappers that send the same JSON commands to Vibium's Go binary over a local WebSocket. The sync client blocks until the binary replies; the async client returns an awaitable that resolves on the same reply. The binary, not the client, does the real work.

Does Vibium's auto-waiting run in Python or in the Go binary?

Auto-waiting and actionability checks run server-side in the Go binary, not in the client. That is why a sync click() and an async await click() wait identically — both trigger the same Go-side polling loop, so neither calling style is faster or more reliable than the other.

Is the async API faster than the sync API in Vibium?

For a single linear script, no. Each command still waits for the browser. Async helps when you need to run other awaitable work concurrently or integrate with an event loop, not because the browser commands themselves complete any faster than in the sync API.

Vibium is created by Jason Huggins. This is an independent tutorial — see the official Vibium site and GitHub repo for canonical docs.

Related guides