Timeouts
A Timeout describes how long a task is allowed to execute before the ThreadPool records that execution as timed out.
vix::threadpool::TaskOptions options = vix::threadpool::TaskOptions::with_timeout(
vix::threadpool::Timeout::milliseconds(500)
);
auto future = pool.submit([](){
return 42;
}, options);A timeout observes execution duration.
It does not forcibly interrupt C++ code when the configured duration is reached.
Timeout vs deadline
Timeouts and deadlines measure different things.
A timeout measures execution duration:
task starts
↓
execution timer starts
↓
callable runs
↓
execution timer stopsA deadline represents an absolute point in time:
submission
↓
queue waiting
↓
absolute deadlineThe important difference is:
Timeout
queue waiting does not consume it
Deadline
queue waiting does consume itSee Deadlines for absolute time limits.
Timeout
Timeout is a lightweight wrapper around:
std::chrono::millisecondsIts duration type is:
vix::threadpool::Timeout::durationwhich is equivalent to:
std::chrono::millisecondsDisabled timeout
A default timeout is disabled:
vix::threadpool::Timeout timeout;It reports:
enabled() false
disabled_value() true
count() 0A disabled timeout never expires:
timeout.expired(
std::chrono::seconds{10}
);returns:
falseYou can also create a disabled timeout explicitly:
auto timeout = vix::threadpool::Timeout::disabled();Create a timeout in milliseconds
Use:
auto timeout = vix::threadpool::Timeout::milliseconds(500);The stored value is:
500 msand:
timeout.count();returns:
500Create a timeout in seconds
Use:
auto timeout = vix::threadpool::Timeout::seconds(2);The value is converted to milliseconds.
timeout.count();returns:
2000Construct from milliseconds
A timeout can also be constructed directly:
vix::threadpool::Timeout timeout(
std::chrono::milliseconds{500}
);This is equivalent to:
auto timeout = vix::threadpool::Timeout::milliseconds(500);Negative durations are disabled
Negative timeout values are normalized to zero.
auto timeout = vix::threadpool::Timeout::milliseconds(-10);The result is:
enabled() false
count() 0Conceptually:
negative duration
↓
normalize()
↓
0 ms
↓
timeout disabledThis differs from Deadline::after(), where a negative duration creates an already expired absolute deadline.
Zero disables timeout observation
A zero timeout is also disabled:
auto timeout = vix::threadpool::Timeout::milliseconds(0);It reports:
enabled() falseTherefore:
timeout <= 0
↓
disabledfor timeout values constructed through the public Timeout API.
Check whether timeout is enabled
Use:
if (timeout.enabled())
{
// Timeout observation is active.
}The opposite check is:
if (timeout.disabled_value())
{
// Timeout observation is disabled.
}Read the duration
Use:
const auto duration = timeout.value();The returned type is:
std::chrono::millisecondsFor example:
auto timeout = vix::threadpool::Timeout::milliseconds(250);
const auto duration = timeout.value();The value is:
250 msUse:
timeout.count();when only the numeric millisecond count is needed.
Check an elapsed duration
Timeout::expired() compares an elapsed duration with the configured timeout.
auto timeout = vix::threadpool::Timeout::milliseconds(50);
const bool expired = timeout.expired(
std::chrono::milliseconds{51}
);The result is:
trueThe comparison is strictly greater than the configured timeout.
Therefore:
timeout.expired(
std::chrono::milliseconds{50}
);returns:
falsewhile:
timeout.expired(
std::chrono::milliseconds{51}
);returns:
trueConceptually:
elapsed <= timeout
↓
not expired
elapsed > timeout
↓
expiredTimeout precision
Timeout stores milliseconds.
expired() converts the supplied elapsed duration to milliseconds before comparing it.
Conceptually:
elapsed duration
↓
duration_cast<milliseconds>
↓
compare with timeoutThe comparison therefore uses millisecond resolution.
Attach a timeout to a task
Use TaskOptions::with_timeout():
vix::threadpool::TaskOptions options = vix::threadpool::TaskOptions::with_timeout(
vix::threadpool::Timeout::milliseconds(100)
);
auto future = pool.submit([](){
return 42;
}, options);Or use the setter:
vix::threadpool::TaskOptions options;
options.set_timeout(
vix::threadpool::Timeout::milliseconds(100)
);
auto future = pool.submit([](){
return 42;
}, options);Check whether task options contain an active timeout with:
if (options.has_timeout())
{
// Timeout observation is enabled.
}Timeout starts when execution starts
A timeout does not measure how long the task has existed.
It measures execution duration.
Conceptually:
task created
↓
task waits in queue
↓
worker takes task
↓
execution starts
↓
timeout measurement starts
↓
callable runs
↓
execution ends
↓
timeout evaluatedTime spent waiting in the worker queue does not count toward the timeout.
Queue waiting does not consume the timeout
Suppose a task has:
timeout = 100 msand waits:
500 msin a worker queue before executing.
If its callable then runs for:
20 msthe timeout observes:
20 msnot:
520 msA timeout therefore cannot be used to say:
this task must finish within 100 ms of submissionUse a Deadline when queue waiting must count toward the time limit.
Low-level Task timeout behavior
Task::run() records the execution start time immediately before invoking the callable:
status = running
↓
started_at = now
↓
callable executes
↓
finished_at = nowIt then checks:
finished_at - started_atagainst the configured timeout.
If execution exceeded the timeout:
status = timed_out
result = timeoutLow-level timeout example
A low-level task can observe an execution timeout directly:
vix::threadpool::TaskOptions options = vix::threadpool::TaskOptions::with_timeout(
vix::threadpool::Timeout::milliseconds(1)
);
vix::threadpool::Task task(
vix::threadpool::TaskId{1},
vix::threadpool::TaskFunction([](){
std::this_thread::sleep_for(
std::chrono::milliseconds{5}
);
}),
options,
1
);
const auto result = task.run();After execution:
status = timed_out
result = timeoutThe callable still ran for its complete duration.
Timeout does not prevent execution from starting
Unlike cancellation and deadlines, timeout is not part of:
options.should_skip_before_run();That function checks only:
cancellation requested
or
deadline expiredA timeout cannot already be considered exceeded before execution because its timer begins when execution starts.
Timeout does not interrupt the callable
Suppose:
timeout = 10 msand the callable runs for:
5 secondsThe runtime does not stop it after 10 ms.
The execution is:
callable starts
↓
10 ms passes
↓
callable continues
↓
5 seconds passes
↓
callable returns
↓
runtime observes timeoutTimeout is therefore observational.
It is not preemptive execution control.
post() timeout behavior
ThreadPool::post() keeps the timeout attached to the low-level task.
vix::threadpool::TaskOptions options = vix::threadpool::TaskOptions::with_timeout(
vix::threadpool::Timeout::milliseconds(10)
);
const bool accepted = pool.post([](){
perform_work();
}, options);If the callable exceeds the timeout:
callable executes completely
↓
Task::run() measures duration
↓
timeout exceeded
↓
TaskStatus::timed_out
TaskResult::timeoutThe boolean returned by post() only describes task acceptance.
It does not describe the final execution outcome.
Accepted does not mean completed within timeout
This:
const bool accepted = pool.post([](){
perform_work();
}, options);can return:
trueeven if the task later exceeds its timeout.
The two questions are different:
post() return value
↓
Was the task accepted?
timeout result
↓
Did execution exceed its configured duration?Because post() has no Future, timeout outcomes are primarily visible through runtime metrics and statistics.
InlineExecutor timeout behavior
InlineExecutor also observes execution duration.
vix::threadpool::InlineExecutor executor;
vix::threadpool::TaskOptions options = vix::threadpool::TaskOptions::with_timeout(
vix::threadpool::Timeout::milliseconds(10)
);
const bool accepted = executor.post([](){
perform_work();
}, options);The callable executes synchronously.
After it returns, InlineExecutor compares the elapsed duration with the timeout.
If the timeout was exceeded:
timed_out_tasks increasesand post() still returns:
truebecause the callable was executed and the timing outcome was handled.
submit() adds a Future layer
ThreadPool::submit() is different because the user callable is wrapped in asynchronous result handling.
auto future = pool.submit([](){
return 42;
}, options);Conceptually:
low-level Task
↓
result wrapper
↓
user callable
↓
Promise
↓
FutureThe low-level Task still measures timeout around the wrapper.
However, the wrapper publishes the Future result before control returns to the low-level Task timeout check.
This creates an important distinction in the current implementation.
Current submit() timeout behavior
Consider:
vix::threadpool::TaskOptions options = vix::threadpool::TaskOptions::with_timeout(
vix::threadpool::Timeout::milliseconds(1)
);
auto future = pool.submit([](){
std::this_thread::sleep_for(
std::chrono::milliseconds{10}
);
return 42;
}, options);The execution path is currently:
Task::run()
↓
start execution timer
↓
submit wrapper runs
↓
user callable runs for 10 ms
↓
Promise::set_value(42)
↓
Future becomes completed
↓
wrapper returns
↓
Task::run() measures duration
↓
timeout exceeded
↓
low-level Task becomes timed_outThe Future result and low-level Task result can therefore differ.
Verified current result
With:
timeout = 1 ms
callable duration ≈ 10 ms
return value = 42the current implementation produces:
value: 42
future status: completed
future result: success
completed: 0
timed out: 1The Future reports successful completion because its Promise received the value before the low-level timeout check.
The worker metrics report the task as timed out because Task::run() observed that execution exceeded the timeout afterward.
Future and Task timeout state are separate
For the current submit() implementation:
Future
↓
result published by submit wrapper
low-level Task
↓
execution duration observed after wrapper returnsThese states can diverge.
For an over-time submit() operation:
Future:
status = completed
result = success
value = available
low-level Task:
status = timed_out
result = timeout
worker metrics:
timed_out += 1Application code should therefore not currently use the Future alone to determine whether a completed submit() exceeded its configured execution timeout.
Likewise, pool timeout metrics should not be interpreted as meaning that every corresponding Future contains a timeout error.
handle() has the same timeout layering
ThreadPool::handle() uses the same result-producing wrapper model as submit().
auto handle = pool.handle([](){
return 42;
}, options);The handle's Future can receive the callable result before the low-level Task evaluates execution timeout.
Conceptually:
TaskHandle
↓
Future
↓
result wrapper
↓
value published
↓
low-level timeout evaluated afterwardThe same distinction between asynchronous result state and worker-level timeout state applies.
Timeout does not currently become a Future error after execution
A timeout error in a Future is represented as:
status = timed_out
result = timeout
error = timeoutand:
future.get();throws std::system_error.
However, the current submit() execution-time timeout path does not publish:
ThreadPoolErrc::timeoutafter a callable has already produced its value.
The Future can therefore remain successful even though the low-level task is classified as timed out.
This is the current runtime behavior and should be considered when using task timeouts for result-producing work.
Deadline timeout and execution timeout are different
A Future can receive ThreadPoolErrc::timeout when a deadline prevents execution from starting.
For example:
deadline expires while queued
↓
submit wrapper checks deadline
↓
Promise::set_error(timeout)
↓
Future:
status = timed_out
result = timeout
error = timeoutThis is different from an execution-duration timeout:
callable runs too long
↓
Promise may already contain value
↓
low-level Task records timeoutBoth use timing concepts, but the current observable Future behavior is different.
Default timeout
ThreadPoolConfig provides:
config.default_timeout;The default value is:
std::chrono::milliseconds{0}which means no default timeout.
Configure one with:
vix::threadpool::ThreadPoolConfig config;
config.default_timeout = std::chrono::milliseconds{500};
vix::threadpool::ThreadPool pool(config);Tasks without their own timeout receive this value.
Default timeout merging
When a task is submitted, the pool performs:
task timeout enabled?
│
├── yes → keep task timeout
│
└── no
↓
pool default_timeout > 0?
│
├── yes → apply pool default
│
└── no → timeout remains disabledFor example:
vix::threadpool::ThreadPoolConfig config;
config.default_timeout = std::chrono::milliseconds{500};
vix::threadpool::ThreadPool pool(config);
auto future = pool.submit([](){
return 42;
});The task receives:
500 msas its effective execution timeout.
Task timeout overrides pool default
An explicit task timeout takes precedence:
vix::threadpool::ThreadPoolConfig config;
config.default_timeout = std::chrono::milliseconds{500};
vix::threadpool::ThreadPool pool(config);
vix::threadpool::TaskOptions options = vix::threadpool::TaskOptions::with_timeout(
vix::threadpool::Timeout::milliseconds(100)
);
auto future = pool.submit([](){
return 42;
}, options);The effective timeout is:
100 msnot:
500 msThe pool only supplies its default when the task timeout is disabled.
Disable a configured pool default for one task
The current merge behavior applies the pool default whenever:
options.has_timeout()is false.
Since zero means disabled:
options.set_timeout(
vix::threadpool::Timeout::disabled()
);still allows the pool's default_timeout to be merged.
Therefore, when a positive pool default timeout is configured, the current TaskOptions API does not provide a separate state meaning:
explicitly disable the pool default for this taskThe states are currently:
task timeout enabled
↓
override default
task timeout disabled
↓
pool default may be appliedExceptions and timeout
If a low-level task callable throws before timeout evaluation completes:
callable throws
↓
Task catches exception
↓
status = failed
result = failureThe low-level Task::run() catch path does not then replace that result with timeout.
For submit(), the wrapper itself catches the user exception and publishes it to the Future.
Because that wrapper returns normally to the low-level task, a sufficiently long failing submit() operation can still be classified as timed out by the worker layer while its Future stores the original exception.
This follows from the same separation between Future state and low-level Task state.
Timeout and cancellation
A task can have both timeout and cancellation:
vix::threadpool::CancellationSource source;
vix::threadpool::TaskOptions options;
options
.set_timeout(
vix::threadpool::Timeout::milliseconds(500)
)
.set_cancellation(
source.token()
);They describe different conditions.
cancellation
↓
explicit stop request
timeout
↓
observed execution duration exceededFor low-level Task::run(), post-execution checks currently evaluate timeout and deadline before cancellation.
Conceptually:
callable finishes
↓
timeout or deadline exceeded?
┌───────┴───────┐
yes no
│ │
timeout cancelled?
│
├── yes → cancelled
└── no → successSee Cancellation.
Timeout and deadline together
A task can also use both timing mechanisms:
vix::threadpool::TaskOptions options;
options
.set_timeout(
vix::threadpool::Timeout::milliseconds(100)
)
.set_deadline(
vix::threadpool::Deadline::after(std::chrono::seconds{1})
);Suppose the task experiences:
queue waiting = 800 ms
execution = 50 msThen:
deadline window = 1 second
timeout = 100 msboth conditions remain within their limits.
If queue waiting exceeds one second:
deadline can prevent executionIf execution takes 150 ms:
timeout can be recorded after executionThese controls therefore complement each other.
Future::wait_for() is not a task timeout
This:
const auto status = future.wait_for(
std::chrono::milliseconds{100}
);does not configure or modify the task timeout.
It only limits how long the calling thread waits.
The difference is:
Future::wait_for(100 ms)
↓
caller waits at most 100 ms
TaskOptions timeout = 100 ms
↓
runtime observes task execution durationIf wait_for() returns:
std::future_status::timeoutthe task can still be queued or running.
Nothing is cancelled automatically.
Use wait_for() for caller-side waiting
For example:
auto future = pool.submit([](){
return perform_work();
});
const auto status = future.wait_for(
std::chrono::milliseconds{100}
);
if (status == std::future_status::timeout)
{
// The result was not ready within 100 ms.
}This says nothing about whether the task violated a TaskOptions timeout.
It only describes result readiness from the caller's perspective.
Use cancellation for cooperative early stop
If a long-running callable must stop while it is still executing, timeout observation alone is insufficient.
Use cooperative cancellation:
auto token = source.token();
auto future = pool.submit([token](){
while (has_more_work())
{
if (token.stop_requested())
{
return false;
}
process_next_item();
}
return true;
}, options);The callable determines where it is safe to stop.
Timeout itself never performs this interruption.
Check elapsed time inside the callable
Application code can also implement its own execution boundary when it needs to react during execution.
const auto start = std::chrono::steady_clock::now();
auto future = pool.submit([start](){
while (has_more_work())
{
const auto elapsed =
std::chrono::steady_clock::now() - start;
if (elapsed > std::chrono::milliseconds{500})
{
return false;
}
process_next_item();
}
return true;
});This is application-controlled cooperative timing.
It is different from the ThreadPool's observational timeout classification.
Equality
Timeout values can be compared directly:
auto first = vix::threadpool::Timeout::milliseconds(500);
auto second = vix::threadpool::Timeout::milliseconds(500);
if (first == second)
{
// Same stored duration.
}Inequality is also supported:
if (first != second)
{
// Different stored durations.
}Equality compares the normalized millisecond values.
For example:
vix::threadpool::Timeout::milliseconds(-1) ==
vix::threadpool::Timeout::milliseconds(0)is true because both normalize to a disabled zero-duration timeout.
Choosing between timing mechanisms
Use a timeout when you need to observe:
How long did execution itself take?Use a deadline when you need to express:
Do not begin this work after this absolute time.Use Future::wait_for() when you need:
How long should this caller wait for the result?Use cancellation when running code must cooperatively react to:
This work is no longer needed.The four concepts are intentionally separate:
Timeout
execution duration
Deadline
absolute expiration
Future::wait_for()
caller waiting duration
Cancellation
cooperative stop requestTimeout model summary
The core Timeout type behaves as:
duration <= 0
↓
disabled
duration > 0
↓
enabled
↓
task execution starts
↓
measure elapsed time
↓
elapsed > timeout?
┌───────┴───────┐
yes no
│ │
timed out successFor low-level Task, post(), and InlineExecutor, timeout classification happens after the callable returns.
For submit() and handle(), the current architecture adds another result layer:
Task starts
↓
wrapper starts
↓
user callable runs
↓
Promise publishes result
↓
Future becomes ready
↓
wrapper returns
↓
Task checks timeoutThe important properties are:
Timeoutstores milliseconds.- A zero timeout is disabled.
- Negative values normalize to zero.
milliseconds()andseconds()are provided as factories.expired()uses a strictly greater-than comparison.- Queue waiting does not count toward timeout duration.
- Timeout measurement begins when low-level task execution starts.
- Timeout does not prevent a callable from starting.
- Timeout does not forcibly interrupt running C++ code.
post()can be accepted and later recorded as timed out.InlineExecutorexecutes the callable completely before recording timeout.ThreadPoolConfig::default_timeoutis applied when a task has no enabled timeout.- An explicit enabled task timeout overrides the pool default.
- A disabled task timeout does not currently suppress a positive pool default.
- In the current
submit()andhandle()implementation, the Future result can be published before the low-level timeout is evaluated. - Because of that layering, a Future can report success while worker metrics report the task as timed out.
Future::wait_for()is caller-side waiting and is not a task timeout.- Use cooperative cancellation or explicit checks inside long-running code when execution must stop before the callable naturally returns.
Continue with Scopes for structured concurrent work or Task Groups for manual task-group coordination.