azure_messaging_eventhubs 0.16.0

Rust client for Azure Eventhubs Service
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
<!-- cspell:words azeventhubs checkpointing minghuaw -->

# Migrating to azure_messaging_eventhubs from azeventhubs

This guide helps you migrate from the community [`azeventhubs`](https://crates.io/crates/azeventhubs) crate (by [minghuaw](https://github.com/minghuaw), latest v0.20.0) to the official [`azure_messaging_eventhubs`](https://crates.io/crates/azure_messaging_eventhubs) crate (v0.15.0) published by the Azure SDK for Rust team.

This is a **cross-library migration**, not a version bump. `azeventhubs` and `azure_messaging_eventhubs` are different crates, written by different authors, with different type names, module layouts, and design philosophies. You are switching libraries, so expect to rewrite client construction, authentication, and the send/receive code rather than just bumping a version in `Cargo.toml`. The good news: the official crate covers everything `azeventhubs` did and adds the pieces it explicitly did not (a processor, checkpoint stores, and Microsoft Entra ID authentication).

## Table of Contents

- [Why Migrate](#why-migrate)
- [At a Glance](#at-a-glance)
- [General Changes](#general-changes)
  - [Crate Name and Cargo.toml](#crate-name-and-cargotoml)
  - [use Statements and Module Layout](#use-statements-and-module-layout)
  - [Authentication](#authentication)
  - [Client Construction](#client-construction)
- [Common Scenarios](#common-scenarios)
  - [Producing Events](#producing-events)
  - [Producing Events in a Batch](#producing-events-in-a-batch)
  - [Consuming Events from a Partition](#consuming-events-from-a-partition)
  - [Scalable Consumption with EventProcessor](#scalable-consumption-with-eventprocessor)
  - [Checkpoint Store Setup (Blob Storage)](#checkpoint-store-setup-blob-storage)
- [Error Handling](#error-handling)
- [Async Runtime and Concurrency](#async-runtime-and-concurrency)
- [Feature Flags](#feature-flags)
- [FAQ and Common Pitfalls](#faq-and-common-pitfalls)
- [Additional Resources](#additional-resources)

## Why Migrate

- **Official, actively maintained crate.** `azure_messaging_eventhubs` is built and supported by the Azure SDK team and follows the [Azure SDK Design Guidelines for Rust](https://azure.github.io/azure-sdk/rust_introduction.html). The community `azeventhubs` crate served the Rust ecosystem well as the de facto Event Hubs client, but it is unofficial and its own documentation now points users to the official SDK.
- **Microsoft Entra ID authentication.** `azeventhubs` authenticates with connection strings (shared access keys). The official crate integrates with [`azure_identity`](https://crates.io/crates/azure_identity), so you can use `DeveloperToolsCredential` for local development and managed identity in production, with no secrets in your configuration.
- **Scalable consumption with `EventProcessor`.** `azeventhubs` left "Event Hub Processor" and "Checkpoint Store" as unimplemented features. The official crate ships an `EventProcessor` that balances partitions across instances, plus a `CheckpointStore` trait and a blob-backed implementation in the companion crate `azure_messaging_eventhubs_checkpointstore_blob`.
- **Geo-replication support.** The official client negotiates the Event Hubs [geo-replication](https://learn.microsoft.com/azure/event-hubs/geo-replication) capability so producers and consumers cooperate with namespace failover.
- **Built-in resilience.** The AMQP connection layer transparently re-establishes connections, links, and claims-based-security tokens after transient failures, including detecting and surfacing partition reassignment as a dedicated `ConsumerDisconnected` error.
- **Shared Azure SDK foundation.** Transport runs on `azure_core_amqp`, errors integrate with `azure_core::Error`, and tracing uses the standard `tracing` crate, so Event Hubs behaves consistently with the rest of the Azure SDK for Rust.

## At a Glance

| Concept | `azeventhubs` (community) | `azure_messaging_eventhubs` (official) |
| --- | --- | --- |
| Producer type | `EventHubProducerClient` | `ProducerClient` |
| Consumer type | `EventHubConsumerClient` | `ConsumerClient` |
| Construction | `new_from_connection_string(...)` | `ProducerClient::builder().open(host, name, credential)`, or `.open_with_connection_string(...)` |
| Authentication | Connection string (SAS key) | Microsoft Entra ID via `azure_identity` credentials (recommended), or a connection string / SAS |
| Endpoint input | Connection string contains the host | Fully qualified namespace host, for example `my-ns.servicebus.windows.net` |
| Sent event type | `EventData` | `EventData` (with a builder), or anything `Into<EventData>` |
| Received event type | `ReceivedEventData` | `ReceivedEventData` |
| Receive a partition | `read_events_from_partition(...)` -> stream | `open_receiver_on_partition(...).stream_events()` |
| Start position | `EventPosition` | `StartPosition` + `StartLocation` |
| Scalable consumption | Not available | `EventProcessor` + `CheckpointStore` |
| Checkpoint store | Not available | In-memory, or blob via companion crate |
| Error type | Crate-specific error enums | `azure_messaging_eventhubs::EventHubsError` |

## General Changes

### Crate Name and Cargo.toml

Remove `azeventhubs` and add the official crate plus `azure_identity`, which supplies the credential types the new client authenticates with:

```diff
 [dependencies]
- azeventhubs = "0.20"
+ azure_messaging_eventhubs = "0.15"
+ azure_identity = "1"
```

Both crates are async and run on [tokio](https://tokio.rs), so your existing runtime dependency carries over unchanged.

If you want scalable consumption with durable checkpoints in Azure Blob Storage, also add the companion crate. You build the container client yourself, so you need `azure_storage_blob` and `azure_core` (for the `Url` type) as direct dependencies too:

```diff
 [dependencies]
+ azure_messaging_eventhubs_checkpointstore_blob = "0.9"
+ azure_storage_blob = "1"
+ azure_core = "1"
```

You will also typically want `futures` for the `StreamExt` trait when iterating received events:

```toml
futures = "0.3"
```

### use Statements and Module Layout

The official crate exports its primary types from the crate root, with `models`, `error`, and `processor` submodules for supporting types:

```rust ignore use_statements
// Before: azeventhubs
// use azeventhubs::producer::{EventHubProducerClient, EventHubProducerClientOptions, SendEventOptions};
// use azeventhubs::consumer::{EventHubConsumerClient, EventHubConsumerClientOptions, EventPosition, ReadEventOptions};

// After: azure_messaging_eventhubs
use azure_messaging_eventhubs::{
    ConsumerClient, ProducerClient, SendEventOptions,
    OpenReceiverOptions, StartLocation, StartPosition,
    EventProcessor, CheckpointStore,
};
use azure_messaging_eventhubs::models::EventData;
use azure_messaging_eventhubs::error::ErrorKind;
use azure_identity::DeveloperToolsCredential;
use futures::StreamExt;
```

### Authentication

This is the most significant change. `azeventhubs` parses a connection string (which embeds a shared access key) and the host is part of that string. The official crate takes a fully qualified namespace host and an `azure_identity` credential, so authentication flows through Microsoft Entra ID.

```rust ignore authentication
// Before (azeventhubs): connection string carries both host and key
// let producer = EventHubProducerClient::new_from_connection_string(
//     "Endpoint=sb://my-ns.servicebus.windows.net/;SharedAccessKeyName=...;SharedAccessKey=...",
//     "my-eventhub".to_string(),
//     EventHubProducerClientOptions::default(),
// ).await?;

// After (azure_messaging_eventhubs): host + Entra ID credential, no embedded secret
use azure_identity::DeveloperToolsCredential;
use azure_messaging_eventhubs::ProducerClient;

let credential = DeveloperToolsCredential::new(None)?;
let producer = ProducerClient::builder()
    .open(
        "my-ns.servicebus.windows.net", // fully qualified namespace host
        "my-eventhub",                   // Event Hub name
        credential.clone(),
    )
    .await?;
```

`DeveloperToolsCredential` picks up your Azure CLI login (`az login`) and is appropriate for local development. In production, prefer a managed identity (`ManagedIdentityCredential`) or another specific credential type. See the [`azure_identity`](https://aka.ms/azsdk/rust/identity/docs) documentation for the full set of credentials. To replace key-based access, grant your application identity the **Azure Event Hubs Data Sender** or **Data Receiver** role on the namespace.

#### Keeping a Connection String

You do not have to change the authentication model and the client code in one step. Both builders also accept an Event Hubs connection string, which lets you port the API surface first and move to Entra ID afterwards:

```rust ignore connection_string
use azure_messaging_eventhubs::ProducerClient;

async fn open_with_sas() -> Result<(), Box<dyn std::error::Error>> {
    let connection_string = std::env::var("EVENTHUBS_CONNECTION_STRING")?;

    // The Event Hub name is optional when the connection string carries an
    // `EntityPath`; if you pass both, they must agree.
    let producer = ProducerClient::builder()
        .open_with_connection_string(&connection_string, Some("my-eventhub"))
        .await?;

    producer.close().await?;
    Ok(())
}
```

`ConsumerClient::builder()` exposes the same method. When the connection string carries a `SharedAccessKeyName` and `SharedAccessKey`, the client signs and refreshes SAS tokens for you. When it carries a pre-formed `SharedAccessSignature`, that token is used as-is and cannot be refreshed, so the broker drops the link once the token expires. Entra ID remains the recommendation for production because it avoids storing a secret in your configuration.

### Client Construction

`azeventhubs` constructs clients with associated `new_from_connection_string` functions. The official crate uses a builder that ends in an `.open(...)` call, which establishes the AMQP connection. The builder is where you set options that `azeventhubs` passed through `*Options` structs.

```rust ignore client_construction
// Before: azeventhubs
// let consumer = EventHubConsumerClient::new_from_connection_string(
//     "$Default",                 // consumer group
//     "<CONNECTION_STRING>",
//     "my-eventhub".to_string(),
//     EventHubConsumerClientOptions::default(),
// ).await?;

// After: azure_messaging_eventhubs
use azure_identity::DeveloperToolsCredential;
use azure_messaging_eventhubs::ConsumerClient;

let credential = DeveloperToolsCredential::new(None)?;
let consumer = ConsumerClient::builder()
    .with_consumer_group("$Default".to_string())
    .with_application_id("my-app".to_string())
    // Note: ConsumerClient::open takes the event hub name as an owned String,
    // whereas ProducerClient::open takes &str.
    .open("my-ns.servicebus.windows.net", "my-eventhub".to_string(), credential.clone())
    .await?;
```

Builder methods available before `.open(...)` include `with_consumer_group`, `with_application_id`, `with_instance_id`, `with_retry_options`, and `with_custom_endpoint` on the consumer; the producer builder offers `with_application_id`, `with_retry_options`, and `with_custom_endpoint`.

## Common Scenarios

The snippets below assume you are inside an `async` function and that errors propagate with `?`. See [Async Runtime and Concurrency](#async-runtime-and-concurrency) for the surrounding `#[tokio::main]` setup.

### Producing Events

In `azeventhubs` you build an `EventData` and call `send_event` with `SendEventOptions`. The official crate keeps the `send_event` name and a `SendEventOptions` struct, but accepts anything convertible into `EventData` (a string, a byte vector, or an `EventData` built with the builder), and partition targeting moves into `SendEventOptions.partition_id`.

```rust ignore producing
// Before: azeventhubs
// let event = EventData::from("Hello, Event Hub!");
// producer.send_event(event, SendEventOptions::default()).await?;

// After: azure_messaging_eventhubs
use azure_messaging_eventhubs::{models::EventData, ProducerClient, SendEventOptions};

async fn produce(producer: &ProducerClient) -> Result<(), Box<dyn std::error::Error>> {
    // Send a string to a random partition.
    producer.send_event("Hello, Event Hub!", None).await?;

    // Send raw bytes to a specific partition.
    producer
        .send_event(
            vec![2, 4, 8, 16],
            Some(SendEventOptions {
                partition_id: Some("0".to_string()),
            }),
        )
        .await?;

    // Build an event with properties using the EventData builder.
    let event = EventData::builder()
        .with_content_type("text/plain".to_string())
        .with_body("This is some text")
        .add_property("source".to_string(), "sensor-7")
        .build();
    producer.send_event(event, None).await?;

    Ok(())
}
```

### Producing Events in a Batch

The official crate creates a batch from the producer, adds events to it with `try_add_event_data` (which returns `false` when the batch is full rather than erroring), then sends the whole batch in one call.

```rust ignore batch
// After: azure_messaging_eventhubs
use azure_messaging_eventhubs::{EventDataBatchOptions, ProducerClient};

async fn produce_batch(producer: &ProducerClient) -> Result<(), Box<dyn std::error::Error>> {
    // Build the options from a closure so an overflow batch can target the same
    // partition. EventDataBatchOptions is not Clone.
    let batch_options = || EventDataBatchOptions {
        partition_id: Some("0".to_string()),
        ..Default::default()
    };

    let batch = producer.create_batch(Some(batch_options())).await?;

    // try_add_event_data returns false when the event does not fit, which means
    // the batch is full. Never ignore it, or the event is silently dropped.
    if !batch.try_add_event_data("first event", None)? {
        return Err("event does not fit in an empty batch".into());
    }

    if !batch.try_add_event_data(vec![1, 2, 3, 4], None)? {
        // The batch is full. Send what you have, start a new batch with the same
        // options, and add the event that did not fit to it.
        producer.send_batch(batch, None).await?;

        let batch = producer.create_batch(Some(batch_options())).await?;
        if !batch.try_add_event_data(vec![1, 2, 3, 4], None)? {
            return Err("event does not fit in an empty batch".into());
        }
        producer.send_batch(batch, None).await?;
        return Ok(());
    }

    producer.send_batch(batch, None).await?;
    Ok(())
}
```

### Consuming Events from a Partition

When you already know which partition to read, open a receiver on it and iterate the event stream. This maps directly to `azeventhubs`'s `read_events_from_partition`. The start position changes from `EventPosition` to a `StartPosition` whose `location` is a `StartLocation` (for example `StartLocation::Earliest`).

```rust ignore consuming
// Before: azeventhubs
// let mut stream = consumer
//     .read_events_from_partition("0", EventPosition::earliest(), ReadEventOptions::default())
//     .await?;
// while let Some(event) = stream.next().await {
//     let event = event?;
//     // ... process event.body() ...
// }

// After: azure_messaging_eventhubs
use azure_messaging_eventhubs::{ConsumerClient, OpenReceiverOptions, StartLocation, StartPosition};
use futures::StreamExt;

async fn consume(consumer: &ConsumerClient) -> Result<(), Box<dyn std::error::Error>> {
    let receiver = consumer
        .open_receiver_on_partition(
            "0".to_string(),
            Some(OpenReceiverOptions {
                start_position: Some(StartPosition {
                    location: StartLocation::Earliest,
                    ..Default::default()
                }),
                ..Default::default()
            }),
        )
        .await?;

    let mut stream = receiver.stream_events();
    while let Some(event) = stream.next().await {
        let event = event?; // Result<ReceivedEventData, EventHubsError>
        if let Some(body) = event.event_data().body() {
            println!("received {} bytes", body.len());
        }
        println!("offset {:?}, sequence {:?}", event.offset(), event.sequence_number());
    }

    Ok(())
}
```

`ReceivedEventData` exposes accessors such as `event_data()` (the underlying `EventData`, whose `body()` returns `Option<&[u8]>`), `offset()`, `sequence_number()`, `partition_key()`, `enqueued_time()`, and `raw_amqp_message()`.

### Scalable Consumption with EventProcessor

This capability has no `azeventhubs` equivalent. `EventProcessor` balances partitions across multiple processor instances and records progress in a `CheckpointStore`, so you can run several consumers and let them divide the partitions among themselves. Build it from a `ConsumerClient` and a checkpoint store, run it (typically on a background task), then pull `PartitionClient`s as partitions are assigned to this instance.

```rust ignore processor
// After: azure_messaging_eventhubs (requires the "in_memory_checkpoint_store" feature for InMemoryCheckpointStore)
use azure_messaging_eventhubs::{
    error::ErrorKind, processor::PartitionClient, ConsumerClient, EventProcessor,
    InMemoryCheckpointStore,
};
use azure_identity::DeveloperToolsCredential;
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use std::sync::Arc;

type BoxError = Box<dyn std::error::Error + Send + Sync>;

async fn process() -> Result<(), BoxError> {
    let credential = DeveloperToolsCredential::new(None)?;
    let consumer = ConsumerClient::builder()
        .open("my-ns.servicebus.windows.net", "my-eventhub".to_string(), credential.clone())
        .await?;

    let checkpoint_store = Arc::new(InMemoryCheckpointStore::new());
    // `build` returns an Arc<EventProcessor>, so clone the handle to share it.
    let processor = EventProcessor::builder()
        .build(consumer, checkpoint_store)
        .await?;

    // Run the processor's load balancer in the background.
    let runner = processor.clone();
    let mut handle = tokio::spawn(async move { runner.run().await });

    // Watch the load balancer while consuming. If `run()` stops early, waiting
    // on the next assignment would otherwise block forever.
    let result = tokio::select! {
        runner_result = &mut handle => runner_result?.map_err(BoxError::from),
        consume_result = process_partitions(&processor) => consume_result,
    };

    // Shut the processor down on either outcome, so the load balancer always
    // stops and the task is joined.
    processor.shutdown().await?;
    if !handle.is_finished() {
        handle.await??;
    }
    result
}

async fn process_partitions(processor: &EventProcessor) -> Result<(), BoxError> {
    // Partitions are assigned over time and each stream is long-lived, so keep
    // taking new assignments while the partitions you already hold keep running.
    // `stream_events()` is not `Send`, so drive them on this task with
    // `FuturesUnordered` rather than `tokio::spawn`.
    let mut partitions = FuturesUnordered::new();

    loop {
        tokio::select! {
            partition_client = processor.next_partition_client() => {
                partitions.push(process_partition(partition_client?));
            }
            // Surface failures from partitions that finished, for example after
            // a reassignment.
            Some(finished) = partitions.next(), if !partitions.is_empty() => {
                finished?;
            }
        }
    }
}

async fn process_partition(partition_client: Arc<PartitionClient>) -> Result<(), BoxError> {
    println!("processing partition {}", partition_client.get_partition_id());

    let mut stream = partition_client.stream_events();
    while let Some(event) = stream.next().await {
        match event {
            Ok(event) => {
                // ... handle the event ...
                // Record progress so another instance can resume from here.
                partition_client.update_checkpoint(&event).await?;
            }
            // Load balancing moved this partition to another instance. Stop
            // using this client and let the task end; the processor hands out
            // the next assignment through `next_partition_client`. This is a
            // normal reassignment, not a failure.
            Err(err) if matches!(err.kind, ErrorKind::ConsumerDisconnected(_)) => break,
            Err(err) => return Err(err.into()),
        }
    }

    Ok(())
}
```

`InMemoryCheckpointStore` is convenient for samples and tests but does not survive a restart. For production, use the blob checkpoint store below.

### Checkpoint Store Setup (Blob Storage)

For durable checkpoints shared across instances, use `azure_messaging_eventhubs_checkpointstore_blob`. You construct an `azure_storage_blob::BlobServiceClient`, derive a container client, and wrap it in a `BlobCheckpointStore`. The container should already exist.

```rust ignore checkpoint_store
// After: azure_messaging_eventhubs + azure_messaging_eventhubs_checkpointstore_blob
use azure_core::http::Url;
use azure_identity::DeveloperToolsCredential;
use azure_messaging_eventhubs::{ConsumerClient, EventProcessor};
use azure_messaging_eventhubs_checkpointstore_blob::BlobCheckpointStore;
use azure_storage_blob::BlobServiceClient;
use std::sync::Arc;

// Returns the configured processor; run and consume it as shown above.
async fn build_processor_with_blob_checkpoints(
) -> Result<Arc<EventProcessor>, Box<dyn std::error::Error>> {
    let credential = DeveloperToolsCredential::new(None)?;

    // Build the blob container client that will hold checkpoint and ownership blobs.
    let service_url = Url::parse("https://mystorageaccount.blob.core.windows.net")?;
    let service_client = BlobServiceClient::new(service_url, Some(credential.clone()), None)?;
    let container_client = service_client.blob_container_client("eventhubs-checkpoints");

    let checkpoint_store = BlobCheckpointStore::new(container_client);

    let consumer = ConsumerClient::builder()
        .with_consumer_group("$Default".to_string())
        .open("my-ns.servicebus.windows.net", "my-eventhub".to_string(), credential.clone())
        .await?;

    let processor = EventProcessor::builder()
        .build(consumer, checkpoint_store)
        .await?;

    // Only the checkpoint store changed. Run the processor and consume its
    // partition clients exactly as in the previous scenario; the checkpoints
    // that `update_checkpoint` writes now persist in Azure Blob Storage.
    Ok(processor)
}
```

The same Entra ID credential authenticates both Event Hubs and Blob Storage. Grant the application identity the **Storage Blob Data Contributor** role on the storage account so it can read and write checkpoint blobs.

## Error Handling

`azure_messaging_eventhubs` defines its own error type, `EventHubsError`, exposed through the crate's `Result<T>` alias (`azure_messaging_eventhubs::Result<T>`). Match on the public `ErrorKind` via the error's `kind` field. Note that `kind` is a field, not a method.

Match where the error actually surfaces. `ConsumerDisconnected` is reported by the receive path, so it arrives as a stream item rather than from the call that opened the receiver:

```rust ignore error_handling
use azure_messaging_eventhubs::error::ErrorKind;
use futures::StreamExt;

let receiver = consumer.open_receiver_on_partition("0".to_string(), None).await?;
let mut stream = receiver.stream_events();

while let Some(event) = stream.next().await {
    match event {
        Ok(_event) => { /* ... process the event ... */ }
        Err(err) => match err.kind {
            // The broker disconnected this receiver because another consumer
            // attached with the same or higher owner level (epoch). Stop using
            // this receiver and re-acquire the partition.
            ErrorKind::ConsumerDisconnected(_) => {
                eprintln!("partition reassigned to another consumer");
                break;
            }
            // An error surfaced from azure_core (HTTP, credential, etc.).
            ErrorKind::AzureCore(ref e) => eprintln!("core error: {e}"),
            // An AMQP transport error.
            ErrorKind::AmqpError(ref e) => eprintln!("amqp error: {e:?}"),
            other => eprintln!("other error: {other:?}"),
        },
    }
}
```

`SendRejected` comes from the send path instead, so match it on the producer call:

```rust ignore send_rejected
use azure_messaging_eventhubs::error::ErrorKind;

if let Err(err) = producer.send_event("Hello, Event Hub!", None).await {
    match err.kind {
        // The service rejected the send, for example because a quota was exceeded.
        ErrorKind::SendRejected(ref details) => eprintln!("send rejected: {details:?}"),
        other => eprintln!("other error: {other:?}"),
    }
}
```

`ErrorKind` is `#[non_exhaustive]`, so always include a catch-all arm. `EventHubsError` converts to and from `azure_core::Error`, so it composes with code that works in terms of `azure_core::Result`. When iterating an event stream, each item is a `Result<ReceivedEventData, EventHubsError>`; the `ConsumerDisconnected` variant is the signal that a partition was reassigned and you should re-acquire it through `EventProcessor::next_partition_client`.

## Async Runtime and Concurrency

Like `azeventhubs`, `azure_messaging_eventhubs` is fully async and runs on the [tokio](https://tokio.rs) runtime. All client operations are `async`.

```rust ignore async_runtime
// Cargo.toml:
//
// [dependencies]
// azure_messaging_eventhubs = "0.15"
// azure_identity = "1"
// tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
// futures = "0.3"

use azure_identity::DeveloperToolsCredential;
use azure_messaging_eventhubs::ProducerClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let credential = DeveloperToolsCredential::new(None)?;
    let producer = ProducerClient::builder()
        .open("my-ns.servicebus.windows.net", "my-eventhub", credential.clone())
        .await?;

    producer.send_event("Hello, Event Hub!", None).await?;
    producer.close().await?;
    Ok(())
}
```

Clients are `Send + Sync` and can be shared across tasks. `EventProcessorBuilder::build` already returns an `Arc<EventProcessor>`, so you clone that handle rather than wrapping the processor yourself. That lets the load balancer run on one task while you consume `PartitionClient`s on another, as shown in [Scalable Consumption](#scalable-consumption-with-eventprocessor).

## Feature Flags

| Crate | Feature | Default | Description |
| --- | --- | --- | --- |
| `azure_messaging_eventhubs` | `in_memory_checkpoint_store` | No | Enables `InMemoryCheckpointStore`, a non-durable checkpoint store useful for tests and samples. |
| `azure_messaging_eventhubs` | `default` | Yes | Enables `azure_core_amqp/default`. |

To use the in-memory checkpoint store:

```toml
azure_messaging_eventhubs = { version = "0.15", features = ["in_memory_checkpoint_store"] }
```

Event Hubs traffic runs over AMQP, not HTTP, so the TLS backend comes from the AMQP stack rather than from `azure_core`'s HTTP transport features. The `default` feature enables `azure_core_amqp/default`, which selects `fe2o3-amqp/native-tls`. To use a different backend, disable default features and enable the one you want on `azure_core_amqp` directly.

## FAQ and Common Pitfalls

**Where did the connection string go?**
It is still supported. Call `open_with_connection_string(...)` on either builder to keep your existing connection string, which is the quickest way to port the code without also changing how you authenticate. The official crate recommends Microsoft Entra ID instead: pass the fully qualified namespace host (for example `my-ns.servicebus.windows.net`, the part after `Endpoint=sb://` in your old connection string) plus an `azure_identity` credential to `open(...)`, and assign the **Azure Event Hubs Data Sender** / **Data Receiver** role to your identity to replace key-based access. See [Keeping a Connection String](#keeping-a-connection-string).

**I passed my whole connection string as the host and it failed.**
`open(...)` expects only the host (`my-ns.servicebus.windows.net`), not the full `Endpoint=sb://...;SharedAccessKey=...` string. Strip everything except the namespace host, or call `open_with_connection_string(...)` instead, which parses the full string for you.

**My consumer stopped with a `ConsumerDisconnected` error.**
That is expected when another consumer attaches to the same partition with an equal or higher owner level (epoch), which is exactly how `EventProcessor` reassigns partitions during load balancing. Treat it as a signal to re-acquire a partition via `EventProcessor::next_partition_client` rather than as a fatal error. This has no direct `azeventhubs` analogue because `azeventhubs` had no processor.

**Does the official crate have a buffered producer like the one `azeventhubs` planned?**
Use batches (`create_batch` / `try_add_event_data` / `send_batch`) to group events into a single network request. This gives you explicit control over batching without a separate buffered-producer type.

**How do I get checkpointing that survives restarts?**
`InMemoryCheckpointStore` is in-process only. Add `azure_messaging_eventhubs_checkpointstore_blob` and use `BlobCheckpointStore` so checkpoints and partition ownership persist in Azure Blob Storage and are shared across instances. See [Checkpoint Store Setup](#checkpoint-store-setup-blob-storage).

**`err.kind()` does not compile.**
On `EventHubsError`, `kind` is a public field, not a method. Match on `err.kind`, not `err.kind()`. (This differs from `azure_core::Error`, where `kind()` is a method.)

**Why does `EventData` no longer implement everything it used to?**
The official `EventData` is a distinct type from `azeventhubs`'s. Construct it with `EventData::builder()...build()`, or rely on the `Into<EventData>` conversions for strings and byte vectors when calling `send_event`.

## Additional Resources

- [azure_messaging_eventhubs README](https://github.com/Azure/azure-sdk-for-rust/blob/main/sdk/eventhubs/azure_messaging_eventhubs/README.md)
- [Examples directory](https://github.com/Azure/azure-sdk-for-rust/tree/main/sdk/eventhubs/azure_messaging_eventhubs/examples)
- [Companion crate: azure_messaging_eventhubs_checkpointstore_blob](https://github.com/Azure/azure-sdk-for-rust/tree/main/sdk/eventhubs/azure_messaging_eventhubs_checkpointstore_blob)
- [API documentation on docs.rs](https://docs.rs/azure_messaging_eventhubs/latest/)
- [CHANGELOG.md](https://github.com/Azure/azure-sdk-for-rust/blob/main/sdk/eventhubs/azure_messaging_eventhubs/CHANGELOG.md)
- [azure_identity documentation](https://aka.ms/azsdk/rust/identity/docs)
- [Azure Event Hubs product documentation](https://learn.microsoft.com/azure/event-hubs/)
- [Azure SDK Design Guidelines for Rust](https://azure.github.io/azure-sdk/rust_introduction.html)