HomeAbout UsContact Us

FreeRTOS Message Buffers and Stream Buffers - High-Throughput Data Streaming

By Jithin Tom
Published in Embedded OS
July 04, 2026
6 min read
FreeRTOS Message Buffers and Stream Buffers - High-Throughput Data Streaming

Table Of Contents

01
Message Buffers: Discrete Message Passing
02
Stream Buffers: Continuous Byte Streaming
03
DMA Integration: High-Efficiency Data Transfer
04
Performance Characteristics
05
Configuration Options
06
Common Use Cases
07
Best Practices and Gotchas
08
Comparison with Alternatives
09
Advanced Patterns
10
Configuration and Tuning
11
Limitations and Considerations
12
Integration with Other FreeRTOS Primitives
13
Conclusion
14
Related Reading
15
References
16
Frequently Asked Questions

FreeRTOS provides two specialized data structures for high-throughput data transfer between tasks and interrupts: Message Buffers and Stream Buffers. These lightweight alternatives to queues excel at moving large blocks of data efficiently, making them ideal for scenarios involving DMA transfers, audio processing, sensor data logging, and high-speed communication protocols.

[!WARNING] Crucial Constraint: Unlike standard FreeRTOS Queues, both Message Buffers and Stream Buffers are strictly designed for single-reader, single-writer scenarios. They are not thread-safe for concurrent multiple writers or multiple readers without external synchronization (like a Mutex).

Message Buffers: Discrete Message Passing

Message Buffers are optimized for sending and receiving discrete, variable-length messages where each send operation corresponds to a complete logical message. Unlike queues that store individual data items, Message Buffers store raw bytes and use a length prefix to delineate message boundaries.

Key Characteristics

  • Variable-length messages: Each message can be any length (up to buffer size minus sizeof(size_t) minus 1 byte)
  • Pass-by-copy: Data is copied into and out of the buffer space
  • Single-Reader / Single-Writer: Lock-free implementation optimized strictly for 1:1 communication channels
  • Length-encoded: Each message is prefixed with its length for boundary detection
  • ISR-safe: Dedicated FromISR APIs exist for interrupt-safe usage
  • No discrete item queueing: Unlike queues, Message Buffers don’t allocate specific slots for items - they store a continuous byte stream with embedded length markers

Basic Usage

// Create a Message Buffer (1KB capacity)
MessageBufferHandle_t xMessageBuffer = xMessageBufferCreate(1024);
// Send a message from task context
size_t bytes_sent = xMessageBufferSend(
xMessageBuffer,
tx_buffer,
message_length,
portMAX_DELAY
);
// Receive a message from task context
size_t bytes_received = xMessageBufferReceive(
xMessageBuffer,
rx_buffer,
sizeof(rx_buffer),
portMAX_DELAY
);
// Send from ISR (no blocking)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xMessageBufferSendFromISR(
xMessageBuffer,
tx_data,
tx_length,
&xHigherPriorityTaskWoken
);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);

Internal Structure

Message Buffers use a circular buffer design with these key components:

  • Storage area: Raw byte storage for message data
  • Write index: Points to next write position
  • Read index: Points to next read position
  • Length tracking: sizeof(size_t) length prefix (typically 4 bytes) before each message data
  • Space tracking: Maintains available space for send operations

When sending a message:

  1. Check if sufficient space exists (message length + sizeof(size_t) for length prefix)
  2. Write length prefix (configurable via configMESSAGE_BUFFER_LENGTH_TYPE, typically size_t)
  3. Write message data
  4. Advance write index (with wrap-around)
  5. Update space tracking

Stream Buffers: Continuous Byte Streaming

Stream Buffers are designed for continuous byte streams where there are no inherent message boundaries. Think of them as “pipes” for bytes - you can write any number of bytes at any time and read any number of bytes when available.

Key Characteristics

  • Byte-stream oriented: No concept of individual messages
  • Arbitrary chunk sizes: Read/write any number of bytes (1 to buffer size)
  • Single-Reader / Single-Writer: Lock-free implementation optimized strictly for 1:1 communication
  • No message overhead: No length prefixes or message boundaries
  • ISR-safe: Dedicated FromISR APIs for interrupt context usage
  • Ideal for DMA: Perfect for peripheral-to-memory or memory-to-peripheral streaming

Basic Usage

// Create a Stream Buffer (2KB capacity, 1 byte trigger level)
StreamBufferHandle_t xStreamBuffer = xStreamBufferCreate(2048, 1);
// Send bytes from task context
size_t bytes_sent = xStreamBufferSend(
xStreamBuffer,
tx_data,
tx_length,
portMAX_DELAY
);
// Receive bytes from task context
size_t bytes_received = xStreamBufferReceive(
xStreamBuffer,
rx_buffer,
rx_length,
portMAX_DELAY
);
// Send from ISR
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xStreamBufferSendFromISR(
xStreamBuffer,
tx_data,
tx_length,
&xHigherPriorityTaskWoken
);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);

Internal Structure

Under the hood in FreeRTOS, Message Buffers are actually built on top of Stream Buffers. Stream Buffers serve as the foundational lock-free circular buffer primitive with the following structure:

  • Circular buffer: Raw byte storage
  • Write index (xHead): Next write position
  • Read index (xTail): Next read position
  • Space/full tracking: Standard circular buffer semantics
  • Trigger level: Configurable bytes required before unblocking a receiving task

The trigger level is particularly useful - you can configure a Stream Buffer to only unblock a receiving task when at least N bytes are available, reducing task wake-up frequency for byte-stream processing.

DMA Integration: High-Efficiency Data Transfer

Both buffer types excel when combined with DMA for high-efficiency data transfer between peripherals and tasks.

UART Receive with DMA + Stream Buffer

// Global handles
StreamBufferHandle_t xUartRxStream;
DMA_HandleTypeDef hdma_usart2_rx;
// Stream Buffer for UART RX (4KB, 1 byte trigger level)
xUartRxStream = xStreamBufferCreate(4096, 1);
// UART IDLE line interrupt (detects frame end)
void USART2_IRQHandler(void)
{
if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE)) {
// Clear IDLE flag
__HAL_UART_CLEAR_IDLEFLAG(&huart2);
// Calculate bytes received via DMA
uint16_t bytes_received = UART_RX_BUFFER_SIZE -
__HAL_DMA_GET_COUNTER(&hdma_usart2_rx);
// Send bytes to Stream Buffer from ISR
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xStreamBufferSendFromISR(
xUartRxStream,
uart_rx_buffer,
bytes_received,
&xHigherPriorityTaskWoken
);
// Restart DMA for next reception
HAL_UART_Receive_DMA(&huart2, uart_rx_buffer, UART_RX_BUFFER_SIZE);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
// UART RX Task
void uart_rx_task(void *param)
{
uint8_t rx_buffer[128];
size_t bytes_received;
while (1) {
// Block until at least 1 byte available (or use higher trigger level)
bytes_received = xStreamBufferReceive(
xUartRxStream,
rx_buffer,
sizeof(rx_buffer),
portMAX_DELAY
);
// Process received bytes (could be any number 1-128)
process_uart_data(rx_buffer, bytes_received);
}
}

Variable-Length SPI Packet Reception

// Message Buffer for dynamic SPI packets
MessageBufferHandle_t xSpiPacketMsg;
uint8_t spi_rx_buffer[MAX_SPI_PACKET];
// SPI receive complete ISR
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi->Instance == SPI1) {
// Calculate dynamic packet length (e.g., from header)
size_t packet_length = calculate_packet_length(spi_rx_buffer);
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// Send the entire packet as one discrete message
xMessageBufferSendFromISR(
xSpiPacketMsg,
spi_rx_buffer,
packet_length,
&xHigherPriorityTaskWoken
);
// Setup next reception
HAL_SPI_Receive_DMA(&hspi1, spi_rx_buffer, MAX_SPI_PACKET);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}

Performance Characteristics

Memory Efficiency

Both buffer types are highly memory-efficient for variable-length streams:

  • Structure Overhead: ~32-40 bytes for the buffer control structure (StreamBuffer_t)
  • Per-message overhead: Stream Buffers have zero per-message overhead. Message Buffers add a sizeof(size_t) overhead per message, whereas Queues waste memory if items are variable length (since Queue items are fixed-size).
  • Static allocation option: Can be statically compiled for deterministic memory use

Throughput and Latency

Stream and Message Buffers are significantly faster than Queues for bulk data transfer due to lower per-operation overhead (no item-size multiplication, no linked-list management). The exact throughput depends on:

  • CPU clock speed and core architecture (e.g., Cortex-M4 vs Cortex-M7)
  • Compiler optimization level (-O2 vs -Os)
  • Memory region (TCM/SRAM vs external SDRAM with wait states)
  • Transfer chunk size (larger chunks amortize per-call overhead)
  • RTOS tick rate (affects blocking granularity)

Configuration Options

Buffer Creation Parameters

// Message Buffer: xMessageBufferCreate(size_t xBufferSizeBytes)
// Stream Buffer: xStreamBufferCreate(size_t xBufferSizeBytes, size_t xTriggerLevelBytes)
// Example: Stream Buffer with 64-byte trigger level
xStreamBuffer = xStreamBufferCreate(1024, 64); // Unblock when ≥64 bytes available

Trigger Level Strategy (Stream Buffers Only)

  • Low trigger level (1-16 bytes): Low latency, high task wake-up frequency
  • Medium trigger level (32-128 bytes): Balanced latency/throughput
  • High trigger level (256+ bytes): Higher latency, fewer task wake-ups
  • Application-specific: Set based on processing chunk size (e.g., audio frame size)

Memory Allocation

Both support static and dynamic allocation:

  • Dynamic: xMessageBufferCreate() / xStreamBufferCreate() (uses heap)
  • Static: xMessageBufferCreateStatic() / xStreamBufferCreateStatic() (user-provided storage)

Common Use Cases

Audio Processing Pipeline

I2S Peripheral
↓ DMA
Stream Buffer (ISR → Task)
Audio Processing Task
Message Buffer (Task → Task)
USB Audio Class Task

Sensor Data Logging

Multiple Sensors
↓ (SPI/I2C/UART)
Stream Buffers (Per-Channel ISR → Task)
Data Aggregation Task
Message Buffer (Task → File System Task)
SD Card Writing Task

Command/Response Protocol

UART Receive
↓ DMA
Stream Buffer (ISR → Parser Task)
Command Parsing Task
Message Buffer (Parser → Executor)
Command Execution Task
Message Buffer (Executor → Response Task)
UART Transmit (Task → ISR via DMA)

Best Practices and Gotchas

1. The Single Reader / Single Writer Rule

This is the most common pitfall when migrating from Queues to Buffers. Stream and Message Buffers intentionally omit the heavy critical sections required for multi-client access to achieve high throughput.

  • Never allow multiple tasks to write to the same buffer concurrently.
  • Never allow multiple tasks to read from the same buffer concurrently.
  • If multiple sensors (writers) must feed a single aggregation task (reader), you must wrap the xStreamBufferSend calls in a Mutex, or use a separate Stream Buffer for each sensor.

2. Buffer Sizing

  • Message Buffers: Size = ((largest message + sizeof(size_t)) × expected concurrent messages) + 1 byte (Note: FreeRTOS ring buffers require 1 empty byte to distinguish full from empty)
  • Stream Buffers: Size = (max burst size × safety factor) + 1 byte
  • Rule of thumb: Make buffers 2-4× larger than your expected maximum burst

3. ISR Usage Patterns

  • Always use *FromISR() APIs in interrupt context
  • Check return value for pdTRUE to see if a task was woken
  • Call portYIELD_FROM_ISR() or portEND_SWITCHING_ISR() when needed
  • Never use blocking versions (portMAX_DELAY) in ISRs

4. Error Handling

  • Check return values from send/receive functions
  • 0 return from receive means timeout occurred (when not using portMAX_DELAY)
  • Send functions return number of bytes actually sent (may be < requested on timeout)
  • Consider implementing timeout detection for stalled data streams

5. Memory Management

  • Prefer static allocation in safety-critical systems
  • Monitor buffer space availability during development
  • Consider implementing buffer overflow detection callbacks
  • Remember Stream Buffer trigger level affects when tasks wake up

6. Data Coherency

  • For DMA usage, ensure cache coherency if using cached memory regions
  • May need cache clean/invalidate operations before/after DMA transfers
  • Consider using non-cached memory regions for DMA buffers when possible

Comparison with Alternatives

vs Queues

FeatureMessage/Stream BuffersQueues
Thread SafetyStrictly 1:1 (Single Reader/Writer)M:N (Multiple Readers/Writers safe)
Memory overhead~32-40 bytes struct + buffer~76 bytes struct + (QueueLength × ItemSize)
Max item sizeLimited only by buffer sizeFixed queue item size
Data copyingAlways copies data into and out of bufferAlways copies data into and out of queue
Use caseLarge data streams, variable messagesSmall fixed-size items, discrete events

vs Ring Buffers (Manual Implementation)

FeatureFreeRTOS BuffersManual Ring Buffers
ConcurrencyStrictly 1:1 (Single Reader/Writer)Depends on implementation
ISR safetyDedicated FromISR APIsMust handle manually
Blocking/waitingBuilt-in (Direct to Task Notifications)Must implement manually
Priority inheritanceNot supported (Mutexes only)Not supported
PortabilityStandard across FreeRTOS platformsPlatform-specific
TestingWell-tested, community verifiedCustom implementation risk

Advanced Patterns

DMA Circular Buffer to Stream Buffer

Stream Buffers eliminate the need for complex application-level ping-pong buffers. A single Stream Buffer can continuously accept data from a circular DMA buffer.

// Circular ADC DMA buffer
#define ADC_DMA_BUFFER_SIZE 256
uint16_t adc_buffer[ADC_DMA_BUFFER_SIZE];
// Single Stream Buffer for continuous ADC data
StreamBufferHandle_t xAdcStream;
// DMA Half/Full Transfer callbacks (using circular DMA mode)
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// First half of circular buffer is ready
size_t bytes = (ADC_DMA_BUFFER_SIZE / 2) * sizeof(uint16_t);
xStreamBufferSendFromISR(xAdcStream, &adc_buffer[0], bytes, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// Second half of circular buffer is ready
size_t bytes = (ADC_DMA_BUFFER_SIZE / 2) * sizeof(uint16_t);
xStreamBufferSendFromISR(xAdcStream, &adc_buffer[ADC_DMA_BUFFER_SIZE / 2], bytes, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

Message Buffer for Variable-Length Network Packets

Message Buffers preserve message boundaries, making them perfect for network packets where each send is one complete packet.

void ethernet_rx_task(void *param)
{
uint8_t *rx_buffer;
size_t bytes_received;
// Allocate a max-size buffer for receiving complete messages
rx_buffer = pvPortMalloc(MAX_PACKET_SIZE);
configASSERT(rx_buffer);
while (1) {
// Read entire packet as a single message
bytes_received = xMessageBufferReceive(
xEthMsgBuffer,
rx_buffer,
MAX_PACKET_SIZE,
portMAX_DELAY
);
if (bytes_received > 0) {
// Process the complete packet
process_ethernet_packet(rx_buffer, bytes_received);
}
}
}

Configuration and Tuning

Buffer Size Guidelines

(Note: As mentioned in Best Practices, always add +1 byte to your final calculation)

  1. Stream Buffers for DMA:

    • Size = ((DMA block size) × (number of buffered blocks) × 2) + 1
    • Example: For 128-byte DMA blocks with 4-block buffering: (128 × 4 × 2) + 1 = 1025 bytes
  2. Message Buffers for Variable Messages:

    • Size = ((max_message_size + sizeof(size_t)) × max_concurrent_messages) + 1
    • Example: For 256-byte max messages with 8 concurrent (assuming 4-byte size_t): ((256+4) × 8) + 1 = 2081 bytes
  3. Stream Buffer Trigger Level:

    • Set to match your processing chunk size
    • For audio: Set to sample size × channels × frame duration
    • For logging: Set to typical log entry size
    • For protocol parsing: Set to minimum message size

Runtime Monitoring

// Check buffer status for debugging
size_t spaces_msg = xMessageBufferSpaceAvailable(xMsgBuf);
bool is_empty_msg = xMessageBufferIsEmpty(xMsgBuf);
// For Stream Buffers:
size_t spaces_stream = xStreamBufferSpacesAvailable(xStreamBuf);
size_t bytes_stream = xStreamBufferBytesAvailable(xStreamBuf);
bool is_empty_stream = xStreamBufferIsEmpty(xStreamBuf);
bool is_full_stream = xStreamBufferIsFull(xStreamBuf);
// Reset buffers (emergency use only)
xMessageBufferReset(xMsgBuf);
xStreamBufferReset(xStreamBuf);

Limitations and Considerations

Message Buffer Limitations

  • No message persistence: Messages are consumed when read
  • No message peeking: Cannot view message without consuming it
  • Fixed maximum message size: Limited by buffer size
  • No message prioritization: FIFO order only

Stream Buffer Limitations

  • No message boundaries: Application must implement framing if needed
  • No built-in timestamps: Must add externally if timing important
  • No message identification: Raw byte stream only

When NOT to Use

  • Small discrete commands: Consider queues with command structures
  • Complex message routing: Consider message queues or mailboxes
  • Need message persistence: Consider logging or file buffers
  • Require message broadcasting: Consider event groups with message passing

Integration with Other FreeRTOS Primitives

Combined with Event Groups

// Use Stream Buffer for data + Event Group for signaling
StreamBufferHandle_t xSensorData;
EventGroupHandle_t xSensorEvents;
#define DATA_NEW_BIT (1 << 0)
#define ALARM_BIT (1 << 1) // Example of a second event
// ISR: Sensor data arrival
void sensor_isr(void)
{
size_t bytes = read_sensor_fifo(sensor_buf, SENSOR_FIFO_SIZE);
// Send data to buffer. We pass NULL for pxHigherPriorityTaskWoken
// because the task is blocked on the Event Group, not the Stream Buffer.
xStreamBufferSendFromISR(xSensorData, sensor_buf, bytes, NULL);
// Signal that new data is available
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xEventGroupSetBitsFromISR(
xSensorEvents,
DATA_NEW_BIT,
&xHigherPriorityTaskWoken
);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// Processing Task
void sensor_task(void *param)
{
EventBits_t uxBits;
uint8_t buffer[256];
while (1) {
// Wait for data available OR an alarm event
uxBits = xEventGroupWaitBits(
xSensorEvents,
DATA_NEW_BIT | ALARM_BIT,
pdTRUE, // Clear on exit
pdFALSE, // Don't wait for all bits
portMAX_DELAY
);
if (uxBits & DATA_NEW_BIT) {
size_t len;
// CRITICAL: We must loop to drain the buffer completely!
// Since the event bit was cleared, if we don't read everything now,
// remaining bytes will be stranded until the next interrupt occurs.
do {
len = xStreamBufferReceive(
xSensorData,
buffer,
sizeof(buffer),
0 // Don't block - drain available bytes
);
if (len > 0) {
process_sensor_data(buffer, len);
}
} while (len > 0);
}
if (uxBits & ALARM_BIT) {
handle_alarm_condition();
}
}
}

Receive Timeout for Stalled Streams

The xTicksToWait parameter provides built-in timeout detection without requiring external software timers.

// Using Stream Buffer receive timeout to detect stalled streams
void stream_processing_task(void *param)
{
uint8_t rx_buffer[128];
const TickType_t xBlockTime = pdMS_TO_TICKS(2000); // 2 second timeout
while (1) {
size_t bytes_received = xStreamBufferReceive(
xUartStream,
rx_buffer,
sizeof(rx_buffer),
xBlockTime
);
if (bytes_received > 0) {
// Process received data
process_data(rx_buffer, bytes_received);
} else {
// Timeout occurred (0 bytes received in 2 seconds)
// Handle stalled stream - flush, reset, or error recovery
handle_stream_stall();
}
}
}

Conclusion

FreeRTOS Message Buffers and Stream Buffers provide efficient, high-throughput mechanisms for data transfer between tasks and interrupts. Their low memory overhead, ISR-safe APIs, and DMA-friendly design make them superior to traditional queues for large data transfers.

Choose Message Buffers when:

  • You need discrete, variable-length messages
  • Message boundaries are important
  • You’re transferring structured data or packets
  • You want length-encoded message storage

Choose Stream Buffers when:

  • You have a continuous byte stream
  • Message boundaries don’t matter or are handled externally
  • You’re streaming sensor data, audio, or protocol bytes
  • You want arbitrary chunk-sized reads/writes

Both primitives excel in scenarios involving DMA, high-speed communication, and real-time data processing where traditional queues would introduce excessive overhead or latency. By understanding their characteristics and best practices, you can design efficient data pipelines that maximize throughput while minimizing CPU overhead in your FreeRTOS-based embedded systems.

References

  1. FreeRTOS Kernel Documentation, Stream Buffer API Reference, https://www.freertos.org/Documentation/02-Kernel/04-API-references/08-Stream-buffers/00-RTOS-stream-buffer-API
  2. FreeRTOS Kernel Documentation, Message Buffer API Reference, https://www.freertos.org/Documentation/02-Kernel/04-API-references/09-Message-buffers/00-RTOS-message-buffer-API
  3. FreeRTOS Kernel Documentation, Stream and Message Buffer Introduction, https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/04-Stream-and-message-buffers/01-RTOS-stream-and-message-buffers
  4. Richard Barry, FreeRTOS Reference Manual, FreeRTOS.org
  5. STMicroelectronics, STM32F7xx Reference Manual RM0385, Section 12 (DMA)

Frequently Asked Questions

What is the difference between Message Buffers and Stream Buffers in FreeRTOS?

Message Buffers are designed for discrete, variable-length messages where each send/write operation corresponds to a complete message. Stream Buffers are designed for a continuous stream of bytes where the concept of individual messages doesn't exist - data can be read/written in any byte-aligned chunks.

When should I use Stream Buffers instead of Message Buffers?

Use Stream Buffers when you have a continuous data stream like audio samples, sensor data logging, or protocol byte streams where you don't need message boundaries. Use Message Buffers when you need to send discrete packets, commands, or structured data where each buffer operation represents a complete logical message.

Are Message Buffers and Stream Buffers interrupt-safe?

Yes, both provide dedicated ISR-safe APIs (xMessageBufferSendFromISR(), xStreamBufferReceiveFromISR(), etc.) that can be called from interrupt context. Note that these APIs internally use taskENTER_CRITICAL_FROM_ISR(), which briefly masks interrupts at or below configMAX_SYSCALL_INTERRUPT_PRIORITY. ISRs above that priority level must not call any FreeRTOS API.

Tags

freertosmessage-bufferstream-bufferrtoshigh-throughputisrdma

Share


Previous Article
Atomic Operations in Embedded C: Lock-Free Synchronization for Cortex-M
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

FreeRTOS Heap Implementations Compared: Heap_1 to Heap_5
FreeRTOS Heap Implementations Compared: Heap_1 to Heap_5
July 14, 2026
5 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media