How Long Can You Go On Using MSMQ? — The Migration Decision for a Legacy Queue That Isn't Even "Deprecated"

· Updated: · · Windows, .NET, C#, MSMQ, Message Queue, Legacy Technology, Migration, Information Systems

Revision history (first version, published Jul 29, 2026)
First published
Cite this article(DOI (registered archive): 10.5281/zenodo.22170812)

The DOIs below refer to previously archived versions and may not match the current text. Use this page’s URL to reference the current text.

Go Komura (2026). How Long Can You Go On Using MSMQ? — The Migration Decision for a Legacy Queue That Isn't Even "Deprecated". KomuraSoft LLC. https://comcomponent.com/en/blog/msmq-migration-decision-guide/

DOI (registered archive)
10.5281/zenodo.22170812
DOI (last registered version)
10.5281/zenodo.22170813

“I heard MSMQ has been discontinued. Does the system we have running today have to be migrated right away?” — when you maintain an existing system, this is the first thing that has to be pulled apart.

Whether MSMQ (Microsoft Message Queuing) survives as an OS feature and whether there is an API for using it from .NET are two separate questions. As of July 2026, the reference point for this article, MSMQ does not appear on the official deprecated-features lists and remains an optional Windows feature. The standard library System.Messaging, on the other hand, is .NET Framework only and was never ported to .NET (Core and later).123

So the problem is not that MSMQ will stop working tomorrow. It is that the queue implementation becomes the obstacle when you move the app to .NET.

This article is written for the developers who maintain and migrate systems that use MSMQ, and for the information systems departments that set the policy. It starts by establishing the facts and the conditions for continued use, then works through the inventory, the choice of migration target, the implementation of a table queue, and the cutover procedure.

For migration from VB6 or from .NET Framework as a whole, see “Practical VB6-to-.NET Migration” and “Pre-Migration Checklist for Moving from .NET Framework to .NET”. This article narrows in on the queue.

1. The Bottom Line First: Separate the OS’s Survival From the App’s Migration

If you are moving the app to .NET, the rule is to migrate the queue along with it. If you will keep running on .NET Framework for the time being, you can choose to continue using MSMQ once you have checked the OS support period and the operational conditions. Adopting MSMQ in a new project is not recommended.4

Read from whatever you need to decide now

What you need to decide now Where the decision starts Where it is covered
Is the “it has been discontinued” story true Separate the state of the OS feature from the state of the managed API Chapter 1: The facts
Can we keep using it as-is for now Check the OS support period, and whether you can put monitoring, recovery and handover in place Chapter 2: Conditions for continued use
We want to replace it together with the .NET migration Take inventory of dependencies, formats, DTC and offline tolerance Chapter 3: Inventory, Chapter 4: Migration targets
Can we keep it alive with P/Invoke or CoreWCF Separate being able to call it from being able to take on the maintenance burden Section 1.3: The managed API and P/Invoke, Section 1.4: CoreWCF
RabbitMQ or Azure Service Bus Before that choice, consider whether a table queue in the same business database is enough Chapter 4: Choosing by requirement, Chapter 5: The table queue
We want to bring the dequeue SQL into our own database Check the isolation level, the snapshot setting and the lock hints Section 5.3: SQL and database settings
We need to preserve the queue’s processing order Separate the dequeue order from the order in which business data is updated Section 5.4: Parallelism and ordering
We want to put the worker into production Check the transaction boundary, and the reclaim, retry and cleanup work Section 5.5: The worker, Section 5.6: Operations
How do we cut over a system that is live Decide the message format, the receiving side, the duplicate handling and what happens to leftover messages Chapter 6: Cutover procedure

If you read the article straight through, the order is confirm what survives → conditions for continued use → inventory → migration target → implementation → cutover. When you revisit the decision later, see the summary in Chapter 7.

1.1. Confirm Which Layer the Question Is About

The answer to “can MSMQ still be used” differs by layer, as follows. The reference point for these states is July 2026, the same as at the top of the article.

Layer Current state What it means in practice
OS feature (a Windows optional feature) Alive. It ships with current Windows client and Windows Server releases, and no deprecation has been declared12 Enable it and it still runs. You can keep using it for as long as the OS is supported
Win32 native API (MQSendMessage and the like) Documented and available5 The route of calling it from .NET via P/Invoke stays open. But you end up writing and maintaining the formatters and transaction integration yourself
System.Messaging in .NET Framework Available, but only for .NET Framework 1.1 through 4.8.13 This is where existing systems run. There is nothing beyond it
The official managed API on .NET (Core and later) Does not exist. It is not in the Windows Compatibility Pack either6 The moment you move the app up to .NET, the queue part has to be rebuilt
WCF’s MSMQ binding → CoreWCF.MSMQ A community-led port exists. CoreWCF itself has a support policy, but the MSMQ implementation depends on a community port of System.Messaging7 Usable to extend the life of a WCF service invoked through a queue (the receiving side), but it is neither a substitute for the sending side nor a general-purpose queue API
New adoption Not recommended Because there is no official path forward

1.2. What Has Not Been Deprecated Is the OS Feature

The Windows client’s “Deprecated features” list carries entries such as NTLM, VBScript and WordPad, but there is no entry for MSMQ. The same holds for Windows Server’s “Features Removed or No Longer Developed”, including the Windows Server 2025 tab.12

Deprecated is an official stage indicating that active development has ended and that the feature may be removed in a future update. It is distinct from removed. For MSMQ, what this article confirmed is that not even that deprecation has been declared.

1.3. What Blocks Migration to .NET Is the Managed API

The reference for System.Messaging.MessageQueue covers .NET Framework 1.1 through 4.8.1; there is no edition for .NET (Core and later). The Windows Compatibility Pack (Microsoft.Windows.Compatibility) supplies roughly 20,000 APIs, covering areas such as the registry, WMI, Windows services and EventLog, but it does not include System.Messaging.36

If You Keep It Alive With P/Invoke, You Take On Maintaining the Wrapper

The native API has not gone away either. Calling Win32 APIs such as MQSendMessage and MQReceiveMessage from .NET via P/Invoke is possible in itself. But you will be writing and maintaining your own wrapper for the formatters and transaction integration that System.Messaging used to provide. This is not the main route for a migration; it is a way of extending the life of MSMQ when you absolutely have to keep it.5

1.4. Judge CoreWCF as a Route for the WCF Receiving Side

WCF in .NET Framework had a binding that uses MSMQ. As a route for hosting that on .NET, there is CoreWCF’s MSMQ transport (CoreWCF.MSMQ).7

Its purpose, though, is the receiving side of a WCF service invoked through a queue. It is neither a general-purpose queue API that replaces System.Messaging nor a substitute for the sending-side client.

CoreWCF itself has a Microsoft support policy. The MSMQ transport implementation, however, depends on a community port of the .NET Framework version of System.Messaging, and that dependency cannot be treated as carrying the same guarantee. Adopt it only after checking whether the version you use falls under support, and who will maintain the dependent piece.7

So “MSMQ has already been discontinued” and “it cannot be moved to .NET as-is” do not mean the same thing. Choosing P/Invoke or CoreWCF defers replacing the queue at the price of taking on the maintenance burden of that route.

In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (28 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle

2. Whether to Keep Using It: Decide From the .NET Migration Plan and Your Operations Setup

2.1. Why “It Works” Is Not Enough to Decide On

.NET Framework 4.8 is supported as a Windows component, along the lifecycle of the OS it is installed on. You can therefore plan to keep it running within that support period.4

What you want to establish is not only whether it runs. If a dependency on MSMQ keeps the whole app on .NET Framework, the following maintenance and migration burdens remain as well.

Burden Effect on the migration decision
Securing maintainers and handing the system over Fewer and fewer engineers can explain System.Messaging and DTC, so the cost of investigating the configuration from scratch goes up
Runtime and library constraints Some new C# syntax is available with nothing more than a compiler update, but runtime performance improvements and new standard libraries are out of reach. More and more packages drop .NET Framework as a target
Dependence on DTC If you leave the part that commits “dequeue from the queue” and “update the database” together, you modernize only the periphery and the hardest problem is left until last
Old serialization formats The BinaryFormatter implementation was removed from the runtime in .NET 9, so the migration has to cover the format as well8

The problem of losing the maintainer is also covered in “Maintaining a System With No Source Code and No Documentation”. MSMQ inquiries tend to begin with a migration estimate rather than with a failure precisely because what causes trouble is this kind of dependency and handover, rather than the behavior itself.

“It runs for as long as the OS supports it” and “the longer you wait, the easier migration gets” are two different claims. Even when you are not migrating for now, take inventory of the hard parts up front.

2.2. If You Are Not Migrating for Now, Meet Four Minimum Conditions

Having no plan to move the app up to .NET and leaving it unchanged for the time being on cost-benefit grounds — what is often called mothballing — is reasonable under conditions. The premise, though, is that all four of the following are met.

Check Minimum condition What to do concretely What happens if you do not
State MSMQ explicitly in the kitting and restore procedures MSMQ is a Windows optional feature. Write the steps for enabling it through Turn Windows features on or off, or through DISM or PowerShell, into the environment build manual Someone forgets to enable it when a PC or server is replaced, and on the day of the switch nothing works for no apparent reason
Monitor queue length Put threshold monitoring and notification on the backlog count. Include the dead-letter queue and the journal queue as well When the receiving side stops, nothing raises an error and messages simply pile up, so the business grinds to a halt with nobody noticing
Document the configuration Record the queue paths, the permissions, whether transactions are used, the formatter and the journal settings (the inventory table in Section 3.2 can be used as-is) A future migration estimate has to start over from investigation, and both its accuracy and its effort suffer
Review the decision once a year Check annually whether it has appeared on a deprecation list, and whether an OS update has changed its behavior You end up scrambling only after the deprecation notice is published

The annual review in particular is the procedure that keeps a deprecation notice or the effect of an OS update from slipping past you. In case the person responsible transfers away, leave behind not only the configuration but the recovery procedure as well. Keeping the status quo without filling in these conditions carries the risk that nobody will know either that it stopped or why.

3. Inventory: Record What You Have Delegated to MSMQ

3.1. Get the Features and the Terminology Straight

With MSMQ, the sender writes a message to a queue, and an app on the same machine or on a different machine dequeues it whenever it suits. There are mainly three properties that have to carry over to the migration target.

It Accumulates Even When the Destination Is Down: Store-and-Forward

Even when the destination is down, messages accumulate locally and are delivered once it recovers. That helps on unreliable site-to-site links, but it does not unconditionally guarantee durability across a restart.

It Commits the Queue and the Database Update Together: Transactions

Queue operations can be made transactional, and with MS DTC (the Distributed Transaction Coordinator) the dequeue from the queue and the database update can be committed in a single distributed transaction.

It Was Usable Without Installing Extra Middleware: Shipped With the OS

Because no additional middleware had to be installed, it was widely used in the 2000s for order-processing integration, asynchronous report generation, and integration between manufacturing steps on the factory floor. The combination of these three properties is why it is still around.

Distinguish Accumulation, Durability and Transactions by Name

The terms used in the decision tables are settled here as well.9

Term What it means, and why you check it
Private queue / public queue A private queue is registered only on the local computer and is not published to Active Directory. It is addressed in a form such as .\private$\QueueName. A public queue is registered in the directory and can be located across the domain. In small and medium-sized business systems, private queues are the common case
Express message The default send mode. It is held in memory both in transit and after delivery, so it is fast, but it is lost if that computer or the Message Queuing service stops
Recoverable message Held on disk on the sending computer, on each relaying computer and at the destination queue, so it survives a restart. The sender specifies it explicitly
Transactional queue Handles transactional messages only. The attribute is fixed when the queue is created and cannot be changed afterwards. Transactional messages are persisted to disk
DTC The Windows service that coordinates a transaction spanning several resources, such as MSMQ and a database. How to replace the consistency between queue and database is the crux of the migration
Journal queue / dead-letter queue System queues that MSMQ generates. The former keeps copies of sent and dequeued messages, the latter holds messages that could not be delivered. Monitor the growth in capacity that comes from leaving them alone

“It can accumulate while the receiver is down” and “it survives a restart of MSMQ or of the machine” are different properties. Investigate separately whether you rely on the former alone, or whether you also need the latter through recoverable or transactional messages.

3.2. Draw the Dependencies Out of Code, Configuration and Operations

Do Not Let the Inventory End at Library References

Start by looking for references to System.Messaging and for the places where MessageQueue is constructed. The absence of such a reference, however, is not by itself grounds for concluding that MSMQ is not in use. Check the call routes separately.

Call route What to look for in code and configuration
.NET Framework library References to System.Messaging, the places where MessageQueue is constructed
WCF configuration netMsmqBinding / msmqIntegrationBinding
Native API and COM MQ* native functions such as MQSendMessage, and calls made through COM

Record, per Queue, the Basis for the Migration Decision

Use the following table to record what you find, one row per queue. The point is not to check and be done with it, but to leave the results behind as the basis for the migration policy and for the internal approval documents.

Aspect Where to look What to record How it bears on the decision
(a) Queue path The path string passed to MessageQueue, the connection target in configuration files, FormatName: specifications Local or remote, private or public, the name of the peer machine If it is remote or site-to-site, decide whether store-and-forward is required (the “offline tolerance” row in Section 4.2). If it is entirely local, it falls into the first row of Section 4.2
(b) Transactions and Recoverable Where the queue is created (is it a transactional queue), whether Recoverable is specified on send, whether DTC is used Transactional queue or not, whether Recoverable is specified, whether DTC takes part If DTC is in use, a table queue is all but settled as the migration target. If neither is in use, state explicitly that the current system is operated on the premise that messages are lost on restart
(c) Formatter Where XmlMessageFormatter / BinaryMessageFormatter / ActiveXMessageFormatter are specified The formatter in use, and the type of the message body If it is BinaryFormatter-based, replacing it with JSON is mandatory (Section 6.1). This becomes the main driver of the migration effort8
(d) Journal and dead-letter queues The queue properties (is the journal enabled), the contents of the system queues, the operations manual Whether the journal is used, whether anyone actually looks at the dead-letter queue How failed messages are handled becomes a design requirement for the migration target (a parking table, in the case of a table queue)
(e) Who creates and deletes them The installer, the deployment scripts, the Create call at app startup Who creates them, and who configures the permissions At migration time this turns directly into “who provisions the new queues”. If you choose to mothball, transcribe it into the kitting procedure (Chapter 2)

4. Migration Targets: Choose by the Properties You Need, Not by a Successor Product Name

4.1. Sort Out the Requirements With Four Questions

From the results of the inventory, establish the following four points.

  1. Is the receiving side a process that updates your own business database?
  2. Are the transactional queue and the database update committed together through DTC?
  3. Are the sender and the receiver on different machines or at different sites? Is store-and-forward actually in use?
  4. How large is the message volume? At the scale of a few thousand to a few tens of thousands a day, which is typical of many business systems, the throughput of the candidates alone is unlikely to settle the choice.

4.2. The First Choice for Each Property

For an asynchronous job that updates the business database of the same system, look at a table queue first. Choose a broker when there is a requirement a table queue cannot meet, such as delivery to several systems or routing.

Property in use First choice Reasoning and cautions
Asynchronous processing within a single system (the receiver updates the database) A database table queue “Dequeuing the message” and “updating the business data” can be committed in the same local transaction, so DTC is no longer needed. The existing backup, monitoring and operations apply unchanged. On the sending side, too, “updating the business data and inserting the message row” can go into one transaction, which is what is known as the outbox pattern
DTC makes the queue and the database update atomic A database table queue Replacing the distributed transaction with a local transaction is the essence of this migration. Cloud queue services cannot take part in DTC, so as long as this requirement exists, a broker is a detour
Loosely coupled integration across several systems and several languages RabbitMQ (on-premises) / Azure Service Bus (can live in the cloud) Fan-out delivery and routing are what brokers are good at. With RabbitMQ, budget for the load of running it yourself (redundancy and updates)
Offline tolerance between sites (store-and-forward) Azure Service Bus or similar plus a resend design, or a table queue at each site plus synchronization There are few mechanisms that transparently replace MSMQ’s “hold it locally on the sender and deliver it later”. Change the design so that the responsibility to hold messages on the sending side is explicitly given to the application
Producer and consumer inside a single process An in-memory queue such as .NET Channels A case where an inter-process queue was never needed in the first place. Keep it inside the process, and move to a table queue if durability is required
Used only as inter-process communication between Windows services IPC such as named pipes But this replacement holds only when both sides are always running at the same time. MSMQ accepts a send while the receiver is stopped (even for express messages) and delivers it later, whereas a pipe fails immediately. If you rely on accumulation while the receiver is down, either implement resend and buffering in the application or keep a queue. For how to choose, see “A Decision Table for Windows IPC

Why a Table Queue Comes Up First

“Look at a table queue first” here is not a wish to recommend a database for every purpose. It is that for the asynchronous processing that updates the same database, which is common in MSMQ-era business systems, consistency stays simple and the existing backup, monitoring and operations can be put to use.

Requirements That Suit a Broker, and What You Have to Design on Top

RabbitMQ and Azure Service Bus suit loosely coupled delivery across several systems and several languages, fan-out and routing. They are also worth considering when high throughput is required. Because the queue and the business database become separate resources, however, do not carry the atomicity of MSMQ plus DTC across as-is: design idempotency and resend in the application. Include monitoring, redundancy, patching and staff training for the new middleware in the estimate.

Migrating Does Not Always Mean the Accumulation While Stopped Can Be Dropped

Check whether offline tolerance and accumulation while the receiver is stopped are requirements you are allowed to drop. Choosing a cloud queue does not automatically bring local accumulation on the sending side with it, and switching to named pipes does not preserve waiting while the peer is stopped in the same form. If you need it, give the application resend and buffering, or keep a queue.

5. The Table Queue: Commit the Queue Operation and the Business Update in the Same Database

This chapter starts with what it means to bring both into one database. From there it looks in turn at the premises of the dequeue SQL, how far ordering is guaranteed, the worker’s transaction and the processing you add for production.

5.1. Why DTC Becomes Unnecessary

With MSMQ, the queue and the database are separate resources. Committing “dequeued from the queue” and “updated the business data” together required a distributed transaction through DTC.

Make the queue a table in the same database as the business data and both become updates to that one database. The dequeue, the business update and the completion record can be committed in a single local transaction. Replacing a distributed transaction with a local one is the heart of this migration.

Not Only the Receiving Side: The Sending Side Can Commit at the Same Time

On the sending side as well, the business data update and the INSERT of the message row can go into the same transaction. This is the outbox pattern, and it prevents the inconsistency where the business data has been updated but no message went out.

What follows is the minimal form on SQL Server. The C# part is pseudocode that shows the transaction boundary; it is not a finished implementation to be deployed as-is.

5.2. The Table That Stores the Jobs

CREATE TABLE dbo.JobQueue (
    Id          BIGINT IDENTITY(1,1) PRIMARY KEY,
    Payload     NVARCHAR(MAX) NOT NULL,                    -- The body. Held as JSON
    Status      TINYINT       NOT NULL DEFAULT 0,          -- 0: pending, 1: in progress, 2: done
    EnqueuedAt  DATETIME2     NOT NULL DEFAULT SYSUTCDATETIME(),
    StartedAt   DATETIME2     NULL,                        -- Used to reclaim rows left in progress by a crash
    RetryCount  INT           NOT NULL DEFAULT 0
);
CREATE INDEX IX_JobQueue_Status ON dbo.JobQueue (Status, Id);

Payload holds the body as JSON, and Status distinguishes pending, in progress and done. StartedAt and RetryCount are used for the reclaim and retry operations.

5.3. Dequeuing One Row, and Checking the Database Settings

Update the Row You Take and Fetch Its Body in a Single SQL Statement

To dequeue, update one row to “in progress” while receiving the body through OUTPUT. READPAST skips past candidates that another worker holds a row lock on instead of waiting for them, which lets several processes share the work.10

WITH next_job AS (
    SELECT TOP (1) *
    -- READCOMMITTEDLOCK is required on a database where READ_COMMITTED_SNAPSHOT is ON (see below).
    -- On a database where it is OFF it matches the default behavior, so leaving it in works for both
    FROM   dbo.JobQueue WITH (READPAST, UPDLOCK, READCOMMITTEDLOCK)
    WHERE  Status = 0
    ORDER  BY Id          -- Dequeue in insertion order. This is what makes it a queue
)
UPDATE next_job
SET    Status = 1, StartedAt = SYSUTCDATETIME()
OUTPUT inserted.Id, inserted.Payload;

Check the Snapshot Setting and the Isolation Level First

When you read this SQL, check READ_COMMITTED_SNAPSHOT and the session’s isolation level.

The official documentation states that when the database’s READ_COMMITTED_SNAPSHOT is ON and either the session is at READ COMMITTED or the query also uses the READCOMMITTED hint, READPAST cannot be specified as it stands. The remedy is to remove the READCOMMITTED hint if one is present, and to include READCOMMITTEDLOCK.10

The default isolation level is READ COMMITTED, so carrying this into a snapshot-enabled database without precautions does not merely block — the SQL statement fails with an error. Azure SQL Database has it ON by default. On-premises it may also have been enabled to relieve read blocking, so check before you start.

SELECT is_read_committed_snapshot_on FROM sys.databases WHERE name = DB_NAME();

The dequeue SQL above includes READCOMMITTEDLOCK and therefore reads with locks regardless of the snapshot setting. On a database where the setting is OFF, that matches the default behavior.10

Why ROWLOCK Is Not Listed Alongside, and the Limits of READPAST

Unlike the commonly seen WITH (READPAST, UPDLOCK, ROWLOCK), ROWLOCK is not listed alongside here. That is because ROWLOCK and READCOMMITTEDLOCK belong to the same granularity hint group, and both cannot be specified for the same table.10

READPAST can skip row locks only; it cannot skip page locks. Within the scope of this example, which takes a single row through a TOP (1) index seek, escalation is not a practical concern, but if you want ROWLOCK spelled out, confirm that READ_COMMITTED_SNAPSHOT is OFF and then drop READCOMMITTEDLOCK instead.

5.4. The Dequeue Order and the Order of Updates to Business Data Are Different Things

Specify an Order for the Candidate Rows

If you drop the ORDER BY and write UPDATE TOP (1), which row is picked is undefined. TOP on an UPDATE does not order the rows it touches, and merely having an index on (Status, Id) is no guarantee of order either. To keep old jobs from being pushed back indefinitely, the example specifies ORDER BY Id.11

Under Parallel Processing, First Out Does Not Mean First Applied

That said, the completion order under parallel processing is not lined up. While worker A is processing job 1, worker B skips that row through READPAST and can commit job 2 first.

What is lined up What is not
Which row is dequeued first (ORDER BY Id) Which row is applied to the business data first

When the order of application carries meaning — updates against the same order number, for instance — choose one of the following.

How to preserve order What you gain, and what you trade for it
Run a single consumer Give up parallelism to get ordering. The simplest option if it still meets the throughput you need
Partition by key Pin a worker to an order number or similar key, or add a condition that skips a row while the same key is in progress. What is guaranteed is the order within a key, not the order across keys

If you choose neither, state explicitly in the design that global FIFO is not guaranteed under parallel processing. The same problem arises with MSMQ as soon as you receive in parallel. What matters is not migrating while still believing that “it is a queue, so it comes out in order.”

5.5. The Worker’s Transaction Boundary

What you line up on the receiving side is the transaction covering the dequeue, the business data update and the completion record. In the pseudocode below, the three operations are handed the same connection and transaction, and are committed together at the end.

while (!stoppingToken.IsCancellationRequested)
{
    using var tx = connection.BeginTransaction();

    var job = DequeueOne(connection, tx);          // The UPDATE ... OUTPUT above
    if (job is null)
    {
        tx.Commit();
        await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);  // Nothing there: wait one polling interval
        continue;
    }

    ApplyBusinessData(connection, tx, job);        // <- Update the business data (the work you actually wanted)
    MarkDone(connection, tx, job.Id);              // Status = 2

    tx.Commit();   // "Dequeue" and "business update" are committed together by this one line
}

The key point is that DequeueOne, ApplyBusinessData and MarkDone use the same connection and transaction, and that the final Commit commits the dequeue and the business update together. If you replace this with a broker, this same-database property is lost, and a different consistency design becomes necessary.

5.6. Reclaim, Retry and Cleanup to Add for Production

On top of the minimal example, design the following operations.

Operation What it does
Reclaiming rows left in progress A recovery job that returns rows to Status = 0 when Status = 1 and StartedAt is older than a set interval
Retry limit and parking Move rows whose RetryCount exceeds the limit to a separate table, or to something like Status = 9. This is the place that corresponds to MSMQ’s dead-letter queue
Cleaning up completed rows Periodically delete or archive rows with Status = 2 to keep the table and its indexes from bloating

In small and medium-sized business systems, a table plus polling, or a notification-driven variant, is often enough. Before adding another queue product, check whether this implementation already satisfies the properties and the operations you need.

6. Cutover: Line Up the Format, the Behavior and the Receiving Side First

For the cutover, first line up the message format and the tests over inputs and results. After that, choose between bridging old and new and draining the old queue before switching.

6.1. Decide a Message Format in Which Old and New Can Coexist

Make JSON the default after the migration. What you do not carry over is BinaryFormatter-based serialization such as BinaryMessageFormatter. The BinaryFormatter implementation was removed from the runtime in .NET 9, and it is not recommended on security grounds either.8

That does not mean every binary format is out, though. Formats whose specification is maintained independently, such as Protocol Buffers and MessagePack, can be carried over to the new system as they are without a problem. Establish the formatter and the body type first, in the inventory in Section 3.2.

6.2. Before Rewriting, Pin Down Inputs and Results With Tests

Queue processing is an area where timing-dependent defects are easy to introduce. Prepare characterization tests up front that say “this input message produces this result”, so that you can verify mechanically that the replacement produces the same result. See also “Pin Behavior Down With Characterization Tests Before You Refactor”.

6.3. Handle the Receiving Side First, and Design the Bridge Around Loss and Duplication

Bring the Receiving Side Over First

First provision the new queue, get the receiving side working against it, and only then switch the sending side.

Inserting a small bridge that moves messages from the old MSMQ to the new queue saves you from switching every sender at once. The bridge itself can stay on .NET Framework.

Split the Handover Procedure by Whether Transactions Are Used

But if it stops between dequeuing from MSMQ and writing to the new queue, you get either loss or double delivery. Design as follows, according to the kind of source queue.

Source Minimum handover required
Transactional queue Receive transactionally on the MSMQ side so that a failed write can be rolled back. On the new queue side, reject duplicates by message ID and process idempotently
Non-transactional queue Peek to read it, write it idempotently to the new queue, confirm the write succeeded, and only then remove it from the old queue. Duplicates caused by a stop before the removal are absorbed on the new queue side

A queue’s transactional attribute is fixed when it is created and cannot be changed afterwards. Note that the transactional receive procedure cannot simply be applied to a non-transactional queue.

6.4. If the Bridge Is Not Worth It, Drain the Queue and Switch

If the measures against loss and duplication in the bridge are out of proportion to the size of the system, do not force old and new to coexist; lean toward draining the old queue during a planned outage and then switching.

Switching with messages still sitting in the queue causes double processing and loss. Emptying the queue before switching is, in the end, often both the safest and the fastest route — that is this article’s practical judgment.

7. Summary

As of July 2026, the reference point for this article, MSMQ has not been officially deprecated. Surviving as an OS feature and being easy to migrate to .NET are two different things. System.Messaging is confined to .NET Framework, so the rule is that if you move the app up to .NET, you review the queue along with it.123

If you keep using it for the time being, check the OS support period and put in place the build and recovery procedures, queue monitoring, a record of the configuration, and an annual review of the decision. The big risk of mothballing is not only technical: it is that nobody is left who can touch it.

If you migrate, take inventory of the dependencies and the message formats first. For asynchronous processing that updates the same business database, a table queue is the first choice. The consistency that MSMQ plus DTC used to provide can be replaced by a local transaction in that one database. Consider RabbitMQ or Azure Service Bus when there is a requirement it cannot meet, such as delivery to several systems or routing.

On top of that, check the SQL isolation level and hints, the order in which updates are applied under parallelism, and the loss and duplication measures in the bridge. What matters is not rushing because “apparently it has been discontinued”, but understanding what your own system depends on and deciding on that basis between keeping it and migrating.

Komura Software LLC handles inventories and migration plans for legacy configurations including MSMQ, migration from .NET Framework to .NET, and the modification of business systems that involve queue processing.

References

  1. Microsoft Learn, Deprecated features in the Windows client. The official list of features on the Windows client for which active development has ended, that is, deprecated features. On the fact that, as of July 2026, the list carries NTLM, VBScript, WordPad and others while MSMQ (Microsoft Message Queuing) is not listed, and that deprecated is a stage meaning “not actively being developed and possibly removed in a future update”, which is distinguished from removed.  2 3 4

  2. Microsoft Learn, Features Removed or No Longer Developed in Windows Server. The official list of features removed from Windows Server and of features no longer under development, that is, deprecated ones. On the fact that MSMQ is not listed as of July 2026, including on the Windows Server 2025 tab, and that deprecated components continue to ship with Windows Server, remain supported for production use in accordance with the product lifecycle, and continue to receive security and quality updates.  2 3 4

  3. Microsoft Learn, MessageQueue Class (System.Messaging). On the fact that the reference for the System.Messaging.MessageQueue class covers versions from .NET Framework 1.1 through 4.8.1 and that no edition exists for .NET (Core and later).  2 3 4

  4. Microsoft Learn, .NET Framework official support policy. On the fact that .NET Framework 4.8 is defined as a component of the Windows operating system and is supported in accordance with the lifecycle policy of the parent product (the OS) it is installed on.  2

  5. Microsoft Learn (archived), Message Queuing Functions, MQSendMessage and MQReceiveMessage. On the fact that MSMQ’s Win32 native API (MQCreateQueue, MQSendMessage, MQReceiveMessage and others) is documented for C/C++ applications and allows queues to be created and messages to be sent and received without going through a managed API.  2

  6. Microsoft Learn, Use the Windows Compatibility Pack to port code to .NET. On the fact that the Windows Compatibility Pack (the Microsoft.Windows.Compatibility package) supplies roughly 20,000 APIs to cover dependencies on .NET Framework-only APIs during a migration to .NET, and that its list of technology areas (CodeDom, configuration, Directory Services, Drawing, ODBC, ACL, WCF, the registry, WMI, performance counters, Windows services, EventLog and so on) does not include messaging (System.Messaging).  2

  7. CoreWCF project, CoreWCF.MSMQ (NuGet) and CoreWCF 1.4.0 Preview release; Microsoft, CoreWCF Support Policy. On the fact that CoreWCF is a community-led project that ports the server side of WCF to .NET and that Microsoft provides an official support policy for it, that MSMQ support (the CoreWCF.MSMQ package) has been published as part of its queued transports, and that this MSMQ implementation depends on a community port of the .NET Framework version of the System.Messaging library.  2 3

  8. Microsoft Learn, BinaryFormatter migration guide. On the fact that BinaryFormatter has been phased out for security reasons, that from .NET 9 its implementation has been removed from the runtime and it cannot be used by default, and that safe serialization formats such as JSON (System.Text.Json) are given as migration targets.  2 3

  9. Microsoft Learn (archived), Express and Recoverable Messaging, System-Generated Queues, Message Queuing (MSMQ). On the facts that an express message is held in RAM both in transit and after delivery and is lost when the computer holding it stops or the Message Queuing service stops; that a recoverable message is written to disk on the sending computer and on each relaying computer and is also held on disk at the destination queue; that a public queue is registered in the directory service whereas a private queue is registered only on the local computer and is not published to the directory service; and that the journal queue, which keeps copies of messages dequeued from a queue and of messages already sent, and the dead-letter queue, which holds messages that could not be delivered, are system queues generated by MSMQ. 

  10. Microsoft Learn, Table Hints (Transact-SQL). On the facts that READPAST is a hint that skips past rows locked by other transactions without reading them, that it can skip only row-level locks and not page-level locks, and that it can be specified only at the READ COMMITTED or REPEATABLE READ isolation level. In particular, for the statement that “when the READ_COMMITTED_SNAPSHOT database option is set to ON and either (a) the transaction isolation level of the session is READ COMMITTED or (b) the query also specifies the READCOMMITTED table hint is true, the READPAST table hint cannot be specified. To specify the READPAST hint in these cases, remove the READCOMMITTED table hint if present, and include the READCOMMITTEDLOCK table hint in the query.” See the same page also for the facts that READCOMMITTEDLOCK makes READ COMMITTED reads use locking regardless of the READ_COMMITTED_SNAPSHOT setting, and that no more than one granularity hint (PAGLOCK / NOLOCK / READCOMMITTEDLOCK / ROWLOCK / TABLOCK / TABLOCKX) can be specified for a single table. The database-side setting can be checked through is_read_committed_snapshot_on in sys.databases 2 3 4

  11. Microsoft Learn, TOP (Transact-SQL). On the fact that when TOP is used with INSERT, UPDATE, MERGE or DELETE the referenced rows are not arranged in any order, and that a subquery with TOP and ORDER BY should be used when the order matters. 

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Frequently Asked Questions

Common questions about the topic of this article.

Has MSMQ been discontinued?
No. As of July 2026, MSMQ appears on neither the Windows client's list of deprecated features nor Windows Server's list of removed features and features no longer under development. It still ships with current OS versions as a Windows optional feature. The widespread claim that it has been "discontinued" comes from conflating it with the situation on the .NET side. System.Messaging, the standard class library for working with MSMQ, exists only in .NET Framework and was never ported to .NET (Core and later). To put it precisely: it survives as an OS feature, but there is no official way to use it from modern .NET.
Is there any way to use MSMQ from .NET 8 or .NET 10?
There is no official managed API. System.Messaging is an API for .NET Framework up to 4.8.1, and it is not included in the Windows Compatibility Pack (Microsoft.Windows.Compatibility) either. Calling MSMQ's native Win32 API (functions such as MQSendMessage) via P/Invoke is technically possible, but it means taking on the job of writing and maintaining your own wrapper, including formatters and transaction integration. On the community side, the CoreWCF project publishes a package for the MSMQ transport (CoreWCF.MSMQ), but that exists to host, on modern .NET, WCF services that are invoked through a queue — a port of WCF's server side. It is neither a general-purpose queue API that replaces System.Messaging nor a substitute for the sending-side client. Its implementation also depends on a community port of the .NET Framework version of System.Messaging. It is a viable option for evaluation or for temporarily extending the life of a system. CoreWCF itself does have an official Microsoft support policy, but that guarantee does not automatically extend to the community port of System.Messaging that this MSMQ implementation depends on, so if you are going to build a production system on it, decide only after checking whether the version you use falls under the support policy and how the dependent piece is handled. In practice, the right course is to migrate the queue at the same time you move the app up to .NET.
Which is the better migration target, RabbitMQ or Azure Service Bus?
Before that choice, consider turning a database table into the queue. In most small and medium-sized business systems that use MSMQ, the party on the other end of the queue is a process that updates the company's own database. In that case, a table queue lets you commit "dequeuing the message" and "updating the business data" in the same local transaction as the business data, replacing the consistency that MSMQ plus a distributed transaction (DTC) used to provide with a much simpler mechanism. Only when a table queue falls short of a requirement — loosely coupled delivery across multiple systems, or the need for high throughput, for example — should you move on to considering RabbitMQ if the on-premises requirement is strong, or Azure Service Bus if it can live in the cloud. That ordering is the one least likely to fail.
Is it acceptable to just leave things on .NET Framework for now?
Conditionally, yes. .NET Framework 4.8 is supported as a Windows component, along the OS's own lifecycle, and MSMQ itself has not been deprecated, so you can reasonably expect it to keep running. That said, if you choose to leave it as-is, do at minimum these four things: state the enabling of the Message Queuing feature explicitly in your kitting procedure, put monitoring in place for queue length and the journal, document the message format and connection configuration, and leave a recovery procedure behind in case the person responsible moves on. The real risk of leaving it as-is is not technical — it is that nobody is left who can touch it.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog