Skip to content

Async and runtime

This page explains how vix::async, vix::runtime, and Vix Core work together.

Use it when you want to understand the difference between asynchronous I/O, coroutine scheduling, runtime workers, HTTP sessions, and application execution.

Public header

cpp
#include <vix.hpp>

For lower-level usage, the related headers are:

cpp
#include <vix/async/core/io_context.hpp>
#include <vix/async/core/task.hpp>
#include <vix/async/core/spawn.hpp>
#include <vix/executor/RuntimeExecutor.hpp>
#include <vix/runtime/Runtime.hpp>

The short version

Vix Core uses two execution models:

text
vix::async
  -> network I/O
  -> timers
  -> cancellation
  -> coroutine tasks
  -> TCP accept/read/write

vix::runtime
  -> internal task execution
  -> worker threads
  -> scheduling
  -> run queues
  -> runtime metrics

Core connects both:

text
vix::core
  -> App
  -> HTTPServer
  -> Session
  -> Router
  -> RuntimeExecutor

Why there are two systems

An HTTP server needs to do two different kinds of work.

First, it must wait for I/O.

text
accept connection
read request bytes
write response bytes
wait for timers
handle cancellation

This is the job of vix::async.

Second, it must execute work.

text
run tasks
schedule work
track pending tasks
manage workers
reschedule yielded tasks
collect metrics

This is the job of vix::runtime.

Keeping them separate makes the architecture easier to reason about.

Mental model

Think of the system like this:

text
vix::async keeps the network moving.

vix::runtime executes scheduled work.

vix::core connects both into an HTTP application model.

Core request flow

A simplified HTTP request flow looks like this:

text
client
  -> TCP connection
  -> HTTPServer
  -> vix::async accept
  -> Session
  -> vix::async read
  -> Request parser
  -> Router
  -> RequestHandler
  -> user handler
  -> Response
  -> vix::async write
  -> client

The runtime executor is part of the Core execution model:

text
App
  -> RuntimeExecutor
  -> vix::runtime::Runtime
  -> Scheduler
  -> Workers

vix::async role

vix::async is used for asynchronous I/O.

In Core, it handles:

  • TCP listener creation
  • async listen
  • async accept
  • async read
  • async write
  • session coroutines
  • timers
  • cancellation tokens
  • detached async tasks
  • I/O context execution

The HTTP server owns an async context.

text
HTTPServer
  -> io_context

The I/O threads run that context.

text
io_thread_0 -> io_context.run()
io_thread_1 -> io_context.run()
io_thread_N -> io_context.run()

vix::runtime role

vix::runtime is the internal task execution engine.

It handles:

  • runtime startup
  • runtime shutdown
  • task submission
  • task scheduling
  • worker threads
  • local run queues
  • work stealing
  • task yielding
  • metrics

The runtime model is exposed to Core through RuntimeExecutor.

text
RuntimeExecutor
  -> Runtime
  -> Scheduler
  -> Worker
  -> RunQueue
  -> Task

RuntimeExecutor role

RuntimeExecutor is the bridge between Core and vix::runtime.

It turns higher-level callable work into runtime tasks.

cpp
executor.post([]()
{
  // work
});

Internally:

text
callable
  -> RuntimeExecutor
  -> runtime task
  -> Runtime::submit
  -> Scheduler
  -> Worker

HTTPServer and async

The HTTP server uses vix::async directly.

text
HTTPServer::run
  -> start I/O threads
  -> spawn start_server
  -> start accept loop

The accept loop is asynchronous.

text
accept_loop
  -> co_await listener.async_accept
  -> spawn handle_client

Each accepted client is handled as an async task.

text
handle_client
  -> create Session or TlsSession
  -> co_await session.run()

Session and async

A session is asynchronous.

It uses vix::async::core::task<void>.

text
Session::run
  -> co_await read_request
  -> co_await dispatch_request
  -> co_await send_response
  -> co_await close_stream_gracefully

The session awaits transport reads and writes.

text
co_await transport.async_read(...)
co_await transport.async_write(...)

Transport and async

Transports also use vix::async.

The transport interface returns async tasks.

cpp
task<std::size_t> async_read(
    std::span<std::byte> buffer,
    cancel_token token);

task<std::size_t> async_write(
    std::span<const std::byte> buffer,
    cancel_token token);

This lets the session read and write without blocking the whole server.

Timers and cancellation

Core uses async timers and cancellation for session timeouts.

Conceptually:

text
start timer
  -> wait for configured timeout
  -> cancel token
  -> close transport

The same cancellation token can be passed to transport operations.

text
transport.async_read(buffer, token)
transport.async_write(buffer, token)

Runtime worker model

vix::runtime runs work on workers.

text
Runtime
  -> Scheduler
    -> Worker 0
    -> Worker 1
    -> Worker N

Each worker owns a local queue.

text
Worker
  -> RunQueue
  -> Task

When a worker has no local work, it can try to steal work from another worker.

text
idle worker
  -> try steal
  -> execute stolen task

Runtime tasks

A runtime task returns a task result.

cpp
vix::runtime::TaskResult::complete
vix::runtime::TaskResult::yield
vix::runtime::TaskResult::failed

Meaning:

ResultMeaning
completeThe task finished.
yieldThe task should be scheduled again later.
failedThe task failed.

Yielding work

A low-level runtime task can yield.

cpp
std::size_t step = 0;

executor.submit([step]() mutable -> vix::runtime::TaskResult
{
  ++step;

  if (step < 10)
  {
    return vix::runtime::TaskResult::yield;
  }

  return vix::runtime::TaskResult::complete;
});

This lets long work be split into smaller steps.

Async task vs runtime task

These are different concepts.

An async task is coroutine-based.

cpp
vix::async::core::task<void>

It is used for operations like:

text
co_await sleep
co_await accept
co_await read
co_await write

A runtime task is a lower-level scheduled unit of work.

cpp
vix::runtime::Task

It returns a TaskResult.

text
complete
yield
failed

Comparison

ConceptUsed for
vix::async::core::task<T>Coroutine-based async operations.
vix::async::core::io_contextRunning async I/O tasks.
vix::async::net::tcp_listenerAccepting TCP connections.
vix::async::net::tcp_streamReading and writing TCP bytes.
vix::runtime::RuntimeRunning internal scheduled tasks.
vix::runtime::TaskLow-level runtime work unit.
vix::executor::RuntimeExecutorAdapter from Core to runtime.
vix::server::HTTPServerNative HTTP server built on async.
vix::session::SessionOne client connection lifecycle.

Thread model

A Core app can involve several thread groups.

text
main thread
  -> constructs App
  -> calls run or listen

server thread
  -> started by App::listen
  -> calls HTTPServer::run

I/O threads
  -> run io_context
  -> process async network work

runtime workers
  -> owned by RuntimeExecutor
  -> execute runtime tasks

Simplified:

text
I/O threads handle network progress.

Runtime workers handle scheduled runtime work.

Why not run everything on one thread pool

Network I/O and scheduled work have different needs.

I/O needs:

text
wait for sockets
resume coroutines
handle timers
react to cancellation

Runtime work needs:

text
execute tasks
balance work
reschedule yielded tasks
track metrics
avoid blocking I/O progress

Separating them avoids mixing two different responsibilities.

HTTP server thread flow

When the app starts:

text
App::listen
  -> starts server thread
  -> HTTPServer::run
  -> starts I/O threads
  -> spawns start_server coroutine
  -> starts accept loop

The accept loop then handles connections.

text
listener.async_accept
  -> handle_client
  -> Session::run

Runtime executor startup

The app creates and starts the executor.

text
App constructor
  -> make RuntimeExecutor
  -> executor.start()
  -> Runtime::start()
  -> Scheduler starts workers

After that, runtime workers are ready to execute submitted work.

Runtime executor shutdown

Shutdown is designed to stop accepting work first.

text
RuntimeExecutor::stop
  -> accepting = false
  -> Runtime::stop
  -> workers join

For graceful draining:

text
RuntimeExecutor::stop_and_wait
  -> accepting = false
  -> wait_idle
  -> stop

HTTP server shutdown

The HTTP server shutdown flow is:

text
HTTPServer::stop_async
  -> mark stop requested
  -> close listener
  -> stop io_context
  -> wake monitor thread

Then the blocking path joins threads.

text
HTTPServer::stop_blocking
  -> stop_async
  -> join_threads

App shutdown

App::close() coordinates server shutdown.

text
App::close
  -> mark stop requested
  -> run shutdown callback
  -> server.stop_async
  -> server.stop_blocking
  -> join server thread

The executor is stopped safely by its lifecycle.

I/O work example

This kind of work belongs to vix::async.

text
accept new connection
read request bytes
write response bytes
sleep for timeout
cancel slow connection

Example shape:

cpp
co_await listener.async_accept();
co_await stream.async_read(buffer, token);
co_await stream.async_write(buffer, token);
co_await ctx.timers().sleep_for(timeout, token);

Runtime work example

This kind of work belongs to vix::runtime.

text
execute submitted task
reschedule yielded task
track active tasks
count timed-out tasks
stop workers

Example shape:

cpp
executor.post([]()
{
  // scheduled work
});

Direct runtime task:

cpp
executor.submit([]() -> vix::runtime::TaskResult
{
  return vix::runtime::TaskResult::complete;
});

Core handler example

Most user code does not interact directly with the low-level async or runtime pieces.

cpp
#include <vix.hpp>

int main()
{
  vix::App app;

  app.get("/", [](vix::Request &req, vix::Response &res)
  {
    (void)req;

    res.text("Hello from Vix");
  });

  app.run(8080);

  return 0;
}

Internally:

text
HTTPServer uses async.
RuntimeExecutor connects to runtime.
Session dispatches request.
Router runs handler.

Custom executor example

Use a custom executor when you need explicit runtime worker control.

cpp
#include <vix.hpp>

int main()
{
  auto executor = std::make_shared<vix::executor::RuntimeExecutor>(4);

  vix::App app{executor};

  app.get("/", [](vix::Request &req, vix::Response &res)
  {
    (void)req;

    res.text("custom executor");
  });

  app.run(8080);

  return 0;
}

Executor metrics route

You can expose runtime metrics from an app.

cpp
#include <vix.hpp>

int main()
{
  vix::App app;

  app.get("/metrics", [&app](vix::Request &req, vix::Response &res)
  {
    (void)req;

    const auto m = app.executor().metrics();

    res.json({
      {"pending", m.pending},
      {"active", m.active},
      {"timed_out", m.timed_out}
    });
  });

  app.run(8080);

  return 0;
}

Direct runtime task example

Advanced code can submit runtime tasks directly.

cpp
#include <vix.hpp>

int main()
{
  vix::App app;

  app.executor().submit([]() -> vix::runtime::TaskResult
  {
    vix::print("runtime task");

    return vix::runtime::TaskResult::complete;
  });

  app.run(8080);

  return 0;
}

This is advanced usage.

Most routes should use normal handlers.

Fast HTTP task path

RuntimeExecutor provides a fast path for short HTTP work.

cpp
app.executor().post_http_fast([]()
{
  // short hot-path work
});

This path avoids extra bookkeeping used by generic post(...).

Use it only when you know the task is short and does not need generic executor accounting.

What should go where

Use vix::async for:

  • waiting for sockets
  • async reads
  • async writes
  • timers
  • cancellation
  • coroutine-based operations

Use vix::runtime for:

  • scheduled internal work
  • worker-based execution
  • task queues
  • task yielding
  • runtime metrics

Use vix::core for:

  • routes
  • handlers
  • middleware
  • request parsing
  • response writing
  • app lifecycle
  • HTTP server behavior

Avoid blocking I/O threads

Long blocking work should not run on I/O threads.

Bad pattern:

cpp
app.get("/slow", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  std::this_thread::sleep_for(std::chrono::seconds(5));

  res.text("done");
});

This can reduce server responsiveness.

Prefer moving expensive work into a runtime-managed path or making the work asynchronous when possible.

Short handlers are best

Keep handlers short and clear.

cpp
app.get("/status", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  res.json({{"status", "ok"}});
});

Use separate services for heavier logic.

cpp
app.get("/reports", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  // Call a service that handles the expensive work.
  res.json({{"report", "ready"}});
});

Heavy routes

Core can mark routes as heavy.

cpp
app.get_heavy("/reports", [](vix::Request &req, vix::Response &res)
{
  (void)req;

  res.json({{"report", "ready"}});
});

Heavy route metadata lets the runtime and router model identify expensive routes.

Async I/O lifecycle

Simplified I/O lifecycle:

text
create io_context
  -> start I/O threads
  -> bind listener
  -> start accept loop
  -> accept connections
  -> run sessions
  -> stop io_context
  -> join I/O threads

Runtime lifecycle

Simplified runtime lifecycle:

text
create RuntimeExecutor
  -> create Runtime
  -> start workers
  -> accept tasks
  -> execute tasks
  -> stop accepting
  -> stop runtime
  -> join workers

Combined lifecycle

The combined lifecycle in App is:

text
construct App
  -> create RuntimeExecutor
  -> start runtime workers
  -> create HTTPServer
  -> create async io_context

listen/run
  -> start server thread
  -> start I/O threads
  -> accept connections

request
  -> async read
  -> router handler
  -> async write

shutdown
  -> stop server
  -> stop I/O context
  -> stop runtime executor

Architecture diagram

text
                 user code
                    |
                    v
                  App
                    |
        +-----------+------------+
        |                        |
        v                        v
     Router              RuntimeExecutor
        |                        |
        v                        v
 RequestHandler              Runtime
        |                        |
        v                        v
     Session               Scheduler
        |                        |
        v                        v
    Transport                 Workers
        |
        v
   vix::async I/O

Full request architecture

text
client
  -> TCP socket
  -> vix::async::net::tcp_listener
  -> HTTPServer::accept_loop
  -> HTTPServer::handle_client
  -> Session or TlsSession
  -> Transport
  -> Session::read_request
  -> vix::http::Request
  -> Router::handle_request
  -> RequestHandler
  -> user handler
  -> vix::http::Response
  -> Session::send_response
  -> Transport::async_write
  -> client

Complete example

cpp
#include <vix.hpp>

int main()
{
  auto executor = std::make_shared<vix::executor::RuntimeExecutor>(4);

  vix::App app{executor};

  app.get("/", [](vix::Request &req, vix::Response &res)
  {
    (void)req;

    res.text("Hello from Vix");
  });

  app.get("/metrics", [&app](vix::Request &req, vix::Response &res)
  {
    (void)req;

    const auto metrics = app.executor().metrics();

    res.json({
      {"pending", metrics.pending},
      {"active", metrics.active},
      {"timed_out", metrics.timed_out}
    });
  });

  app.get_heavy("/report", [](vix::Request &req, vix::Response &res)
  {
    (void)req;

    res.json({
      {"report", "ready"}
    });
  });

  app.run(8080);

  return 0;
}

API summary

APIPurpose
vix::async::core::io_contextRuns async I/O tasks.
vix::async::core::task<T>Coroutine-based async result.
vix::async::core::spawn_detachedStart a detached async task.
vix::async::net::tcp_listenerAccept TCP connections.
vix::async::net::tcp_streamRead and write TCP bytes.
vix::executor::RuntimeExecutorBridge Core to vix::runtime.
RuntimeExecutor::postSubmit normal callable work.
RuntimeExecutor::submitSubmit low-level runtime work.
RuntimeExecutor::post_http_fastSubmit short HTTP fast-path work.
RuntimeExecutor::metricsRead runtime executor metrics.
vix::runtime::RuntimeInternal runtime engine.
vix::runtime::TaskRuntime task unit.
vix::runtime::TaskResultRuntime task result.

Best practices

Use vix::App for normal application code.

cpp
vix::App app;

Let Core manage vix::async and vix::runtime.

cpp
app.run(8080);

Keep handlers short.

cpp
app.get("/status", handler);

Use route metadata for expensive routes.

cpp
app.get_heavy("/reports", handler);

Use the app executor only when you need advanced runtime control.

cpp
auto &executor = app.executor();

Do not block I/O paths with long synchronous work.

text
Keep I/O responsive.
Move expensive work to appropriate runtime-managed code.

Next steps

Read the next pages:

Released under the MIT License.