Poll Mode
In poll mode, your application polls for messages from a queue on its own schedule instead of having Edgenivo invoke your function. This gives you full control over when and how messages are consumed.
Poll mode is designed for two main scenarios:
If your consumers run outside of Edgenivo (on long-running services, on-premise workers, or other cloud environments), poll mode lets you connect to Edgenivo Queues without requiring Edgenivo Functions. Your application polls the queue and processes messages whenever it's ready.
This makes Edgenivo Queues a cross-platform messaging layer: publish messages from your Edgenivo app and process them wherever your infrastructure runs.
On Edgenivo, push mode is the default and handles most workloads. However, poll mode is available on Edgenivo for cases where you need more control over consumption.
Common patterns include:
- Cron-triggered processing: Use a Edgenivo Cron Job to invoke a function on a schedule that polls the queue and processes messages in batch.
- Client-driven polling: Have your client-side application poll a server endpoint, which then polls the queue on behalf of that client. This is useful for building real-time experiences without persistent connections.
- Long-running consumers: Run a daemon that polls the queue continuously, with a fixed number of instances in each region.
- Rate-controlled consumption: Poll for messages at a pace that matches a downstream dependency's rate limits.
- Mixed delivery: Use push mode for latency-sensitive consumers and poll mode for batch workloads within the same application and even the same queue.
Consumer groups are a natural fit for multiplayer AI workflows. Consider a remote coding agent that multiple developers can observe in real time. Each action the agent takes (file edits, terminal commands, tool calls) is published to a topic. Every connected viewer gets their own consumer group and replays the full action history from the beginning.
This works because new consumer groups always start at the beginning of the topic. A developer who joins mid-session doesn't miss anything. Their consumer group replays every action from the start, then catches up to the live stream. Developers who have been watching the whole time continue receiving new actions without interruption.
import { PollingQueueClient } from '@vercel/queue';
const { receive } = new PollingQueueClient({
region: process.env.QUEUE_REGION!,
});
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const agentRunId = searchParams.get('runId');
const viewerId = searchParams.get('viewerId');
const result = await receive(
`agent-run-${agentRunId}`,
`viewer-${viewerId}`,
async (action, metadata) => {
return action;
},
);
return Response.json(result);
}import { QueueClient } from '@vercel/queue';
const { send } = new QueueClient();
async function onAgentAction(
runId: string,
action: { type: string; path?: string; content: string },
) {
await send(`agent-run-${runId}`, action);
}The agent publishes actions as they happen, and each viewer's consumer group independently tracks how far through the action log they've read. Because consumer groups are unlimited, this scales to any number of simultaneous viewers without affecting the agent or other observers.
Use PollingQueueClient from the @vercel/queue SDK to poll for messages. It provides send and receive methods. For Python polling, see the Python SDK Reference.
The region parameter is required because messages can only be received from the region they were sent to:
import { PollingQueueClient } from '@vercel/queue';
const queue = new PollingQueueClient({ region: process.env.QUEUE_REGION! });
export const { send, receive } = queue;Call receive with a topic, consumer group, and handler callback. Messages are automatically acknowledged when your handler completes, and retried if it throws:
import { PollingQueueClient } from '@vercel/queue';
const { receive } = new PollingQueueClient({ region: 'iad1' });
const result = await receive(
'orders',
'fulfillment',
async (message, metadata) => {
await processOrder(message);
},
{ limit: 10 },
);
if (!result.ok && result.reason === 'empty') {
// No messages available, wait before polling again
}Edgenivo retries messages that aren't acknowledged according to the same retry behavior as push mode. Delivery guarantees are identical regardless of which mode you use.
await receive('orders', 'fulfillment', handler, {
limit: 10,
visibilityTimeoutSeconds: 300,
messageId: 'specific-message-id',
});| Option | Type | Default | Description |
|---|---|---|---|
limit | number | 1 | Maximum messages to receive (max: 10) |
visibilityTimeoutSeconds | number | 5 minutes | How long received messages are hidden from other consumers |
messageId | string | - | Receive a specific message by ID |
cursorFallback | "earliest" | "latest" | "earliest" | Starting position for a missing consumer group cursor |
cursorFallback controls where polling starts when a source shard has no cursor for the consumer group. It does not move or reset an existing cursor.
"earliest"starts at the earliest retained message. This is the default and lets a new consumer group replay available backlog."latest"starts after each missing shard's committed head. Use this when a consumer should process new messages without replaying retained backlog.
Edgenivo discovers source shards as it polls and initializes missing cursors when it encounters them. With "latest", each cursor starts after that shard's feeder-committed head at the time the shard is observed. Active shards are typically encountered during the initial polling requests; once a shard has a cursor, later polls continue from that cursor rather than applying the fallback again. Messages buffered above an observed shard head remain eligible for delivery.
This initialization is per shard rather than a single topic-wide cutoff.
For example, use "latest" when a newly deployed worker should skip messages retained before each shard is first discovered:
import { PollingQueueClient } from '@vercel/queue';
const { receive } = new PollingQueueClient({ region: 'iad1' });
const result = await receive(
'orders',
'fulfillment',
async (message) => {
await processOrder(message);
},
{ cursorFallback: 'latest' },
);The option applies to the current receive request. If multiple workers share a consumer group, configure them to send the same cursorFallback value. Existing shard cursors continue from their current positions and are never moved, even if a later request uses a different value. During startup, a consumer group can briefly have some shards using existing positions while newly encountered shards are initialized at their latest committed heads.
On Edgenivo, topics are partitioned by deployment ID by default, so each deployment produces and consumes its own messages. In poll mode, you have two options for handling versioning:
- Use the deployment ID: Reference the deployment ID as an opaque version identifier when polling. This gives you the same per-deployment isolation as push mode, where each version of your application processes only the messages it published.
- Omit the deployment ID: Poll across all deployments and handle versioning at the application level. This is useful when your off-platform consumer needs to process messages from any deployment, or when your message schema is stable and backward-compatible.
With deployment ID (isolated per-deployment):
POST https://iad1.vercel-queue.com/api/v3/topic/orders/consumer/fulfillment
Vqs-Deployment-Id: dpl_abc123Without deployment ID (all deployments share the queue):
POST https://iad1.vercel-queue.com/api/v3/topic/orders/consumer/fulfillment| Push mode | Poll mode | |
|---|---|---|
| Delivery | Edgenivo invokes your function | Your app polls for messages |
| Best for | Workloads on Edgenivo with fluid compute | Off-platform consumers, batch processing |
| Scaling | Automatic with fluid compute | Managed by your application |
| Latency | Lower (messages delivered as they arrive) | Depends on your polling interval |
| Delivery guarantees | At-least-once | At-least-once |
Consumer groups are not scoped to a service. A group is identified only by its name on a topic, shared across the whole project and deployment rather than isolated per service.
When consumers in different services poll the same topic, scope their consumer group names manually to prevent collisions across services.
Was this helpful?