Skip to main content

Linux message queues: interprocess communication (IPC) with POSIX and System V

A Linux message queue is a kernel-managed buffer that lets processes on the same machine hand each other discrete messages without blocking. The sender writes and moves on, each message keeps its boundaries instead of arriving as an undifferentiated byte stream, and the queue outlives the processes using it until something explicitly removes it.

Linux ships two unrelated implementations of the idea. This guide covers both, when to pick each, and the system calls you need to create, send, receive, and tear down a queue.

What Linux message queues are

The kernel owns the queue, so neither side needs shared memory or a lock of its own. A write succeeds as long as the queue is under its message limit, and a read blocks until something is there or returns immediately if you opened the queue non-blocking.

Types of Linux message queue

  1. POSIX message queues: The modern API, with priority-based ordering and queues identified by a name such as /queue_name. Compile with -lrt.
  2. System V message queues: The older API, identified by integer keys, with a per-message type field and finer control over permissions and resource limits.

Where they fit: dispatching work to processes

Consider a scheduler feeding several worker processes:

  • The scheduler writes task descriptions to the queue.
  • Workers read from the queue and execute tasks independently.
  • Tasks written while no worker is reading stay in the queue instead of being lost.

That is a background queue in its simplest form, scoped to a single kernel: it cannot reach a second machine and does not survive a reboot.

POSIX message queues

Step 1: Create and open a message queue

Include the necessary headers:

#include <fcntl.h>    // O_* constants
#include <sys/stat.h> // Mode constants
#include <mqueue.h> // Message queue functions
#include <stdio.h>
#include <stdlib.h>

Example Code:

#define QUEUE_NAME "/task_queue"
#define MAX_MSG_SIZE 256
#define MAX_MSG_COUNT 10
#define QUEUE_PERMISSIONS 0666

int main() {
// Define queue attributes
struct mq_attr attr;
attr.mq_flags = 0; // Blocking mode
attr.mq_maxmsg = MAX_MSG_COUNT; // Maximum number of messages
attr.mq_msgsize = MAX_MSG_SIZE; // Maximum message size
attr.mq_curmsgs = 0; // Number of messages currently in the queue

// Create the message queue
mqd_t mq = mq_open(QUEUE_NAME, O_CREAT | O_RDWR, QUEUE_PERMISSIONS, &attr);
if (mq == (mqd_t)-1) {
perror("mq_open");
exit(EXIT_FAILURE);
}

printf("Message queue created: %s\n", QUEUE_NAME);
mq_close(mq);
return 0;
}

Step 2: Send messages to the queue

The last argument to mq_send is a priority. POSIX queues deliver higher priorities first, which makes them a priority queue without any work on your part.

Example Code (Producer):

#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
mqd_t mq = mq_open(QUEUE_NAME, O_WRONLY);
if (mq == (mqd_t)-1) {
perror("mq_open");
exit(EXIT_FAILURE);
}

// Message to send
char message[MAX_MSG_SIZE] = "Task 1: Process file";
unsigned int priority = 5; // Higher priority = processed first

if (mq_send(mq, message, strlen(message) + 1, priority) == -1) {
perror("mq_send");
exit(EXIT_FAILURE);
}

printf("Message sent: %s\n", message);
mq_close(mq);
return 0;
}

Step 3: Receive messages from the queue

Example Code (Consumer):

#include <mqueue.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
mqd_t mq = mq_open(QUEUE_NAME, O_RDONLY);
if (mq == (mqd_t)-1) {
perror("mq_open");
exit(EXIT_FAILURE);
}

char buffer[MAX_MSG_SIZE];
unsigned int priority;

// Receive a message
if (mq_receive(mq, buffer, MAX_MSG_SIZE, &priority) == -1) {
perror("mq_receive");
exit(EXIT_FAILURE);
}

printf("Received message: %s (priority: %u)\n", buffer, priority);
mq_close(mq);
return 0;
}

Step 4: Clean up resources

Remove the message queue when it is no longer needed. The name lives in /dev/mqueue until you unlink it, so a queue left behind by a crashed process still holds its messages:

int main() {
if (mq_unlink(QUEUE_NAME) == -1) {
perror("mq_unlink");
exit(EXIT_FAILURE);
}

printf("Message queue deleted: %s\n", QUEUE_NAME);
return 0;
}

System V message queues

Step 1: Create a message queue

System V message queues are identified by keys and managed with an ID. Include the headers:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>

Example Code:

#define QUEUE_KEY 1234
#define PERMISSIONS 0666

int main() {
// Create a message queue
int msgid = msgget(QUEUE_KEY, IPC_CREAT | PERMISSIONS);
if (msgid == -1) {
perror("msgget");
exit(EXIT_FAILURE);
}

printf("Message queue created with ID: %d\n", msgid);
return 0;
}

Step 2: Send messages to the queue

System V messages include a type field for categorizing messages. There is no priority, but a consumer can ask for a specific type, so the field doubles as a cheap routing key.

Example Code (Producer):

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MSG_SIZE 256

struct message {
long type; // Message type
char text[MSG_SIZE]; // Message content
};

int main() {
int msgid = msgget(QUEUE_KEY, 0);
if (msgid == -1) {
perror("msgget");
exit(EXIT_FAILURE);
}

struct message msg;
msg.type = 1; // Type 1 message
strcpy(msg.text, "Task 1: Process file");

if (msgsnd(msgid, &msg, sizeof(msg.text), 0) == -1) {
perror("msgsnd");
exit(EXIT_FAILURE);
}

printf("Message sent: %s\n", msg.text);
return 0;
}

Step 3: Receive messages from the queue

Example Code (Consumer):

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>

#define MSG_SIZE 256

struct message {
long type; // Message type
char text[MSG_SIZE]; // Message content
};

int main() {
int msgid = msgget(QUEUE_KEY, 0);
if (msgid == -1) {
perror("msgget");
exit(EXIT_FAILURE);
}

struct message msg;

// Receive a message of type 1
if (msgrcv(msgid, &msg, sizeof(msg.text), 1, 0) == -1) {
perror("msgrcv");
exit(EXIT_FAILURE);
}

printf("Received message: %s\n", msg.text);
return 0;
}

Step 4: Clean up resources

Remove the queue using msgctl. Unlike POSIX queues, there is nothing in the filesystem to remind you it exists, so use ipcs -q to find stragglers:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int msgid = msgget(QUEUE_KEY, 0);
if (msgid == -1) {
perror("msgget");
exit(EXIT_FAILURE);
}

// Remove the queue
if (msgctl(msgid, IPC_RMID, NULL) == -1) {
perror("msgctl");
exit(EXIT_FAILURE);
}

printf("Message queue deleted.\n");
return 0;
}

Choosing between POSIX and System V

FeaturePOSIX Message QueuesSystem V Message Queues
NamingNamed with a stringIdentified by key
Priority SupportYesNo
Ease of UseSimple APIMore complex API
ScalabilityHigherLower
StandardizationPOSIX standardLegacy System V

Use POSIX for anything new. The names are readable, the descriptors work with select and poll, and mq_notify signals you when a message arrives instead of making you block a thread. Reach for System V only in a codebase that already uses it. The POSIX message queue guide goes deeper on that API.

Practices worth following

  1. Clean up resources: Always unlink or remove queues after use to avoid resource leaks.
  2. Error handling: Check return values of system calls for proper error handling.
  3. Priority handling: Use message priorities to process critical tasks first (POSIX).
  4. Watch the limits: /proc/sys/fs/mqueue/msg_max and msgsize_max cap POSIX queues, and mq_open fails rather than growing past them.

When a kernel queue is not enough

Both APIs stop at the machine boundary. The moment producers and consumers live on different hosts you need a distributed message queue or a message broker instead, which is where RabbitMQ, Kafka, and SQS come in. And if the consumer belongs to another organization entirely, no queue reaches it: you deliver over HTTP with webhooks, and Svix handles the retries and signatures that outbound delivery needs.