Skip to content
Ironflow is pre-1.0: APIs can change between releases. Pin a version. What is ready →

Manual Acknowledgment

Manual acknowledgment ensures at-least-once delivery by requiring explicit confirmation that an event was processed. This is essential for critical reliability patterns like payment processing or database synchronization.

ModeBehavior
autoEvents are acknowledged automatically upon delivery.
manualYou must explicitly call ack(), nak(), or term() for each event.

MethodDescription
ack(eventId)Success: Confirms processing is complete.
nak(eventId, delay?)Retry: Requests redelivery (optionally after a delay).
term(eventId)Terminal Failure: Permanently rejects the event (no redelivery).

The examples join a consumer group. The group must already exist with ackMode set to manual. A browser join to a group that does not exist creates it in auto mode, and acks on an auto group do nothing. See Creating Consumer Groups.

import { ironflow, type AckableSubscription } from "@ironflow/browser";
// The default ConnectRPC transport cannot send acks. Use the WebSocket transport.
ironflow.configure({ serverUrl: "http://localhost:9123", transport: "websocket" });
// `joinConsumerGroup()` sets `consumerGroup` and `ackMode: "manual"` for you.
let sub: AckableSubscription;
sub = await ironflow.joinConsumerGroup("order-workers", "events:payment.>", {
onEvent: async (event) => {
try {
await processPayment(event.data);
// Use the 'sub' object to acknowledge
await sub.ack(event.eventId);
} catch (err) {
// Retry in 5 seconds
await sub.nak(event.eventId, 5000);
}
},
});

Go and Node can join a MANUAL group over PubSubService.SubscribeBidirectional. The stream is the group member, and it carries the events and the acks. Each ack, nak or term waits for the server result, so a failed ack returns an error instead of a silent loss. WebSocket stays the default transport.

sub, err := client.JoinConsumerGroup(ctx, "order-workers",
ironflow.WithJoinTransport("grpc"))
if err != nil {
log.Fatal(err)
}
defer sub.Unsubscribe()
for event := range sub.Events() {
if err := processPayment(event.Data); err != nil {
_ = sub.Nak(event.ID, 5*time.Second)
continue
}
if err := sub.Ack(event.ID); errors.Is(err, ironflow.ErrStaleAck) {
// The stream reconnected. The server redelivers the event.
}
}

The Node SDK holds events until joinConsumerGroupStream returns, so sub is set when onEvent runs. The stream needs Ironflow server v0.48.0 or later. Against an older server the join fails with a SERVER_TOO_OLD error that names the first server version with the stream (0.48.0). The SDK does not retry it. Use the WebSocket transport there.

If the stream drops, the SDK reconnects with backoff. The member ID is new after a reconnect, so an ack for an event of the old stream fails with ErrStaleAck (Go) or StaleAckError (Node). The server redelivers that event. The browser cannot stream both ways, so it keeps the WebSocket ack frame. See Protocols for the frames and result codes.

  1. Timeout Handling: Ensure your processing logic completes before the server’s visibility timeout.
  2. Idempotency: Always implement idempotency in your onEvent handlers, as network issues can cause duplicate deliveries before an ack is received.
  3. Use TERM for Invalid Data: Don’t use nak() for validation errors; use term() to prevent an infinite retry loop of bad data.