Fire-and-Forget vs Synchronous Function Invocations in the iii Protocol
Fire-and-forget invocations in the iii protocol suppress the return path by omitting the invocation_id and setting action to TriggerAction::Void, while synchronous calls generate a UUID, wait for Message::InvocationResult, and return the deserialized JSON value.
The iii protocol, maintained in the iii-hq/iii repository, supports multiple invocation patterns for remote functions. Understanding the distinction between fire-and-forget and synchronous execution is critical for building responsive applications that balance latency guarantees against reliability requirements.
Core Differences Between Invocation Patterns
The iii engine routes every function call through a single WebSocket message: Message::InvokeFunction. This message contains an optional invocation_id and an optional action field. The presence or absence of these fields determines whether the call executes synchronously or as fire-and-forget.
Synchronous Invocation Flow
In the default synchronous mode, the SDK generates a unique invocation_id (UUID) and sends it with the InvokeFunction message. According to the engine implementation in engine/src/engine/mod.rs, the engine executes the function and builds a Message::InvocationResult that routes back to the caller using the same correlation ID.
The SDK waits up to timeout_ms for this result and returns the deserialized JSON value. If the function throws an error or times out, the SDK receives the error details in the InvocationResult payload and propagates it as a Result::Err.
Fire-and-Forget Invocation Flow
For fire-and-forget, the SDK sets action to Some(TriggerAction::Void) and deliberately omits the invocation_id (sends None). The engine still invokes the function via spawn_invoke_function, but because there is no ID to correlate a response, it never sends an InvocationResult back to the caller.
The SDK immediately resolves with Value::Null (JSON null) without waiting for function completion. Consequently, the caller cannot know if the function succeeded, failed, or timed out.
Async Queue (Enqueue) Pattern
A third option uses TriggerAction::Enqueue { queue }, which still transmits an invocation_id for acknowledgment but places the job on a named queue. The engine returns a receipt object containing messageReceiptId rather than the function result. This differs from fire-and-forget because the caller receives confirmation that the message entered the queue, whereas fire-and-forget provides zero acknowledgment.
Implementation Details in the iii Engine and SDKs
SDK Side: The trigger Method
In sdk/packages/rust/iii/src/iii.rs (lines 1010-1066), the III::trigger method inspects the action field of the TriggerRequest. If the action matches TriggerAction::Void, the client sends InvokeFunction with invocation_id = None and returns Value::Null immediately. For synchronous calls, it generates the UUID, registers a pending promise, awaits the correlated InvocationResult, and returns the payload.
Engine Side: Message Routing
In engine/src/engine/mod.rs (lines 1087-1099), the Message::InvokeFunction handler matches on the action field. When the action is Void, the engine calls spawn_invoke_function with None for the invocation ID parameter, thereby suppressing any response message back to the caller. The function executes in a spawned task, but the engine discards the result rather than serializing it back to the WebSocket client.
Protocol Definitions
TriggerAction::Void is defined in sdk/packages/rust/iii/src/protocol.rs and mirrored in engine/src/protocol.rs as the explicit fire-and-forget routing option. This enum variant signals to both SDK and engine that no return path should be established.
Code Examples
Rust SDK: Fire-and-Forget vs Synchronous
use iii_sdk::{III, TriggerRequest, TriggerAction};
use serde_json::json;
// Synchronous call – waits for result and errors
let result = iii.trigger(TriggerRequest {
function_id: "compute_sum".into(),
payload: json!({ "x": 10, "y": 20 }),
action: None, // Default synchronous behavior
timeout_ms: Some(5000),
}).await?; // Returns JSON value or error
// Fire-and-forget – returns null immediately, function executes asynchronously
iii.trigger(TriggerRequest {
function_id: "log_metrics".into(),
payload: json!({ "cpu": 45.2, "memory": 1024 }),
action: Some(TriggerAction::Void),
timeout_ms: None,
}).await?; // Always resolves to Value::Null
Node.js SDK: Fire-and-Forget
import { iii, TriggerAction } from '@iii/sdk';
// Register a function that will receive the fire-and-forget call
iii.registerFunction('analytics.track', async (data) => {
console.log('Tracking event:', data);
// Return value is discarded; caller receives null
return { recorded: true };
});
// Fire-and-forget invocation – returns immediately
iii.trigger({
function_id: 'analytics.track',
payload: { event: 'purchase', amount: 99.99 },
action: TriggerAction.Void(),
});
// Execution continues without awaiting function completion
The test file sdk/packages/node/iii/tests/bridge.test.ts (lines 42-71) validates this behavior by asserting that TriggerAction.Void() resolves instantly while the registered function processes the payload in the background.
Async Queue Pattern for Comparison
let receipt = iii.trigger(TriggerRequest {
function_id: "iii::durable::publish".into(),
payload: json!({ "topic": "orders", "data": { "id": 123 } }),
action: Some(TriggerAction::Enqueue { queue: "orders".into() }),
timeout_ms: None,
}).await?; // Returns { "messageReceiptId": "uuid-here" }
When to Use Each Pattern
When to Use Fire-and-Forget
Use fire-and-forget when you need low latency and do not require confirmation of success. Ideal scenarios include:
- Emitting metrics or telemetry data
- Logging events to external systems
- Sending notifications where delivery confirmation is unnecessary
- Triggering side effects that must not block the caller
Because the iii protocol provides no error propagation for fire-and-forget, ensure the function handles errors internally via logging or separate error-tracking triggers.
When to Use Synchronous Calls
Choose synchronous invocations when the caller depends on the function output or must react to failure:
- Data retrieval or validation operations
- Transactional workflows requiring confirmation before proceeding
- Sequential logic where step B requires the result of step A
- User-facing operations where errors must propagate to the UI
Summary
- Fire-and-forget sends
Message::InvokeFunctionwithout aninvocation_idand withaction: TriggerAction::Void, resulting in immediatenullreturn and no error propagation to the caller. - Synchronous invocations generate a UUID for
invocation_id, await the correspondingMessage::InvocationResult, and return the full JSON response or error details. - The distinction is enforced in
sdk/packages/rust/iii/src/iii.rs(SDK logic) andengine/src/engine/mod.rs(engine routing). - Use fire-and-forget for high-throughput, non-critical side effects; use synchronous calls when data integrity or error handling is required.
Frequently Asked Questions
What happens if a fire-and-forget function fails in the iii protocol?
The caller never receives the error. Because fire-and-forget omits the invocation_id, the engine cannot route an InvocationResult back to the SDK. Errors must be handled internally within the function using try-catch blocks, internal logging, or external observability services.
Can I convert a synchronous call to fire-and-forget without modifying the function code?
Yes. The caller controls the invocation pattern via the action field in the TriggerRequest. The same function can be invoked synchronously by one client and as fire-and-forget by another, depending solely on whether action is set to None or Some(TriggerAction::Void).
Is there a network latency penalty for synchronous calls compared to fire-and-forget?
Synchronous calls incur higher latency because the SDK waits for the function to execute plus the round-trip time for the InvocationResult message. Fire-and-forget reduces network chatter by eliminating the response message, making it suitable for high-frequency operations where minimal latency is prioritized over delivery confirmation.
How does the async queue option differ from fire-and-forget in iii?
Unlike fire-and-forget, the enqueue pattern (TriggerAction::Enqueue) still transmits an invocation_id and returns a messageReceiptId to confirm the job entered the queue. Fire-and-forget provides no acknowledgment whatsoever, while enqueue provides confirmation of queue entry but defers execution to a background worker process.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →