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

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

“This system uses MSMQ, and I’ve heard it’s been discontinued. Do we need to migrate right away?” — this is a question I’ve heard repeatedly in legacy-migration consultations over roughly the past year.

The answer is a little twisted. MSMQ (Microsoft Message Queuing) is far from discontinued — it hasn’t even been officially deprecated. As of July 2026, MSMQ does not appear anywhere on Microsoft’s list of deprecated features, and it still ships as an optional feature in current versions of Windows. And yet it feels, on the ground, like “a technology that’s over” — because the official managed API on the .NET side is closed off. System.Messaging, the standard library for working with MSMQ, exists only in .NET Framework and was never ported to .NET (Core and later).

In other words, MSMQ is not a technology that will “stop working tomorrow” — it’s a technology that becomes a wall the moment you try to move your app to .NET.

This article is written for developers responsible for maintaining and migrating existing systems that use MSMQ, and for the IT department staff who are asked to make that call. The goal is to sort fact from rumour about the “it’s been discontinued” story, and to reach a point where you can decide whether to migrate based on the conditions in your own environment.

This article separates rumour from fact, then sets out how to decide between continuing to use MSMQ and migrating, and how to choose a migration target. Migration from VB6 or .NET Framework itself is covered in “Practical VB6-to-.NET Migration” and “.NET Framework to .NET Pre-Migration Checklist”, so this article focuses specifically on the queue.

1. The Bottom Line First

  • MSMQ has not been officially deprecated. It appears on neither the Windows client’s list of deprecated features nor Windows Server’s list of removed/no-longer-developed features (as of July 2026).12
  • But there is no official managed API for .NET. System.Messaging covers only .NET Framework 1.1 through 4.8.1,3 and it is not included in the Windows Compatibility Pack, the standard means of bridging to .NET migration.4 Calling the native Win32 API via P/Invoke remains an option, but it is best treated as a stopgap (Section 3).
  • A CoreWCF port exists, but it applies only to migrating a queue-based WCF service (the receiving side), and it’s a path to use only after checking its conditions. CoreWCF itself has a Microsoft support policy, but the MSMQ transport implementation (CoreWCF.MSMQ) depends on a community port of the .NET Framework version of System.Messaging.5
  • The axis of the decision is whether you’re moving the app to .NET. If you are, migrating the queue along with it is the rule (keeping it alive via P/Invoke or CoreWCF is also possible, but that’s a decision to take on the maintenance burden). If you’re not (leaving it as-is), you can plan to keep it running for as long as .NET Framework 4.8 is supported as an OS component.6
  • The first-choice migration target is a database table queue, not a message broker. The consistency that MSMQ plus a distributed transaction coordinator (DTC) used to guarantee is more straightforwardly replaced by a table queue that can be processed in the same transaction as the business database (Section 5).
  • Do an inventory of message formats first. BinaryFormatter-based serialisation was removed from the runtime in .NET 9,7 and letting old and new formats coexist will get you stuck (Section 6).

2. MSMQ in 30 Seconds

MSMQ is the message-queuing platform that has shipped with Windows. An application writes a message to a queue, and another application (whether on the same machine or a different one) retrieves it whenever convenient. Its defining characteristics come down to three points.

  • Store-and-forward: even if the destination is down, the message is stored locally and delivered once the destination recovers. This makes it resilient to unstable links between sites. That persistence isn’t unconditional, though — express messages, the default, can remain in memory only and are lost if the MSMQ service or the machine restarts. What gets persisted to disk is a message the sender explicitly marked Recoverable, or a transactional message.
  • Transactions: queue operations can be made transactional, and combined with MS DTC (the Distributed Transaction Coordinator) you can commit “dequeuing from the queue” and “updating the database” as a single distributed transaction.
  • Bundled with the OS: because it could be used without installing any additional middleware, it saw wide adoption in 2000s-era business systems — order-data integration in particular, asynchronous report processing, and inter-process linkage on factory floors.

It’s precisely because this combination — bundled with the OS, transactional, resilient offline — was so good that MSMQ is still in active use today. When you’re thinking about a migration target, which of these three properties you’re actually relying on is the core of the decision.

The Minimum Set of Terms Used in This Article

The decision table in Section 5 and the inventory in Section 6 assume the following terms. Get them straight here, all at once.8

  • Private queue — a queue that isn’t published to a directory service (Active Directory) and is registered only on that computer. It’s specified in a form such as .\private$\queue-name. Its counterpart, the public queue, is registered in the directory service and can be looked up from within the domain. In small and medium-sized business systems, almost everything you’ll see is a private queue.
  • Express message — the default send mode. Fast, because the message stays in memory both in transit and after delivery, but it’s lost if the MSMQ service or the machine stops.
  • Recoverable message — a mode in which the message is written to disk at the sender and at each computer it’s routed through, and is held on disk at the destination queue as well. It survives restarts. The sender must specify it explicitly.
  • Transactional queue — a queue that handles transactional messages only. This is decided when the queue is created and cannot be changed afterwards (this matters for the migration procedure in Section 6).
  • DTC (Distributed Transaction Coordinator) — a Windows service that coordinates transactions spanning multiple resources (an MSMQ queue and a database, for instance). It’s the mechanism that commits “dequeuing” and “the database update” as a single unit, and it’s the single biggest point of contention in the migration decision in Section 5.
  • Journal queue / dead-letter queue — system queues that MSMQ generates automatically. The journal accumulates copies of sent/retrieved messages, and the dead-letter queue accumulates messages that could not be delivered. Left unattended they eat up capacity, so they need to be monitored if you’re leaving the system as-is (Section 7).

3. The Facts — “Discontinued” Is Not Accurate

The question “Can you still use MSMQ?” gets confusing because the answer differs by layer, yet it’s discussed as if it were one single thing. Here’s the whole picture in one table first.

Layer Current status Practical meaning
OS feature (a Windows optional feature) Alive. Ships with current Windows client/Server, and no deprecation notice has been issued12 Turn it on and it still works today. It can be kept in use for as long as the OS is supported
Win32 native API (MQSendMessage and others) Documented and usable9 The route of calling it from .NET via P/Invoke remains open. But you’ll be writing and maintaining your own formatter and transaction integration
System.Messaging in .NET Framework Usable. But it covers only .NET Framework 1.1 through 4.8.13 Existing systems run here. There’s nothing beyond this
Official managed API for .NET (Core and later) Does not exist. Not included in the Windows Compatibility Pack either4 The moment you move the app 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.Messaging5 Usable to keep a queue-invoked WCF service (the receiving side) alive, but it’s neither a replacement for the sending side nor a general-purpose queue API
New adoption Cannot be recommended Because there’s no official path forward

The following walks through the basis for each row of this table, in order.

First, MSMQ does not appear on the deprecated list. Looking at the Windows client’s “Deprecated features” list, NTLM, VBScript, and WordPad are all there, but there’s no entry for MSMQ.1 On the Windows Server side, the “Features Removed or No Longer Developed” list doesn’t mention MSMQ either, including the Windows Server 2025 tab.2 Deprecated is an official declaration that “active development has stopped” — and the current state of affairs is that not even that declaration has been made.

Second, System.Messaging has stopped at .NET Framework. The reference documentation’s covered versions run from .NET Framework 1.1 to 4.8.1, and no version exists for .NET (Core and later).3 The Windows Compatibility Pack (Microsoft.Windows.Compatibility) — the receptacle for .NET-Framework-only APIs — provides around 20,000 APIs covering the registry, WMI, Windows services, EventLog, and more, but its list of technology areas does not include messaging (System.Messaging).4 To be precise, what’s closed off is the managed API. MSMQ’s Win32 native API (MQSendMessage, MQReceiveMessage, and so on) is still documented today, and calling it from .NET via P/Invoke is itself possible. But you’ll end up writing your own wrapper for the formatter and transaction integration that System.Messaging used to absorb, and maintaining it yourself — so this isn’t “the real answer for continuing to use it,” it’s positioned as “a stopgap for when you absolutely have to keep it.”

Third, WCF’s MSMQ integration sits behind the same wall. WCF in .NET Framework had a binding that used MSMQ underneath, but trying to reproduce that path in modern .NET leads you to the community-led CoreWCF project. CoreWCF publishes an MSMQ-support package (CoreWCF.MSMQ) as part of its queue-based transports, but its implementation explicitly states a dependency on a community port of the .NET Framework version of the System.Messaging library.5 Does it work? Yes, it works, and CoreWCF itself has an official Microsoft support policy in place, so it isn’t “an entirely unofficial library” either.5 Whether that same treatment extends all the way down to the community port of System.Messaging that the MSMQ transport depends on, though, is a separate question. If you adopt it, decide only after checking whether the version you use falls under the support policy, and how that dependent piece is treated.

That covers the basis for each row of the table at the top of this section. It isn’t that “MSMQ has been discontinued” — it’s that “there’s no official path from modern .NET to MSMQ.” Keeping that distinction in mind will help internal discussions actually connect.

4. What “It Works, But It’s a Problem” Really Means

Consultations about MSMQ-involved systems usually start not from an incident but from a migration estimate. The real problem isn’t MSMQ itself — it’s that MSMQ becomes an anchor that ties the whole app to .NET Framework.

.NET Framework 4.8 is treated as a Windows component and is supported along the lifecycle of the OS it’s installed on.6 So the question “will it keep running?” can be answered “yes, for the time being it will keep running.” Even so, the following items reliably get heavier over time.

  • You can’t hire for it, and you can’t hand it over. The number of engineers who can explain System.Messaging and DTC shrinks every year. It’s a textbook case of the pattern described in “When You Inherit a System With No Source Code and No Documentation”.
  • You don’t get the benefit of the runtime or of newer libraries. Many newer C# language features can be used on .NET Framework too, purely through a compiler update, but you don’t get runtime-side performance improvements or newer standard libraries, and you increasingly lose access to recent packages that are dropping .NET Framework support.
  • The distributed transaction becomes the hardest part of the migration. MSMQ+DTC’s design — making “dequeuing” and “the database update” atomic — cannot be reproduced with cloud-based queue services. If you leave this alone and only move the surrounding parts to .NET, you’re left with the hardest core piece for last.
  • A time bomb in the serialisation format. In older systems the message body is sometimes binary-serialised, and BinaryFormatter was removed from the runtime in .NET 9.7 You’ll need to review the message format itself at migration time.

In other words, the practical answer to “how long can you keep using MSMQ” is: “it will run for as long as the OS supports it, but the difficulty of migrating only goes up the longer you wait.” Even if you defer the decision, you should at least get a grip on “where the hard parts are” from the decision table in the next section.

5. A Decision Table for the Migration Target

You don’t choose a migration target by asking “which product succeeds MSMQ” — you choose it according to which of MSMQ’s properties you’re actually relying on.

5.1. First, Break the Requirement Down Into Four Questions

  1. Is the recipient at the other end of the queue (the receiving-side process) a process that updates your own database?
  2. Are you using a distributed transaction (DTC)? (a transactional queue plus a database update)
  3. Are the sender and receiver on different machines, at different sites? Are you actually relying on offline resilience (store-and-forward)?
  4. What’s the actual message volume? (Most business systems handle a few thousand to a few tens of thousands a day, which every option here can handle comfortably.)

5.2. The Decision Table

Property you’re relying on First choice Reasoning and caveats
Asynchronous processing within the same system (the receiving side updates the DB) A database table queue You can commit “dequeuing the message” and “updating business data” in a single local transaction, eliminating the need for DTC. Your existing backup, monitoring, and operations carry over unchanged. On the sending side too, “updating business data and inserting the message row” can go in a single transaction — this is the so-called Outbox pattern
Using DTC to make the queue and the DB update atomic A database table queue The essence of the migration is replacing the distributed transaction with a local transaction. Cloud queue services can’t participate in DTC, so as long as this requirement exists, broker-based options are a detour
Loosely coupled integration across multiple systems / languages RabbitMQ (on-premises) / Azure Service Bus (cloud-capable) Fan-out and routing of delivery is squarely a broker’s strength. For RabbitMQ, budget for the load of running it yourself (redundancy, patching)
Offline resilience across sites (store-and-forward) Azure Service Bus etc. plus a retry design, or a site-side table queue plus synchronisation There aren’t many mechanisms that transparently substitute for MSMQ’s “accumulate locally on the sending side and deliver later.” Change the design so the app itself explicitly owns the responsibility for accumulating on the sending side
Producer/consumer within the same process In-memory queues such as .NET’s Channels A case where an inter-process queue was never actually needed. Keep it within the process, and go to a table queue if you need persistence
Used only as inter-process communication between Windows services IPC such as named pipes This substitution only works if both sides are always running at the same time, though. Even while the receiver is down, MSMQ (even with express messages) still accepts sends and delivers them later, whereas a pipe fails immediately. If you’re relying on accumulation while the receiver is down, either implement retry/buffering in the app yourself, or keep the queue. For how to choose, see “Choosing Windows Inter-Process Communication ── A Decision Table for Named Pipes / TCP / gRPC / Shared Memory / COM

5.3. A Minimal Table-Queue Implementation

A lot of people say “table queue” doesn’t quite click for them, so here is the minimal shape. First, the table definition (SQL Server).

CREATE TABLE dbo.JobQueue (
    Id          BIGINT IDENTITY(1,1) PRIMARY KEY,
    Payload     NVARCHAR(MAX) NOT NULL,                    -- 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 stuck "in progress"
    RetryCount  INT           NOT NULL DEFAULT 0
);
CREATE INDEX IX_JobQueue_Status ON dbo.JobQueue (Status, Id);

Dequeuing marks exactly one row as “in progress” while receiving its body. Adding READPAST lets you skip rows locked by other workers without waiting for them (so multiple processes can run it concurrently).

WITH next_job AS (
    SELECT TOP (1) *
    -- READCOMMITTEDLOCK is needed on a DB where READ_COMMITTED_SNAPSHOT is ON (see below).
    -- On a DB where it's OFF, this matches the default behaviour, so leaving it on works for both
    FROM   dbo.JobQueue WITH (READPAST, UPDLOCK, READCOMMITTEDLOCK)
    WHERE  Status = 0
    ORDER  BY Id          -- Dequeue in enqueue order. This is the condition for it being a "queue"
)
UPDATE next_job
SET    Status = 1, StartedAt = SYSUTCDATETIME()
OUTPUT inserted.Id, inserted.Payload;

READPAST cannot be used as-is on a database where READ_COMMITTED_SNAPSHOT is ON. The official documentation states explicitly that “READPAST cannot be specified when READ_COMMITTED_SNAPSHOT is set to ON and either the session’s transaction isolation level is READ COMMITTED, or the READCOMMITTED table hint is also being used with the query,” and it names adding the READCOMMITTEDLOCK hint as the fix in that case.10 Because the default isolation level is READ COMMITTED, on a database where READ_COMMITTED_SNAPSHOT has simply been enabled, this dequeue statement doesn’t just fail to “skip other workers’ rows” — the statement itself errors out. Azure SQL Database has it ON by default, and on-premises environments not infrequently enable it too, as a countermeasure against read blocking. Check first which one your target database is.

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

The reason ROWLOCK has been dropped from the commonly seen WITH (READPAST, UPDLOCK, ROWLOCK) is that ROWLOCK and READCOMMITTEDLOCK belong to the same “granularity hint” group, and you cannot specify both on one table.10 READPAST can only skip row locks, not page locks, so you might want to make ROWLOCK explicit — but escalation practically never happens against the single row fetched by a TOP (1) index seek. If you know READ_COMMITTED_SNAPSHOT is OFF and want ROWLOCK to be explicit, drop READCOMMITTEDLOCK instead.

If you drop ORDER BY and write UPDATE TOP (1), which row gets picked becomes indeterminate. The documentation states explicitly that TOP on an UPDATE does not sort the affected rows, so even having a (Status, Id) index doesn’t guarantee dequeue order.11 For an integration that assumes processing in enqueue order, this manifests as a bug where older jobs keep getting pushed to the back.

That said, ORDER BY Id only guarantees the order in which rows are dequeued. It does not guarantee the order in which processing finishes. READPAST exists precisely so that other workers can skip rows locked by others, so while worker A grabs job 1 and takes a long time on it, worker B can grab job 2 and commit first.

What’s guaranteed What isn’t guaranteed
Which row gets dequeued first (ORDER BY Id) Which row gets applied to business data first

For integrations where the order of application matters — say, “it’s a problem if updates to the same order number get swapped around” — this isn’t enough. There are two viable shapes.

  • Run a single consumer. Give up parallelism to get ordering. If throughput is sufficient, this is the simplest and least fragile shape.
  • Partition by key. Either pin a worker to a key such as the order number, or add a condition on the dequeue side — “don’t dequeue if a row with the same key is already in progress” — to guarantee ordering only within a key. Give up on ordering across keys.

If you take neither, make it explicit in your migration design that the moment you run in parallel, there is no overall FIFO guarantee. This point is particularly easy to miss when migrating from MSMQ. The same thing happens if you receive from an MSMQ queue in parallel too, but if you carry over the assumption that “it’s a queue, so it’s in order,” it will look as though ordering problems appeared the instant you switched to a table queue.

The receiving-side worker simply runs this inside the same local transaction as the business processing (C#, pseudo-code).

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);  // Just wait one polling interval if empty
        continue;
    }

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

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

This is the part that replaces MSMQ+DTC. With MSMQ, “dequeuing from the queue” and “updating the DB” were separate resources, so a distributed transaction (DTC) was needed to bundle the two together. If the queue is a database table, both are updates to the same database, so an ordinary local transaction suffices. Bring in RabbitMQ or Azure Service Bus and you lose this property (the queue and the DB go back to being separate resources), and you end up building idempotency and retries into the app yourself.

In real-world operation, there are roughly three things you need to add on top.

  • Reclaiming rows that crashed while “in progress.” Add a periodic recovery job that resets rows to Status = 0 when Status = 1 and StartedAt is older than some threshold.
  • A retry cap and quarantine. Route rows whose RetryCount exceeds the cap to a separate table (or Status = 9). This is the equivalent of MSMQ’s dead-letter queue.
  • Cleaning up completed rows. Periodically delete or archive rows with Status = 2. Leave them and the table bloats and index effectiveness degrades.

On the sending side, put the business-data update and the message-row INSERT in the same transaction (the Outbox pattern). This also eliminates the inconsistency where “business data was updated but no message went out.”

The point I want to emphasise is that for small and medium-sized business systems, a table queue is often the first choice. Message-queue-product comparison articles tend to take the shape of “RabbitMQ vs. Kafka vs. Service Bus,” but what MSMQ-generation systems are typically putting on a queue is “an asynchronous job that updates the same database” — and that can be realised sufficiently, and more simply, with a database table plus polling (or notification). The operational cost of adding one more piece of middleware (monitoring, redundancy, patching, staff training) weighs particularly heavily on smaller teams.

6. The Practical Migration Procedure

The approach follows the standard playbook for legacy migration: “observe, then move.”

  1. Inventory the dependencies. Sweep the code for references to System.Messaging and places where MessageQueue is constructed. Also search for netMsmqBinding / msmqIntegrationBinding in WCF configuration files, and for calls to the native API (MQ* functions such as MQSendMessage) or via COM. A path can depend on MSMQ even without referencing System.Messaging. The points to check are: (a) the queue’s path (local or remote, private or public), (b) whether it’s a transactional queue, and whether messages are marked Recoverable (if neither, the current system is running on the assumption that messages are lost on restart), (c) the formatter (XmlMessageFormatter / BinaryMessageFormatter / ActiveXMessageFormatter), (d) use of the journal and dead-letter queues, and (e) who creates and deletes the queue (the installer, or the app).

    Simply checking these things during the inventory doesn’t feed into the migration decision on its own. Fill in one row per queue in the form below, and use it directly as the basis for your migration policy (you can attach it as-is to an approval document).

    Point to check Where to look What to record How it affects the decision
    (a) Queue path The path string passed to MessageQueue, the destination in the config file, the FormatName: specification Local vs. remote, private vs. public, the peer machine name If remote/cross-site, decide whether store-and-forward is needed (the “offline resilience” row in 5.2). If purely local, this falls into the first row of 5.2
    (b) Transaction and Recoverable Where the queue is created (is it a transactional queue?), whether Recoverable is specified when sending, whether DTC is used Whether it’s a transactional queue, whether Recoverable is specified, whether DTC is involved If DTC is involved, a table queue is almost certainly the migration target. If neither applies, document explicitly that the current system operates on the assumption that “messages disappear on restart”
    (c) Formatter Where XmlMessageFormatter / BinaryMessageFormatter / ActiveXMessageFormatter is specified Which formatter is in use, and the message body’s type If it’s BinaryFormatter-based, replacing it with JSON is mandatory (step 2). This is a major factor in migration effort7
    (d) Journal / dead-letter queue Queue properties (is the journal enabled?), the contents of the system queues, operational runbooks 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 quarantine table, for a table queue)
    (e) Who creates/deletes it The installer, deployment scripts, Create calls at app startup Who creates it, and who configures the permissions At migration time, “who provisions the new queue” carries over directly. If you choose to leave it as-is, transcribe this into your kitting procedure (Section 7)
  2. Decide the message format. Make JSON the default for the post-migration format. What you must not carry forward is BinaryFormatter-based serialisation, such as BinaryMessageFormatter. BinaryFormatter has been removed in .NET 9 and is not recommended on security grounds either.7 On the other hand, if you’re using a binary format whose specification is maintained independently, such as Protocol Buffers or MessagePack, there’s no problem carrying that format over to the new system as-is.
  3. Migrate the receiving side first. Provision the new queue (a table queue, say) first, make the receiving-side processing work against the new queue, and only then switch over the sender. During the migration period, inserting a small bridge that moves messages from the old MSMQ queue to the new one (this can stay on .NET Framework) lets you avoid switching every sender over at once. That bridge, though, loses the message or sends it twice if it crashes between “dequeuing from MSMQ” and “writing to the new queue.” If the source is a transactional queue, the minimum requirement is to make the MSMQ side a transactional receive so a failed write can be rolled back, and design the new queue side to reject duplicates by message ID (i.e. make it idempotent). This approach isn’t available if the source is a non-transactional queue, because a queue’s transactional attribute is fixed at creation and cannot be changed afterwards. In that case, build it as “read with Peek → write idempotently to the new queue → confirm the write succeeded, then remove” (if it crashes before removal, the new queue’s idempotency absorbs the duplicate). If neither approach is worth the effort for the scale involved, it’s safer to skip the bridge and go straight to the “drain, then switch over” approach in step 5.
  4. Pin down the behaviour before rewriting it. Queue processing is an area prone to timing-dependent bugs. Preparing characterization tests before migration — “given this input message, this is the result” — turns post-replacement verification into a mechanical exercise (see “Safely Modifying a Legacy Business App That Has No Tests — Characterization Testing and Refactoring in Practice”).
  5. Switch over once the queue is empty. Switching over with messages still sitting in the queue is a breeding ground for duplicate processing and message loss. Draining the queue completely during a planned outage before switching over ends up being the safest and fastest method.

7. The Minimum Conditions for Choosing to Leave It As-Is

The judgment “there’s no plan to move the app to .NET, and cost-effectiveness says not to migrate for now” is reasonable, conditionally. Below are the minimum conditions for that case, laid out as a checklist you can transcribe directly into an approval or review document. Treat “leaving it as-is” as a valid option only once all four boxes are checked.

Check Minimum condition What to do concretely What happens if it’s not met
Document MSMQ explicitly in the kitting/restore procedure MSMQ is a Windows optional feature. Write the enabling steps — via “Turn Windows features on or off” or DISM/PowerShell — into the environment build procedure Enabling it gets forgotten when a PC or server is replaced, and it fails to work for no apparent reason on rollout day
Monitor queue length Set up threshold monitoring and alerting on the backlog count. Include the dead-letter and journal queues as targets too Even if the receiver stops, nothing errors — messages just keep piling up, and business grinds to a halt without anyone noticing
Document the configuration Record the queue path, permissions, whether it’s transactional, the formatter, and journal settings (the inventory table from Section 6 works as-is) A future migration estimate has to start the investigation over from scratch, hurting both accuracy and effort
Revisit the decision once a year Check annually whether it has appeared on the deprecated list, and whether behaviour has changed with an OS update You end up scrambling only after the deprecation notice is announced

The fourth item is especially easy to underrate, but it’s precisely this annual check that lets you avoid scrambling even after a notice is announced. Conversely, leaving MSMQ as-is in an environment that can’t check off all four is the same as waiting, unseen, for it to stop.

8. Summary

  • As of July 2026, MSMQ is not officially deprecated and continues to exist as an OS feature. The perception that it has “already been discontinued” is not accurate.12
  • On the other hand, System.Messaging has remained .NET-Framework-only, is not included in the compatibility pack, and there’s no official path to .NET (Core and later). CoreWCF itself has a support policy, but its MSMQ transport depends on a community port of System.Messaging.345
  • The axis of the decision is whether you’re moving the app to .NET. If you are, migrating the queue along with it is the rule (keeping it alive via P/Invoke and the like trades against maintenance burden); if you’re leaving it as-is, that’s only viable on the condition of monitoring and documentation.6
  • The first-choice migration target is a database table queue. The essence of the move is replacing MSMQ+DTC’s consistency with a local transaction; consider introducing a broker product only after that.
  • BinaryFormatter-based serialisation cannot be carried into the new system, because of its removal in .NET 9. Make JSON the default at migration time, though an independently maintained format such as protobuf can be carried over as-is.7
  • Switching over in the order receiving side → bridge → sending side, and pinning down behaviour with characterization tests before rewriting, is the way to do this without incidents.

KomuraSoft LLC handles inventorying and migration planning for legacy configurations including MSMQ, migration from .NET Framework to .NET, and modification of business systems that include queue processing.

References

  1. Microsoft Learn, Deprecated features in the Windows client. The official list of features for which active development has ended (deprecated) on the Windows client. On NTLM, VBScript, WordPad, and others appearing on the July 2026 list while MSMQ (Microsoft Message Queuing) does not appear on it, and on deprecated being a stage where a feature “is not being actively developed and may be removed in a future update” — distinct from removed.  2 3 4

  2. Microsoft Learn, Features Removed or No Longer Developed in Windows Server. The official list of features removed from and features no longer developed for Windows Server. On MSMQ not appearing on the July 2026 list, including the Windows Server 2025 tab, and on deprecated components continuing to ship with Windows Server and remaining supported for production use, and continuing to receive security and quality updates, in line with the product lifecycle.  2 3 4

  3. Microsoft Learn, MessageQueue Class (System.Messaging). On the reference documentation for the System.Messaging.MessageQueue class covering versions from .NET Framework 1.1 through 4.8.1, and on no version existing for .NET (Core and later).  2 3 4

  4. Microsoft Learn, Use the Windows Compatibility Pack to port code to .NET. On the Windows Compatibility Pack (the Microsoft.Windows.Compatibility package) providing around 20,000 APIs to cover dependencies on .NET-Framework-only APIs when migrating to .NET, and on its list of technology areas (CodeDom, Configuration, Directory Services, Drawing, ODBC, ACLs, WCF, the registry, WMI, performance counters, Windows services, EventLog, and others) not including messaging (System.Messaging).  2 3 4

  5. CoreWCF project, CoreWCF.MSMQ (NuGet) and CoreWCF 1.4.0 Preview release, Microsoft, CoreWCF Support Policy. On CoreWCF being a community-led project that ports the server side of WCF to .NET, with Microsoft providing an official support policy; on MSMQ support (the CoreWCF.MSMQ package) being published as part of its queue-based transports; and on that MSMQ implementation depending on a community port of the .NET Framework version of the System.Messaging library.  2 3 4 5

  6. Microsoft Learn, .NET Framework official support policy. On .NET Framework 4.8 being defined as a component of the Windows operating system, and being supported in line with the lifecycle policy of the installed parent product (the OS).  2 3

  7. Microsoft Learn, BinaryFormatter migration guide. On BinaryFormatter being phased out for security reasons, with the implementation removed from the runtime and disabled by default from .NET 9 onward, and on safer serialisation formats such as JSON (System.Text.Json) being recommended as the migration target.  2 3 4 5

  8. Microsoft Learn (archived), Express and Recoverable Messaging, System-Generated Queues, Message Queuing (MSMQ). On express messages being held in RAM both in transit and after delivery, and being lost if the computer on which the message resides stops or the MSMQ service stops; on recoverable messages being written to disk at the sender and at each computer they’re routed through, and being held on disk at the destination queue as well; on public queues being registered in the directory service while private queues are registered only on the local computer and not published to the directory service; and on the journal queue, which holds copies of messages retrieved from a queue or already sent, and the dead-letter queue, which holds messages that could not be delivered, being system queues generated by MSMQ. 

  9. Microsoft Learn (archived), Message Queuing Functions and MQSendMessage, MQReceiveMessage. On MSMQ’s Win32 native API (MQCreateQueue, MQSendMessage, MQReceiveMessage, and others) being documented for C/C++ applications, and on being able to create, send to, and receive from queues without going through a managed API. 

  10. Microsoft Learn, Table hints (Transact-SQL). On READPAST being a hint that skips rows locked by other transactions rather than reading them, on it being able to skip only row-level locks and not page-level locks, and on it being specifiable only under the READ COMMITTED or REPEATABLE READ isolation levels. In particular, on the statement that “if the READ_COMMITTED_SNAPSHOT database option is set to ON and either (a) the session’s transaction isolation level is READ COMMITTED, or (b) the query also specifies the READCOMMITTED table hint, 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.” Also see the same page regarding READCOMMITTEDLOCK being a hint that forces lock-based READ COMMITTED behaviour regardless of the READ_COMMITTED_SNAPSHOT setting, and regarding not being able to specify more than one granularity hint (PAGLOCK / NOLOCK / READCOMMITTEDLOCK / ROWLOCK / TABLOCK / TABLOCKX) on a single table. The database-level setting can be checked via sys.databases’s is_read_committed_snapshot_on 2

  11. Microsoft Learn, TOP (Transact-SQL). On the rows referenced by TOP not being arranged in any order when used with INSERT, UPDATE, MERGE, or DELETE, and on using a subquery with TOP and ORDER BY when an order is required. 

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 features removed or no longer developed. 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's no official managed API. System.Messaging is an API for .NET Framework up to 4.8.1, and it isn't included in the Windows Compatibility Pack (Microsoft.Windows.Compatibility) either. Calling MSMQ's native Win32 API (functions like 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 via a queue (a port of WCF's server side) — it is neither a general-purpose queue API to replace 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's a viable option for evaluation or a temporary extension of life. 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're 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 treated. 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 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 requirement is strongly on-premises, 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 hasn't 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 MSMQ 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 in place in case the person responsible moves on. The real risk of leaving it as-is is not technical — it's that no one 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