Vix.cpp v2.8.5 is here Read the blog
Skip to content

Async App ​

This example shows how to use Vix Async inside a Vix application.

Use this when you want an HTTP app that also runs background async work.

The goal is to keep the model clear:

txt
vix::App
  handles HTTP requests

vix::async::core::io_context
  runs async background work

shared application state
  connects background work to HTTP routes

cancellation
  stops the worker cleanly

This is different from a standalone background worker.

background-task.md shows an async worker by itself.

This page shows async work running next to an HTTP application.

What this example builds ​

The app exposes:

txt
GET /api/health
GET /api/status
POST /api/refresh

At the same time, an async worker runs in the background every second.

The worker updates shared state:

txt
tick count
last job result
running flag

The HTTP routes can read that state.

Project structure ​

Create:

txt
async_app_demo/
└── async_app.cpp

Create the file:

bash
mkdir async_app_demo
cd async_app_demo
touch async_app.cpp

Source ​

Open:

txt
async_app.cpp

Add:

cpp
#include <atomic>
#include <chrono>
#include <csignal>
#include <exception>
#include <memory>
#include <mutex>
#include <string>
#include <thread>

#include <vix.hpp>
#include <vix/async.hpp>
#include <vix/middleware.hpp>
#include <vix/print.hpp>

using namespace std::chrono_literals;

struct AppState
{
  std::mutex mutex{};
  int tick_count{0};
  int last_result{0};
  bool worker_running{false};
};

static int compute_background_result(int tick)
{
  return tick * 10;
}

static void set_worker_running(
  const std::shared_ptr<AppState> &state,
  bool running)
{
  std::lock_guard<std::mutex> lock(state->mutex);
  state->worker_running = running;
}

static void update_background_state(
  const std::shared_ptr<AppState> &state,
  int tick,
  int result)
{
  std::lock_guard<std::mutex> lock(state->mutex);

  state->tick_count = tick;
  state->last_result = result;
  state->worker_running = true;
}

static vix::async::core::task<void> background_refresh_worker(
  vix::async::core::io_context &ctx,
  std::shared_ptr<AppState> state,
  vix::async::core::cancel_token token)
{
  int tick = 0;

  set_worker_running(state, true);

  while (!token.is_cancelled())
  {
    ++tick;

    try
    {
      const int result = co_await ctx.cpu_pool().submit([tick]()
      {
        return compute_background_result(tick);
      }, token);

      update_background_state(state, tick, result);

      vix::print("background refresh tick =", tick);
    }
    catch (const std::exception &ex)
    {
      if (token.is_cancelled())
        break;

      vix::eprint("background refresh error:", ex.what());
    }

    try
    {
      co_await ctx.timers().sleep_for(1s, token);
    }
    catch (const std::exception &)
    {
      if (token.is_cancelled())
        break;
    }
  }

  set_worker_running(state, false);

  vix::print("background refresh worker stopped");

  co_return;
}

static vix::async::core::task<void> stop_async_on_signal(
  vix::async::core::io_context &ctx,
  vix::async::core::cancel_source &source)
{
  ctx.signals().add(SIGINT);
  ctx.signals().add(SIGTERM);

  const int signal = co_await ctx.signals().async_wait();

  vix::print("shutdown signal received =", signal);

  source.request_cancel();

  co_await ctx.timers().sleep_for(100ms);

  ctx.stop();

  co_return;
}

static void install_middleware(vix::App &app)
{
  app.use("/api", vix::middleware::app::recovery_dev());
  app.use("/api", vix::middleware::app::request_id_dev());
  app.use("/api", vix::middleware::app::timing_dev());
  app.use("/api", vix::middleware::app::security_headers_dev());
  app.use("/api", vix::middleware::app::rate_limit_dev());
}

static void register_routes(
  vix::App &app,
  const std::shared_ptr<AppState> &state,
  vix::async::core::io_context &async_ctx)
{
  app.get("/", [](vix::Request &, vix::Response &res)
  {
    res.text("Async App example. Try /api/status.");
  });

  app.get("/api/health", [](vix::Request &, vix::Response &res)
  {
    res.json({
      "ok", true,
      "service", "async-app"
    });
  });

  app.get("/api/status", [state](vix::Request &, vix::Response &res)
  {
    int tick_count = 0;
    int last_result = 0;
    bool worker_running = false;

    {
      std::lock_guard<std::mutex> lock(state->mutex);

      tick_count = state->tick_count;
      last_result = state->last_result;
      worker_running = state->worker_running;
    }

    res.json({
      "ok", true,
      "worker_running", worker_running,
      "tick_count", tick_count,
      "last_result", last_result
    });
  });

  app.post("/api/refresh", [state, &async_ctx](vix::Request &, vix::Response &res)
  {
    async_ctx.post([state]()
    {
      std::lock_guard<std::mutex> lock(state->mutex);

      state->tick_count += 1;
      state->last_result = state->tick_count * 100;
      state->worker_running = true;
    });

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

static void run_async_runtime(
  vix::async::core::io_context &ctx)
{
  ctx.run();
  ctx.shutdown();
}

int main()
{
  auto state = std::make_shared<AppState>();

  vix::async::core::io_context async_ctx;
  vix::async::core::cancel_source cancel_source;

  auto worker =
    background_refresh_worker(
      async_ctx,
      state,
      cancel_source.token()
    );

  auto signal =
    stop_async_on_signal(
      async_ctx,
      cancel_source
    );

  std::move(worker).start(async_ctx.get_scheduler());
  std::move(signal).start(async_ctx.get_scheduler());

  std::thread async_thread([&async_ctx]()
  {
    run_async_runtime(async_ctx);
  });

  vix::App app;

  install_middleware(app);
  register_routes(app, state, async_ctx);

  app.run(8080);

  cancel_source.request_cancel();
  async_ctx.stop();

  if (async_thread.joinable())
    async_thread.join();

  return 0;
}

Run it ​

Run:

bash
vix run async_app.cpp

The HTTP server listens on:

txt
http://127.0.0.1:8080

The async worker starts in a separate runtime thread.

Test health ​

bash
curl -i http://127.0.0.1:8080/api/health

Expected body:

json
{
  "ok": true,
  "service": "async-app"
}

Test background status ​

bash
curl -i http://127.0.0.1:8080/api/status

Expected body shape:

json
{
  "ok": true,
  "worker_running": true,
  "tick_count": 3,
  "last_result": 30
}

Call it again after a few seconds:

bash
curl -i http://127.0.0.1:8080/api/status

You should see tick_count and last_result changing.

Trigger one background update from HTTP ​

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/refresh

Expected status:

txt
202 Accepted

Expected body:

json
{
  "ok": true,
  "queued": true
}

This route does not run the work directly inside the HTTP handler.

It schedules work into the async runtime:

cpp
async_ctx.post([state]()
{
  std::lock_guard<std::mutex> lock(state->mutex);

  state->tick_count += 1;
  state->last_result = state->tick_count * 100;
  state->worker_running = true;
});

That keeps the handler small.

Why use a separate async runtime thread ​

vix::App::run(...) blocks while the HTTP server is running.

io_context::run() also blocks while async work is running.

So this example runs the async context in another thread:

cpp
std::thread async_thread([&async_ctx]()
{
  run_async_runtime(async_ctx);
});

The HTTP app runs on the main thread:

cpp
app.run(8080);

The model is:

txt
main thread
  HTTP server

async thread
  background worker
  timers
  CPU pool tasks
  signal watcher

This keeps the example simple and explicit.

Shared state ​

The HTTP routes and async worker share:

cpp
std::shared_ptr<AppState>

The state is protected by a mutex:

cpp
struct AppState
{
  std::mutex mutex{};
  int tick_count{0};
  int last_result{0};
  bool worker_running{false};
};

The worker writes the state:

cpp
update_background_state(state, tick, result);

The HTTP route reads the state:

cpp
std::lock_guard<std::mutex> lock(state->mutex);

tick_count = state->tick_count;
last_result = state->last_result;
worker_running = state->worker_running;

Use this pattern for small shared status.

For larger systems, prefer a service class.

Better structure for bigger apps ​

For bigger applications, do not keep everything in one file.

Use a service:

txt
AsyncRuntime
  owns io_context
  owns cancel_source
  owns thread
  starts workers
  stops workers

AppState
  owns shared state

RouteRegistry
  registers HTTP routes

AppBootstrap
  wires everything together

The single-file example is useful for learning.

A production app should move this logic into classes.

Why the worker uses timers ​

The worker waits with:

cpp
co_await ctx.timers().sleep_for(1s, token);

This is better than:

cpp
std::this_thread::sleep_for(1s);

because the coroutine sleep does not block the scheduler thread.

The async runtime can still process posted callbacks and other async work.

Why the worker uses the CPU pool ​

The worker offloads work to:

cpp
ctx.cpu_pool().submit(...)

Example:

cpp
const int result = co_await ctx.cpu_pool().submit([tick]()
{
  return compute_background_result(tick);
}, token);

Use the CPU pool when the work may be:

txt
CPU-heavy
blocking
synchronous
slow enough to affect the scheduler

For tiny work, direct computation is fine.

For real backend work, avoid blocking the scheduler thread.

Cancellation ​

The app creates:

cpp
vix::async::core::cancel_source cancel_source;

The worker receives:

cpp
cancel_source.token()

When the app shuts down, cancellation is requested:

cpp
cancel_source.request_cancel();

The worker checks:

cpp
while (!token.is_cancelled())

and cancellable waits use the token:

cpp
co_await ctx.timers().sleep_for(1s, token);

The rule is:

txt
cancel_source requests shutdown
cancel_token lets tasks observe shutdown

Signal handling ​

The async runtime listens for:

cpp
SIGINT
SIGTERM

with:

cpp
ctx.signals().add(SIGINT);
ctx.signals().add(SIGTERM);

Then waits:

cpp
const int signal = co_await ctx.signals().async_wait();

When a signal is received:

cpp
source.request_cancel();

This is useful for terminal shutdown:

txt
Ctrl+C

and process managers:

txt
SIGTERM

Shutdown flow ​

The shutdown flow is:

txt
Ctrl+C
  -> signal task wakes up
  -> cancellation requested
  -> worker observes cancellation
  -> async context stops
  -> async thread joins
  -> process exits

The code also handles the case where app.run(...) returns normally:

cpp
cancel_source.request_cancel();
async_ctx.stop();

if (async_thread.joinable())
  async_thread.join();

This ensures the async thread is not left running.

HTTP handlers should not block ​

Avoid this inside a route:

cpp
app.post("/api/refresh", [](Request &, Response &res)
{
  expensive_work();

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

Prefer scheduling the work:

cpp
app.post("/api/refresh", [&async_ctx, state](Request &, Response &res)
{
  async_ctx.post([state]()
  {
    // background update
  });

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

This makes the route respond quickly.

When to use this pattern ​

Use this pattern when the app needs:

txt
periodic refresh
background cleanup
cache warming
sync loop
metrics collection
queue polling
file watching
maintenance jobs

Examples:

txt
refresh product recommendations every minute
clean expired sessions every 10 minutes
sync local data with remote storage
send queued notifications
calculate metrics snapshots

When not to use this pattern ​

Do not use this pattern for everything.

For a simple HTTP API with no background work, you do not need vix::async.

For one-off immediate work inside a handler, regular C++ code may be enough.

Use async when there is a real reason:

txt
periodic work
delayed work
background coordination
cancellable long-running task
offloaded blocking work
signal-driven shutdown

Common mistakes ​

Blocking the HTTP handler ​

Wrong:

cpp
app.post("/api/refresh", [](Request &, Response &res)
{
  std::this_thread::sleep_for(std::chrono::seconds(5));
  res.json({"ok", true});
});

Correct:

cpp
app.post("/api/refresh", [&async_ctx](Request &, Response &res)
{
  async_ctx.post([]()
  {
    // background work
  });

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

Sharing state without synchronization ​

Wrong:

cpp
state->tick_count += 1;

from multiple threads without protection.

Correct:

cpp
std::lock_guard<std::mutex> lock(state->mutex);
state->tick_count += 1;

Forgetting to stop the async runtime ​

If the async thread is running, stop it before exiting:

cpp
cancel_source.request_cancel();
async_ctx.stop();

if (async_thread.joinable())
  async_thread.join();

Calling long work directly in the scheduler ​

Wrong:

cpp
async_ctx.post([]()
{
  expensive_work();
});

Better for expensive work:

cpp
co_await ctx.cpu_pool().submit([]()
{
  return expensive_work();
});

Installing JSON parser globally ​

If a route does not need JSON, do not force it through the JSON parser.

Good:

cpp
app.use("/api/products", middleware::app::json_strict_dev(4096));

Risky:

cpp
app.use("/api", middleware::app::json_strict_dev(4096));

Keep parsers route-specific.

Production notes ​

For production apps, consider wrapping the async runtime.

A clean class could own:

txt
io_context
cancel_source
thread
worker task startup
shutdown logic

Example shape:

cpp
class AsyncRuntime
{
public:
  void start();
  void stop();

  vix::async::core::io_context &context();

private:
  vix::async::core::io_context ctx_;
  vix::async::core::cancel_source cancel_;
  std::thread thread_;
};

Then AppBootstrap can look like:

txt
create AppState
create AsyncRuntime
start AsyncRuntime
create App
register middleware
register routes
run App
stop AsyncRuntime

This keeps main() clean.

Complete test flow ​

Run:

bash
vix run async_app.cpp

Check status:

bash
curl -i http://127.0.0.1:8080/api/status

Wait two seconds.

Check again:

bash
curl -i http://127.0.0.1:8080/api/status

Trigger manual refresh:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/refresh

Check status again:

bash
curl -i http://127.0.0.1:8080/api/status

Stop with:

txt
Ctrl+C

Summary ​

Use examples/background-task.md for a standalone worker.

Use this page when async runs inside an application.

The core shape is:

cpp
vix::async::core::io_context async_ctx;
vix::async::core::cancel_source cancel_source;

auto worker =
  background_refresh_worker(
    async_ctx,
    state,
    cancel_source.token()
  );

std::move(worker).start(async_ctx.get_scheduler());

std::thread async_thread([&async_ctx]()
{
  async_ctx.run();
  async_ctx.shutdown();
});

vix::App app;

register_routes(app, state, async_ctx);

app.run(8080);

cancel_source.request_cancel();
async_ctx.stop();
async_thread.join();

The mental model is:

txt
HTTP handlers
  should stay fast

async runtime
  runs background work

shared state
  connects background work to HTTP routes

cancellation
  stops the worker cleanly

This gives you a practical base for using Vix Async inside real Vix applications.

Released under the MIT License.