Event Hubs Push vs Pull: How Consumers Work
One of the most common confusions about Azure Event Hubs is how events actually get to consumers. Unlike a webhook or Azure Service Bus push subscription, Event Hubs does not push events to your application. Instead, your application reaches out and pulls events from Event Hubs at its own pace. This pull model is what makes Event Hubs scalable and fault-tolerant — consumers control their own rate, and a slow consumer cannot overwhelm itself with events it is not ready to process.
Push vs pull: the fundamental difference
In a push model, the messaging service sends events to your consumer as they arrive. Your application must be ready to receive them at all times. If your application is busy or slow, the service buffers messages and eventually times out or drops events. Examples: HTTP webhooks, Azure Service Bus push subscriptions, Azure Event Grid.
In a pull model, your consumer periodically asks the messaging service “give me the next batch of events.” The consumer controls the rate. If it is busy, it simply does not ask for more events until it is ready. The service holds all events in the partition log until the consumer retrieves them (or until retention expires). Examples: Azure Event Hubs, Apache Kafka, Amazon Kinesis.
| Characteristic | Push model (Service Bus, Event Grid) | Pull model (Event Hubs, Kafka) |
|---|---|---|
| Who initiates reading | The service (pushes to consumer) | The consumer (polls the service) |
| Consumer overload protection | Consumer must implement backpressure or rate limiting | Consumer naturally controls its own rate by polling speed |
| Consumer offline handling | Messages queue up; service retries delivery | Events stay in the partition log until retention expires |
| Multiple independent consumers | Requires topics/subscriptions (complex setup) | Native via consumer groups — each group has independent offsets |
| Replay/reprocessing | Not supported (messages are deleted after delivery) | Yes — seek to any offset or timestamp within retention window |
| Best for | Reliable app-to-app messaging with transaction semantics | High-throughput telemetry, analytics, and log streaming |
How the pull model works in practice
When your Event Hubs consumer calls receive_batch() or receive(), it sends a request to the Event Hubs broker saying “give me up to N events from partition P, starting at offset X.” The broker responds with the next available events. The consumer processes them, then calls receive again for the next batch.
This loop runs continuously for as long as the consumer is active:
import asyncio
from azure.eventhub.aio import EventHubConsumerClient
from azure.eventhub.extensions.checkpointstoreblobaio import BlobCheckpointStore
import json
import time
async def process_event_batch(partition_context, events):
"""
This function is called each time a batch of events is available.
The consumer pulls batches at its own pace.
"""
start_time = time.time()
processed = 0
for event in events:
payload = json.loads(event.body_as_str())
# Simulate processing (database write, ML inference, etc.)
await process_single_event(payload)
processed += 1
elapsed = time.time() - start_time
rate = processed / elapsed if elapsed > 0 else 0
print(
f"Partition {partition_context.partition_id}: "
f"processed {processed} events in {elapsed:.2f}s ({rate:.0f} events/sec)"
)
# Checkpoint after the whole batch is processed
# If we crash here, we restart from the beginning of this batch (at-least-once)
await partition_context.update_checkpoint()
async def process_single_event(payload: dict):
"""Simulate event processing with some I/O"""
# In real code: write to DB, call an API, update a cache, etc.
await asyncio.sleep(0.001) # 1ms simulated processing per event
async def main():
checkpoint_store = BlobCheckpointStore.from_connection_string(
conn_str="DefaultEndpointsProtocol=https;AccountName=mystorageacct;...",
container_name="checkpoints"
)
client = EventHubConsumerClient.from_connection_string(
conn_str="Endpoint=sb://eh-ns-mycompany.servicebus.windows.net/;...",
consumer_group="myapp-cg",
eventhub_name="orders",
checkpoint_store=checkpoint_store
)
print("Starting consumer — pulling events from Event Hubs...")
async with client:
# receive_batch calls process_event_batch whenever events are available
# max_batch_size: pull up to 200 events per batch
# max_wait_time: wait up to 5 seconds for events before calling back with empty batch
await client.receive_batch(
on_event_batch=process_event_batch,
max_batch_size=200,
max_wait_time=5
)
asyncio.run(main())Notice that the consumer controls the batch size (max_batch_size=200). If your processing function takes 2 seconds per batch and you pull 200 events per batch, you process 100 events per second. If your source is producing 1,000 events per second, you will fall behind — the unconsumed events simply stay in the partition log until you catch up.
Backpressure: the pull model’s natural protection
Backpressure is the mechanism by which a slow consumer protects itself from being overwhelmed. In the pull model, backpressure is automatic — if your processing is slow, your polling slows down, and you simply fall behind in the partition log. No configuration needed.
The lag (how far behind the consumer is from the latest event) is measurable. You can monitor it using the Event Hubs metrics in Azure Monitor:
# Check consumer lag for a specific consumer group using Azure CLI
# "Consumer Lag" metric shows how many unprocessed events remain per partition
az monitor metrics list \
--resource "/subscriptions/{sub}/resourceGroups/rg-streaming/providers/Microsoft.EventHub/namespaces/eh-ns-mycompany" \
--metric "IncomingMessages,OutgoingMessages" \
--interval PT1M \
--aggregation Total \
--output tableIf lag is growing persistently, you have three options:
- Add more consumer instances (horizontal scaling — each handles a subset of partitions)
- Increase the partition count of the event hub (requires recreating the hub on standard tier)
- Optimise your processing logic to handle events faster
Seeking and replaying events
One of the most powerful features of the pull model is the ability to seek — to position your consumer at any point in the partition history and reprocess events from that point. This is impossible with push-based systems where events are deleted after delivery.
from azure.eventhub import EventHubConsumerClient, EventPosition
from datetime import datetime, timezone, timedelta
# Scenario: a bug in your processing code corrupted data written to your database.
# The bug was introduced 6 hours ago.
# You want to replay all events from 6 hours ago to the present.
six_hours_ago = datetime.now(timezone.utc) - timedelta(hours=6)
# Set starting positions to 6 hours ago for all partitions
starting_positions = {
"0": EventPosition(six_hours_ago),
"1": EventPosition(six_hours_ago),
"2": EventPosition(six_hours_ago),
"3": EventPosition(six_hours_ago),
}
# The checkpoint store currently has offsets from the current position.
# To force a replay, either:
# Option A: delete the checkpoint blobs in storage (they will start from starting_positions)
# Option B: use a new consumer group with no existing checkpoints
client = EventHubConsumerClient.from_connection_string(
conn_str="Endpoint=sb://...",
consumer_group="replay-cg", # A fresh consumer group with no checkpoints
eventhub_name="orders"
# No checkpoint_store — no checkpointing during replay
)
with client:
events = client.receive(
on_event=lambda pc, e: print(
f"Replay: {e.sequence_number} | {e.body_as_str()[:100]}"
),
starting_position=six_hours_ago,
max_wait_time=30
)Kafka compatibility: using Event Hubs with Kafka clients
Event Hubs exposes a Kafka 1.0 compatible endpoint. If you already have Kafka producers or consumers written in Java, Python (confluent-kafka), or any other Kafka client library, you can point them at Event Hubs with minimal configuration changes — just update the bootstrap server and credentials.
from confluent_kafka import Consumer, KafkaError
# Connect a Kafka consumer to Event Hubs Kafka endpoint
# Event Hubs namespace acts as the Kafka broker
config = {
"bootstrap.servers": "eh-ns-mycompany.servicebus.windows.net:9093",
"security.protocol": "SASL_SSL",
"sasl.mechanisms": "PLAIN",
"sasl.username": "$ConnectionString",
"sasl.password": "Endpoint=sb://eh-ns-mycompany.servicebus.windows.net/;SharedAccessKeyName=...",
"group.id": "kafka-consumer-group",
"auto.offset.reset": "earliest",
"enable.auto.commit": False, # Manual commit for reliability
}
consumer = Consumer(config)
consumer.subscribe(["orders"]) # 'orders' is the Event Hub name
print("Kafka consumer pulling from Event Hubs...")
try:
while True:
msg = consumer.poll(timeout=1.0)
if msg is None:
continue
if msg.error():
if msg.error().code() == KafkaError._PARTITION_EOF:
print(f"Reached end of partition {msg.partition()}")
else:
raise Exception(msg.error())
else:
print(f"Received: partition={msg.partition()}, offset={msg.offset()}, value={msg.value()}")
consumer.commit(message=msg, asynchronous=False)
finally:
consumer.close()This Kafka consumer reads from Event Hubs without any Event Hubs SDK. Organisations migrating from Kafka to Azure often use this compatibility layer as an intermediate step.
Common mistakes
- Expecting Event Hubs to call your application when events arrive. Event Hubs does not call webhooks or push events to HTTP endpoints. Your application must poll continuously. If you need push-to-HTTP-endpoint behaviour, use Azure Event Grid instead.
- Setting max_wait_time too short in low-traffic scenarios. If your event hub receives only a few events per minute, a max_wait_time of 0.1 seconds means your consumer is polling thousands of times per second for mostly-empty responses. Set max_wait_time to at least 1–5 seconds for low-traffic hubs to avoid unnecessary API calls and client CPU usage.
- Checkpointing before processing completes. If you call update_checkpoint() before your database writes commit, a crash after the checkpoint but before the write means those events are skipped forever. Always checkpoint after confirming downstream writes succeeded.
- Using the Event Hubs SDK to build a task queue. Event Hubs is for streaming, not for queuing tasks where exactly-once delivery matters. For work-queue patterns (each task processed by exactly one worker, no replays), use Azure Service Bus or Azure Storage Queues instead.
Summary
- Event Hubs uses a pull model — consumers poll for events at their own pace; Event Hubs never pushes to consumers.
- The pull model provides natural backpressure — slow consumers fall behind in the log rather than being overwhelmed.
- Consumers can seek to any position in the log (by offset or timestamp) for replaying and reprocessing within the retention window.
- Event Hubs exposes a Kafka 1.0 compatible endpoint, allowing Kafka clients to read from Event Hubs without code changes.
- Always checkpoint after confirming downstream processing succeeded, not before, to avoid skipping events after a crash.
Frequently asked questions
Does Event Hubs push events to consumers?
No. Event Hubs uses a pull model — consumers actively request (pull) events from partitions at their own pace. Event Hubs does not push events to consumers or call webhooks. This is unlike Azure Service Bus topics, which can push messages to Azure Functions or Logic Apps via push subscriptions.
What is the AMQP protocol in Event Hubs?
AMQP (Advanced Message Queuing Protocol) is the native protocol used by Event Hubs for communication between clients and the service. The Azure SDK handles AMQP automatically. The Kafka-compatible endpoint uses the Kafka protocol instead. You do not need to work with AMQP directly — just use the SDK.
How do I choose between pull-based Event Hubs and push-based Service Bus?
Use Event Hubs when you have high-throughput telemetry or log data that multiple systems need to consume independently and where consumers process at their own pace. Use Service Bus when you need guaranteed message ordering, dead-letter queuing, message TTL, and push delivery to serverless functions — typical enterprise application messaging patterns.