Skip to main content
  1. AMQP 0-9-1: The Complete Protocol/

Messages — Properties, Headers, and Body

Messages — Properties, Headers, and Body #

An AMQP message has three parts: routing metadata (not stored with the message), properties (stored, delivered with the message), and the body (the payload). Understanding the standard properties is essential — they control durability, expiration, acknowledgment correlation, and format negotiation.

Message Publication #

A producer publishes with basic.publish:

import pika

channel.basic_publish(
    exchange='events',
    routing_key='order.created',
    body=b'{"order_id": "123", "amount": 99.99}',
    properties=pika.BasicProperties(
        delivery_mode=2,            # persistent
        content_type='application/json',
        content_encoding='utf-8',
        headers={'source': 'checkout-service', 'version': '2'},
        priority=5,
        correlation_id='req-abc123',
        reply_to='amq.gen-XYZ',
        expiration='60000',         # 60 seconds in milliseconds (string!)
        message_id='msg-uuid-456',
        timestamp=1711447200,       # Unix epoch seconds
        type='order.created',
        user_id='checkout-service', # must match authenticated AMQP user
        app_id='checkout-service',
    )
)

The exchange and routing_key are routing parameters — they are not stored in the message. Once the broker routes the message to a queue, these values are gone. The consumer never sees the original exchange or routing key used to publish (unless the application includes them in properties or body).

Exception: the basic.deliver method sent to consumers includes exchange and routing_key — but these are the values at the time of routing, which is the original publish values.

The 14 Standard Properties #

AMQP 0-9-1 defines exactly 14 message properties in the content header frame. These are not arbitrary key-value pairs — each has a fixed position in the frame with defined semantics.

delivery-mode #

Type: octet (1 or 2)

  • 1 (non-persistent): message is stored only in memory. Lost if the broker restarts, even on a durable queue.
  • 2 (persistent): message is written to disk before the broker acknowledges receipt. Survives broker restart if delivered to a durable queue.
properties=pika.BasicProperties(delivery_mode=2)

Performance note: persistent messages (delivery-mode=2) require a disk write per message (or per batch with publisher confirms). Non-persistent messages are purely in-memory and much faster. For high-throughput workloads where some message loss is acceptable (e.g., metrics, logs), use non-persistent.

For exactly-once crash-safe messaging: durable queue + persistent message (delivery-mode=2) + publisher confirms.

content-type #

Type: shortstr (string up to 255 bytes)

The MIME type of the message body. Consumers use this to deserialize the body.

pika.BasicProperties(content_type='application/json')
pika.BasicProperties(content_type='application/octet-stream')
pika.BasicProperties(content_type='text/plain')
pika.BasicProperties(content_type='application/protobuf')

AMQP does not enforce or parse content-type — it is informational for consumers. Not setting it means consumers must infer the format from context or convention.

content-encoding #

Type: shortstr

The encoding applied to the message body, typically gzip or utf-8. If the body is compressed, set content-encoding: gzip so consumers know to decompress before parsing.

import gzip

body = gzip.compress(json.dumps(payload).encode('utf-8'))
pika.BasicProperties(
    content_type='application/json',
    content_encoding='gzip'
)

headers #

Type: table (key-value pairs, typed values)

Custom application headers. Unlike the standard properties (which have fixed positions in the content header frame), headers are a flexible key-value table. Use headers for:

  • Application-level metadata not covered by standard properties
  • Routing information for headers exchanges
  • Distributed tracing context (trace-id, span-id)
  • Schema version
  • Source service identification
pika.BasicProperties(headers={
    'trace-id': 'abc123',
    'schema-version': 2,
    'source': 'payment-service',
    'retry-count': 0,
})

Header value types: strings, integers (short, long, longlong), decimals, booleans, byte arrays, lists, nested tables, and timestamps. Type is inferred from the Python type in pika. Use pika.BasicProperties directly or pass correctly-typed values.

Headers exchange routing: when publishing to a headers exchange, the headers property contains the matching values. The binding’s arguments is compared against this table.

priority #

Type: octet (0–255, but typically 0–9)

The message priority for priority queues (Chapter 6, x-max-priority). Higher values are delivered before lower values. If the queue is not a priority queue, this property is ignored.

pika.BasicProperties(priority=9)  # highest priority
pika.BasicProperties(priority=0)  # lowest priority (default)

correlation-id #

Type: shortstr

An application-defined correlation identifier. Primarily used in the RPC pattern (Chapter 13): a client publishes a request and includes a correlation-id; the server processes the request and publishes a response to the reply-to queue with the same correlation-id. The client matches responses to requests by correlation-id.

import uuid

# Client publishes request
correlation_id = str(uuid.uuid4())
channel.basic_publish(
    exchange='',
    routing_key='rpc-server',
    body=b'{"method": "calculate", "args": [1, 2]}',
    properties=pika.BasicProperties(
        reply_to='amq.gen-XYZ',  # client's reply queue
        correlation_id=correlation_id,
    )
)

Correlation-id is also useful for tracking messages across pipeline stages: include a request correlation-id in all downstream messages so logs can be correlated.

reply-to #

Type: shortstr

The name of the queue to which the recipient should send a reply. Used in the RPC pattern. Typically an exclusive, auto-named reply queue:

result = channel.queue_declare(queue='', exclusive=True)
reply_queue = result.method.queue  # 'amq.gen-...'

channel.basic_publish(
    exchange='',
    routing_key='rpc-server',
    body=request_body,
    properties=pika.BasicProperties(
        reply_to=reply_queue,
        correlation_id=str(uuid.uuid4()),
    )
)

The server publishes its response back to reply-to (with the same correlation-id), using the default exchange.

expiration #

Type: shortstr (but contains a numeric value — in milliseconds as a string)

Per-message TTL in milliseconds. The message is expired after this duration if not consumed. Unlike x-message-ttl (a queue property), this is a per-message property.

pika.BasicProperties(expiration='30000')  # 30 seconds — NOTE: string, not int

Important: expiration is a string in the AMQP spec (for historical reasons). Pass '30000', not 30000.

When both expiration and x-message-ttl are set, the shorter of the two is applied.

Classic queue TTL note: in classic queues, TTL is only evaluated when the message is at the head of the queue. A message with expiration='1000' may sit behind other messages for longer than 1 second before it is even checked. Quorum queues evaluate TTL more proactively.

message-id #

Type: shortstr

An application-defined identifier for the message. Useful for:

  • Idempotency: consumers check if they have already processed this message-id.
  • Logging and tracing: include message-id in log lines.
  • Deduplication: store processed message-ids in a database.

AMQP does not enforce uniqueness — that is the application’s responsibility.

pika.BasicProperties(message_id=str(uuid.uuid4()))

timestamp #

Type: longlong (Unix epoch in seconds)

The time the message was published. Useful for:

  • Staleness detection: consumers can discard messages older than a threshold.
  • Metrics: measure queue latency (consumer receive time - timestamp).
  • Debugging.
import time
pika.BasicProperties(timestamp=int(time.time()))

type #

Type: shortstr

A free-form string naming the message type. Serves as a schema discriminator — consumers can dispatch to the correct handler based on type without parsing the body.

pika.BasicProperties(type='order.created')
pika.BasicProperties(type='payment.charged')

Analogous to a CloudEvents type field or a protobuf oneof discriminator.

user-id #

Type: shortstr

The authenticated user ID that published the message. If set, the broker validates that the value matches the AMQP user that is authenticated on the connection. If they differ, the broker rejects the publish with PRECONDITION_FAILED.

pika.BasicProperties(user_id='checkout-service')

Use user-id when consumers need to verify the publishing identity. It cannot be spoofed because the broker validates it against the authenticated connection.

app-id #

Type: shortstr

The application that published the message. An informational identifier — unlike user-id, it is not validated by the broker. Use it for routing decisions, monitoring, and debugging.

pika.BasicProperties(app_id='checkout-service-v2.3.1')

Custom Headers vs Standard Properties #

When should you use headers vs standard properties?

Use caseRecommendation
Message formatcontent-type
Crash durabilitydelivery-mode=2
RPC correlationcorrelation-id + reply-to
Deduplicationmessage-id
Age-based discardexpiration or timestamp
Message classificationtype
Publisher identityuser-id (validated) or app-id (informational)
Everything elseheaders table

Standard properties are type-safe, known to the broker, and efficiently encoded. Headers are flexible but require consumers to know the key names.

Body Encoding #

The message body (body) is an opaque byte array. AMQP does not parse it — the broker stores and delivers it verbatim. Body encoding is entirely the application’s concern:

import json, struct

# JSON (most common)
body = json.dumps({'order_id': '123'}).encode('utf-8')
properties = pika.BasicProperties(content_type='application/json', content_encoding='utf-8')

# Protobuf
from myapp.proto import Order
order = Order(id='123', amount=99.99)
body = order.SerializeToString()
properties = pika.BasicProperties(content_type='application/x-protobuf')

# MessagePack
import msgpack
body = msgpack.packb({'order_id': '123'})
properties = pika.BasicProperties(content_type='application/x-msgpack')

# Binary with manual length prefix
body = struct.pack('>I', len(payload)) + payload

Large messages: if the body exceeds frame-max - 8 bytes, it is split into multiple content body frames. The broker reassembles them before delivery. The consumer receives the complete body in one piece. Application code does not need to handle fragmentation.

Maximum body size: no hard limit in the AMQP spec. In practice, RabbitMQ can handle large messages but they consume memory and slow throughput. For messages > 1 MB, consider storing the payload externally (S3, blob store) and including only a reference in the AMQP message body.

How Messages Are Transmitted (Frame Sequence) #

When a producer publishes, the broker receives three frame types (covered in Chapter 12):

  1. basic.publish method frame: exchange, routing-key, mandatory, immediate flags.
  2. Content header frame: body size, property flags bitmask, and the non-null property values.
  3. One or more content body frames: the body bytes, each at most frame-max - 8 bytes.

When a consumer receives a message, the broker sends:

  1. basic.deliver method frame: consumer-tag, delivery-tag, redelivered flag, exchange, routing-key.
  2. Content header frame: same as above.
  3. One or more content body frames: same as above.

The content header frame includes only the properties that are set (non-null). A property flags bitmask indicates which of the 14 properties are present, so the consumer knows which to expect.

Summary #

An AMQP message has 14 standard properties and a flexible headers table, plus the body payload. delivery-mode=2 makes messages persistent (crash-safe). content-type and content-encoding describe the body format. correlation-id and reply-to enable RPC patterns. expiration provides per-message TTL. message-id enables idempotency. type is a message schema discriminator. Properties that are not set are not transmitted, keeping overhead low. The body is opaque bytes — all encoding and decoding is application responsibility.