COM STA/MTA Fundamentals - Threading Models and How to Avoid Hangs
· Updated: · Go Komura · COM, Windows Development, STA, MTA, Threading
Revision history (1 updates, last updated Sep 1, 2026)
A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.
- Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614451)
- First published
Cite this article(DOI: 10.5281/zenodo.21614450)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). COM STA/MTA Fundamentals - Threading Models and How to Avoid Hangs. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614450 https://comcomponent.com/en/blog/2026/01/31/000-sta-mta-com-relationship/
- DOI (latest version)
- 10.5281/zenodo.21614450
- DOI (this version)
- 10.5281/zenodo.22217116
COM’s STA/MTA is foundational knowledge that is hard to avoid when doing Windows development or touching COM from .NET. The questions people search for most often are: why is the UI thread STA, what happens when a call crosses apartments, and why do things hang?
Table of Contents
- 1. The Conclusion First (In One Line)
- 2. Call Patterns in the Apartment Model (Diagrams)
- 3. STA (Single-Threaded Apartment)
- 4. MTA (Multi-Threaded Apartment)
- 5. Where STA/MTA Gets Decided
- 6. A Concrete Example of a Hang Caused by Getting STA Wrong
- 6.1. The Typical Situation
- 6.2. What Happens
- 6.3. Pseudocode (The Classic Failure Pattern)
- 6.4. Key Points for Avoiding It
- 6.5. What Does “Pumping the Message Loop” Actually Mean?
- 6.6. An Example in the Right Direction (Roughly Sketched)
- 6.7. Another Hang Example: Callbacks During a Synchronous Call
- 7. A Rough Guide to Choosing
- 8. Conclusion
- 9. References
When you use COM, “which thread does this run on” is unavoidable. At the center of that question is the apartment model (STA/MTA). STA/MTA is not a general Windows threading concept - it is a threading model that determines the call rules for COM objects.
In this article, we explain the relationship between STA, MTA, and COM with diagrams, and connect it all the way through to “why things sometimes hang.”
flowchart TB
accTitle: Where the apartment model sits
accDescr: Diagram showing that STA and MTA are the two forms of the apartment model, a threading model that determines the call rules for COM objects rather than a general Windows threading concept.
com["Call rules for COM objects"] --> am["Apartment Model"]
am --> sta["STA"]
am --> mta["MTA"]
am -.-> note["Not a general Windows threading concept"]
Figure 1: STA and MTA are the two forms of the apartment model, which determines the call rules for COM objects.
Knowledge map for this article
COM’s STA (Single-Threaded Apartment) and MTA (Multi-Threaded Apartment) are the two forms of the apartment model, which decides from which thread a component may be called. An STA holds one apartment per thread, and calls that cross an apartment boundary are marshaled through a proxy/stub. An STA that is called from another thread cannot receive the forwarded call unless it is running a message loop, so it hangs, and the pattern where a callback arrives during a synchronous call is also structurally prone to deadlock. An MTA lets several threads share a single apartment, but in exchange it requires the object itself to be designed to be thread-safe. The [STAThread] attribute in .NET is nothing more than a wrapper that configures this apartment initialization.
flowchart LR
accTitle: COM STA/MTA apartment models
accDescr: Diagram showing how the STA and MTA apartment models of COM relate to the message loop, marshaling, and the proxy/stub, and how the absence of a message loop leads to a hang or a deadlock in an STA
sta["STA (Single-Threaded Apartment)"]
mta["MTA (Multi-Threaded Apartment)"]
com["COM (Component Object Model)"]
com_apartment_model["COM apartment model (STA/MTA)"]
coinitializeex["CoInitializeEx"]
message_loop["Message Loop"]
proxy_stub["Proxy/Stub"]
thread_safe_com_object["Thread-Safe COM Object Design"]
windows_forms["Windows Forms"]
stathread_attribute["[STAThread] / [MTAThread] Attributes"]
threadingmodel_registry["ThreadingModel Registry Value"]
com_automation["OLE Automation"]
idispatch["IDispatch"]
midl["MIDL"]
sta_hang["STA Hang (Stalled Call Marshaling)"]
msgwaitformultipleobjects["MsgWaitForMultipleObjects"]
sta_callback_deadlock["Callback Deadlock During Synchronous Call"]
dotnet[".NET (Core and Later)"]
com_marshaling["Marshaling"]
com -->|"uses"| com_apartment_model
com_apartment_model -->|"requires"| coinitializeex
sta -->|"requires"| message_loop
sta -.->|"uses"| proxy_stub
mta -->|"requires"| thread_safe_com_object
windows_forms -.->|"uses"| sta
windows_forms -->|"uses"| message_loop
stathread_attribute -->|"uses"| coinitializeex
threadingmodel_registry -.->|"requires"| sta
com_automation -->|"uses"| idispatch
proxy_stub -.->|"configured by"| midl
sta -.->|"may cause"| sta_hang
message_loop -->|"prevents"| sta_hang
msgwaitformultipleobjects -.->|"prevents"| sta_hang
sta -.->|"may cause"| sta_callback_deadlock
dotnet -.->|"may cause"| sta_hang
mta -.->|"uses"| proxy_stub
stathread_attribute -->|"requires"| com_apartment_model
proxy_stub -->|"implements"| com_marshaling
com_automation -->|"implements"| com_marshaling
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 (20 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
1. The Conclusion First (In One Line)
- A COM object’s call rules are determined by which apartment it belongs to
- It is easiest to think of STA as one apartment per thread, and MTA as one apartment shared by multiple threads
- For calls that cross apartments, COM marshals them through a proxy/stub
flowchart TB
accTitle: How STA, MTA, and cross-apartment calls relate
accDescr: Diagram showing the conclusion that STA takes the form of one apartment per thread and MTA the form of one apartment shared by multiple threads, and that COM marshals cross-apartment calls through a proxy/stub.
sta["STA (one apartment per thread)"] -->|"Call across apartments"| m["Marshaled through a proxy / stub"]
mta["MTA (one apartment for multiple threads)"] -->|"Call across apartments"| m
Figure 2: The apartment an object belongs to decides its call rules, and marshaling is inserted only when a call crosses apartments.
2. Call Patterns in the Apartment Model (Diagrams)
There are broadly three patterns for calling a COM object.
flowchart TB
accTitle: The three call patterns
accDescr: Diagram showing that calls to a COM object fall into three patterns: calls within the same STA thread, calls within the same MTA, and calls that cross apartments.
caller["Calling a COM object"] --> p1["Pattern 1: within the same STA thread"]
caller --> p2["Pattern 2: within the same MTA"]
caller --> p3["Pattern 3: across apartments"]
Figure 3: Calls split into three patterns, and which one applies changes both the overhead and what you have to watch for.
2.1. Pattern 1: Calls Within the Same STA Thread
Within the same STA thread, calls are direct. No overhead.
flowchart LR
subgraph STA[STA thread]
Caller[Calling code]
Obj[COM object]
Caller -->|Direct call| Obj
end
Figure 4: A call within the same STA thread is a direct call with no overhead.
2.2. Pattern 2: Calls Within the Same MTA
From multiple threads inside the MTA, any thread can call directly. However, the object itself must be designed to be thread-safe.
flowchart LR
subgraph MTA[MTA - one apartment]
Thread1[Worker thread 1]
Thread2[Worker thread 2]
Obj[COM object]
Thread1 -->|Direct call| Obj
Thread2 -->|Direct call| Obj
end
Figure 5: Within the same MTA any thread can call directly, but the object needs a thread-safe design.
2.3. Pattern 3: Calls Across Apartments
Between different apartments, COM forwards the call using a proxy/stub. For standard interfaces, the COM runtime handles this for you.
The table further down uses COM-specific vocabulary without explaining it, so here is one line for each term first.
| Term | Meaning |
|---|---|
| Marshaling | Repackaging a call and its arguments into a form that can be handed across an apartment or process boundary, and carrying them over. They are reconstructed into their original form on the other side |
| Proxy / stub | The pair of components responsible for marshaling. The proxy stands on the calling side (it impersonates the real object and receives the call), and the stub stands on the called side (it passes the received call to the real object) |
IDispatch |
The COM interface for looking up a method name as a string and then invoking it by number. This mechanism is what lets scripting languages and VBA use COM |
| Automation | The umbrella term for using COM on top of IDispatch and the limited set of data types available there (BSTR, VARIANT, and so on). As long as you stay within that range, the OS handles marshaling through oleaut32.dll |
| Type library | Machine-readable data that describes the shape of an interface (its methods and argument types). It ships either as a standalone .tlb file or embedded in a DLL or EXE |
| Type library marshaler | The standard COM facility that reads a type library and marshals on the spot. It is the reason you can get by without building a dedicated proxy/stub |
| MIDL | Microsoft’s compiler that generates proxy/stub code and similar output from an interface definition (.idl) |
Note: Proxies/stubs are not automatically available for everything, but in practice you rarely need to generate them explicitly.
| Pattern | Proxy/stub preparation |
|---|---|
IDispatch-based (Automation) |
Not needed. oleaut32.dll handles it |
| Type library registered | Not needed. The type library marshaler handles it |
| .NET COM Interop | Usually not needed. Works via the type library |
Custom interface deriving directly from IUnknown |
Proxy/stub generation and registration via MIDL required |
In other words, you only need MIDL-generated proxies/stubs when you create an interface that derives directly from IUnknown without using IDispatch.
For typical COM components consumed from .NET or scripting languages, this work is rarely necessary.
flowchart TB
accTitle: When you have to prepare a proxy/stub yourself
accDescr: Diagram showing that no proxy/stub generation is needed when standard COM marshaling covers the case, such as IDispatch-based interfaces or the type library marshaler, and that MIDL generation and registration is required only for a custom interface derived directly from IUnknown that standard marshaling cannot cover.
q{"Can standard marshaling cover it"} -->|"Yes"| auto["No proxy/stub generation needed"]
q -->|"No"| midl["Generate and register a proxy/stub with MIDL"]
auto -.-> how["IDispatch or the type library handles it"]
auto -.-> few["This is nearly always the case in practice"]
Figure 6: You generate a proxy/stub explicitly only for a custom interface derived directly from IUnknown that standard marshaling cannot cover.
flowchart LR
subgraph STA[STA thread]
StaCaller[Calling code]
end
subgraph RT[COM runtime - automatic]
Proxy[Proxy]
RPC[RPC/IPC]
Stub[Stub]
Proxy --> RPC --> Stub
end
subgraph MTA[MTA thread]
MtaObj[COM object]
end
StaCaller -->|Call| Proxy
Stub -->|Forward| MtaObj
Figure 7: A call that crosses apartments is forwarded through the proxy, RPC, and stub that the COM runtime sets up automatically.
Key point: Crossing apartments incurs marshaling overhead. For high-frequency calls this affects performance, so it needs to be considered at design time.
2.4. Rough Marshaling Overhead Figures
The following are general ballpark figures (not measured values; they vary greatly depending on the situation and parameter complexity).
| Call pattern | Rough time | Relative feel |
|---|---|---|
| Same apartment (direct) | 10-100 nanoseconds | About the same as a normal function call |
| Different apartments (same process) | 1-10 microseconds | 100-1000x a direct call |
| Different processes (out-of-proc) | 100-1000 microseconds | 10,000-100,000x a direct call |
Relative comparison:
- Same apartment: about one memory access
- Different apartments: about one system call
- Different processes: about a network round trip to localhost
In a scenario like calling 10,000 times in a loop, this difference becomes very noticeable.
How to treat these numbers
The table above is there to give you a feel for the order of magnitude; it is not a sourced measurement. There is no published standard benchmark for this either, so do not use the numbers themselves as the basis for a design decision.
What is documented, on the other hand, is the structure: calls inside the same apartment are direct, and crossing a boundary always inserts marshaling - that part is specified in Microsoft’s documentation. The Single-Threaded Apartments topic states explicitly that an interface pointer can be passed within the same apartment without marshaling, that crossing apartments uses the same marshaling machinery as a cross-process call even inside a single process, and that the call arrives as a window message to a hidden window (window class OleMainThreadWndClass). That structure is why the order of magnitude changes, while the numbers themselves move with the environment and the complexity of the arguments.
When you want a judgment for your own case, the reliable approach is to measure the same interface called from within the same apartment and from a different one, and compare the two.
flowchart TB
accTitle: The structural reason the order of magnitude changes
accDescr: Diagram showing the structural reason the order of magnitude changes: a call inside the same apartment is a direct call, while a call across apartments always inserts marshaling even within a single process, and a call bound for an STA arrives as a window message to a hidden window.
same["Call inside the same apartment"] --> direct["Direct call"]
cross["Call across apartments"] --> marshal["Marshaling is always inserted"]
marshal -.-> msg["Calls bound for an STA arrive at a hidden window"]
Figure 8: The order of magnitude changes because of the specified structure - crossing a boundary always inserts marshaling.
// C#. Repeat the same call N times and derive the time per call
static void Measure(string label, Action call, int iterations = 100_000)
{
call(); // Keep the first-call latency (JIT, proxy creation, connection setup) out of the measurement
var sw = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
{
call();
}
sw.Stop();
double perCallNs = sw.Elapsed.TotalMilliseconds * 1_000_000.0 / iterations;
Console.WriteLine($"{label}: {perCallNs:F1} ns/call");
}
Measuring both a method with few arguments and a method that passes strings or arrays also shows how the impact scales with the amount of marshaling.
3. STA (Single-Threaded Apartment)
STA is the “one thread = one apartment” model.
- COM objects in that apartment fundamentally execute only on that thread
- When called from another thread, COM forwards the call via the message queue/RPC
- Commonly used on UI threads (WinForms/WPF) - the UI also has “single-thread affinity plus a message loop,” so the fit is natural
flowchart TB
accTitle: The STA execution model
accDescr: Diagram showing that in an STA a COM object fundamentally executes only on the thread that created it, and that COM forwards calls from other threads to that thread via the message queue or RPC.
obj["COM object in an STA"] --> own["Executes only on the thread that created it"]
other["Call from another thread"] --> fwd["Forwarded via the message queue / RPC"]
fwd --> own
Figure 9: An STA object runs only on its owning thread, and calls from elsewhere reach it by being forwarded.
3.1. Why STA Is Used on UI Threads
Because the UI thread and STA share the same design.
- UI controls are not thread-safe Buttons, text boxes, and so on can only be safely manipulated from the thread that created them
- STA likewise has “single-thread affinity” COM objects execute directly only on the thread that created them
- The UI thread always pumps a message loop This is required to handle window events, and matches STA’s prerequisite (a message pump)
That is why the UI thread in WinForms/WPF is STA by default.
flowchart TB
accTitle: How the UI thread and STA designs line up
accDescr: Diagram showing that UI controls can only be manipulated safely from the thread that created them, that STA has the same single-thread affinity, and that the message loop a UI thread always pumps matches STA's prerequisite of a message pump, which is why the UI thread is STA by default.
ui["Single-thread affinity of UI controls"] --> match["The designs line up"]
sta["Single-thread affinity of STA"] --> match
loop["Message loop on the UI thread"] --> pre["STA prerequisite of a message pump"]
pre --> match
match --> def["The UI thread is STA by default"]
Figure 10: The designs line up on two points, single-thread affinity and the message loop, which is why UI threads are STA.
Key point: STA gives you strong thread affinity, but in exchange it tends to become congested when there are many callers.
4. MTA (Multi-Threaded Apartment)
MTA is the “multiple threads in one apartment” model.
- COM objects get called concurrently from multiple threads
- The object must be designed to be thread-safe
- Suited to server-side and background processing
Key point: MTA gives high parallelism, but places a heavy burden on the object’s implementation.
flowchart TB
accTitle: The MTA execution model
accDescr: Diagram showing that an MTA shares one apartment across multiple threads, so a COM object is called concurrently from several threads and the object itself must be designed to be thread-safe.
mta["MTA (one apartment for multiple threads)"] --> par["Called concurrently from multiple threads"]
par --> safe["The object must be designed to be thread-safe"]
mta -.-> use["Suited to server-side and background processing"]
Figure 11: MTA lets you call in parallel, but shifts the responsibility for safety onto the object implementation.
5. Where STA/MTA Gets Decided
A COM apartment is decided by initializing each thread.
- The moment you call
CoInitialize/CoInitializeEx, that thread’s apartment is decided - STA:
COINIT_APARTMENTTHREADED - MTA:
COINIT_MULTITHREADED
flowchart TB
accTitle: The moment an apartment is decided
accDescr: Diagram showing that a thread's apartment is decided the moment it calls CoInitialize or CoInitializeEx, becoming an STA with COINIT_APARTMENTTHREADED and an MTA with COINIT_MULTITHREADED.
th["Thread"] --> init["Calls CoInitialize / CoInitializeEx"]
init -->|"COINIT_APARTMENTTHREADED"| sta["Decided as STA"]
init -->|"COINIT_MULTITHREADED"| mta["Decided as MTA"]
Figure 12: An apartment is decided per thread, by what you pass the moment initialization is called.
5.1. STA/MTA in .NET
.NET also has the [STAThread] / [MTAThread] attributes and ApartmentState, but these are wrappers for configuring COM’s apartment model.
[STAThread]→ applied to the Main method (entry point). The thread is initialized as STA when COM is used[MTAThread]→ likewise for the Main method. Initialized as MTAThread.SetApartmentState(ApartmentState.STA)→ for additional threads you create. Must be set before the thread starts
Caveats:
- Even with
[STAThread], nothing is initialized until COM is actually used (it has no effect if you never touch COM) [STAThread]has no effect on additional threads. UseThread.SetApartmentState
In other words, .NET’s STA/MTA is COM’s STA/MTA itself - a mechanism provided for COM Interop.
Important: You cannot change an apartment afterwards. The first initialization is everything.
flowchart TB
accTitle: Where you set the apartment in .NET
accDescr: Diagram showing that the Main method carries the STAThread or MTAThread attribute while additional threads are set with Thread.SetApartmentState before they start, and that in both cases the apartment is fixed by the first initialization and cannot be changed afterwards.
main["Main method"] --> attr["STAThread / MTAThread attribute"]
newth["Additional thread you create"] --> setap["Thread.SetApartmentState before start"]
attr --> fixed["The apartment is fixed by the first initialization"]
setap --> fixed
fixed -.-> nochange["Cannot be changed afterwards"]
Figure 13: The entry point uses an attribute and additional threads use SetApartmentState, and both are settled by that first call.
6. A Concrete Example of a Hang Caused by Getting STA Wrong
A setup like the following is genuinely prone to hangs.
6.1. The Typical Situation
- A background STA thread is created and a COM object is instantiated on it
- That thread is not pumping a message loop
- Another thread (whether STA or MTA) calls that COM object
flowchart TB
accTitle: The setup that tends to hang
accDescr: Diagram showing the hang-prone setup where a background STA thread creates a COM object but is not pumping a message loop, and another thread calls that COM object.
bg["Background STA thread"] --> gen["Creates a COM object"]
bg --> noloop["Not pumping a message loop"]
other["Another thread (STA or MTA)"] -->|"Calls"| gen
Figure 14: The dangerous combination is calling an object owned by a non-pumping STA from another thread.
6.2. What Happens
The reason for the hang comes down to two STA prerequisites. This section is where that reason is explained; the sections that follow do not repeat it.
- A COM object is processed on the STA thread that created it
Whether the caller is STA or MTA, a call from another thread is always forwarded to that STA thread. It arrives as a window message to the hidden window COM creates for that apartment (window class
OleMainThreadWndClass) - To receive that forwarded call, the STA thread has to be pumping messages Microsoft’s documentation states it plainly: every STA must have a message loop in order to handle calls from other processes and from other apartments in the same process
So an STA thread that is not pumping messages cannot receive the call, the caller keeps waiting for a reply, and the result is a hang.
flowchart TB
accTitle: The path to a hang
accDescr: Diagram showing that a call from another thread is forwarded to the STA thread that created the object as a window message to a hidden window, but the STA cannot receive it while no message pump is running, so the caller keeps waiting for a reply and the process hangs.
caller["Call from another thread"] --> fwd["Forwarded to the STA thread that created the object"]
fwd -.-> hidden["Arrives as a message to the hidden window"]
fwd --> nopump["No message pump is running"]
nopump --> norecv["The STA side cannot receive the call"]
norecv --> wait["The caller keeps waiting for a reply"]
wait --> hang["Hang"]
Figure 15: The forwarded call arrives as a message, so on an STA whose pump has stopped there is nobody left to receive it.
The same holds in .NET. The Single-Threaded Apartments documentation carries a warning that blocking an STA thread with Task.Wait(), Task.Result, Thread.Sleep(), ManualResetEvent.WaitOne(), and the like prevents COM callbacks and cross-apartment calls from completing, which deadlocks. The failure example in 6.3 below is stuck in WaitOne() for exactly this reason.
A UI thread, by contrast, pumps a message loop from the start in order to handle window events, so it satisfies STA’s requirement with no extra implementation. That is why the UI thread is a natural place to run an STA COM object.
6.3. Pseudocode (The Classic Failure Pattern)
using System;
using System.Runtime.InteropServices;
using System.Threading;
internal static class StaHangDemo
{
private const uint COINIT_APARTMENTTHREADED = 0x2;
[DllImport("ole32.dll")]
private static extern int CoInitializeEx(IntPtr pvReserved, uint dwCoInit);
[DllImport("ole32.dll")]
private static extern void CoUninitialize();
public static void Run(string progId)
{
var ready = new AutoResetEvent(false);
var done = new AutoResetEvent(false);
object comObj = null;
var staThread = new Thread(() =>
{
// Initialize as STA
CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);
// Assumes a COM class registered with ThreadingModel=Apartment
Type type = Type.GetTypeFromProgID(progId, throwOnError: true);
comObj = Activator.CreateInstance(type);
ready.Set();
// Waiting with no message loop -> this is the fatal flaw
done.WaitOne();
CoUninitialize();
});
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
ready.WaitOne();
try
{
// Calling from another thread (STA or MTA) forwards the call to the STA
// The STA side does not process messages, so this is where it tends to hang
dynamic obj = comObj;
obj.AnyMethod();
}
finally
{
// Even when the call does return - the class turned out to be agile,
// the call was not forwarded, AnyMethod returned immediately - always
// release the STA thread. Skip this and a foreground thread is left
// waiting on done, so the process never exits even when the hang did
// not reproduce, and the symptom no longer tells the two cases apart
done.Set();
staThread.Join();
}
}
}
This code assumes you pass the ProgID of a COM class registered with ThreadingModel=Apartment (that is, STA). Replace AnyMethod with a method the class actually has. It does not reproduce with every COM class. Three conditions have to hold.
| Condition | How to check |
|---|---|
The target class has ThreadingModel set to Apartment |
Look at the ThreadingModel value under HKEY_CLASSES_ROOT\CLSID\{CLSID}\InprocServer32 in the registry. With Both or Free the call is not forwarded, so it does not reproduce |
| The STA thread is not pumping messages | done.WaitOne() in the example above is exactly that |
| The call is made from a different thread | As long as you call from inside the same STA thread it is a direct call, so it does not hang |
obj.AnyMethod() is wrapped in try / finally so that done.Set() and staThread.Join() always run, and the point of that is to tell the non-reproducing case apart. An STA thread is a foreground thread by default, so the process does not exit until done is signaled. Drop the finally and the externally visible symptom becomes the same - the process never exits - both when the hang reproduces (the call never returns) and when it does not (the call returned, but nobody signals done). The tool you built to confirm the reproduction conditions ends up hiding whether they were met. With the form above, a run that does not reproduce simply exits normally.
The quickest way to confirm that it is stuck is to break into the process with a debugger and look at the stacks: the calling thread parked in a COM wait, and the STA thread parked in WaitOne.
sequenceDiagram
participant Main as Main thread
participant STA as STA thread
participant COM as COM runtime
Main->>STA: Start thread
STA->>STA: CoInitializeEx (STA)
STA->>STA: Create COM object
STA->>Main: ready.Set()
STA->>STA: Waiting on done.WaitOne()
Note over STA: No message loop<br/>Stuck right here
Main->>COM: CallComObject()
COM->>STA: Tries to forward the call
Note over COM: Forwards via a message, but...
Note over STA: Stuck in WaitOne, so<br/>cannot process messages
Note over Main: The caller keeps waiting too
Note over Main,STA: Both are waiting → hang
Figure 16: The STA thread stuck in WaitOne and the main thread waiting for the forwarded call can no longer make progress.
The two lines in the middle of the diagram, around “Forwards via a message, but…”, are where the two prerequisites from 6.2 break down.
6.4. Key Points for Avoiding It
- If the STA thread will receive calls from other threads, it must pump a message loop
- If possible, create and use the object on the UI thread (which has a message loop from the start)
- If you do not need STA, use MTA from the beginning
Note: If everything stays within a single thread, Application.Run() is not always required.
However, UI-related and COM-related code so often involves calls from other threads that in practice it is nearly mandatory.
flowchart TB
accTitle: Three directions for avoiding the hang
accDescr: Diagram showing three directions for avoiding the hang: pump a message loop on the STA thread when it receives calls from other threads, create and use the object on the UI thread which has a message loop from the start, or use MTA from the beginning when STA is not required.
q["How to avoid an STA hang"] --> a1["Pump a message loop on the STA thread"]
q --> a2["Create and use the object on the UI thread"]
q --> a3["Use MTA from the beginning if STA is not required"]
a2 -.-> why["The UI thread has a loop from the start"]
Figure 17: The workarounds sort into three directions: pump the loop, move to where a loop already runs, or stop using STA.
6.5. What Does “Pumping the Message Loop” Actually Mean?
It is this familiar pattern that every Win32 UI thread runs.
while (GetMessage(out var msg, IntPtr.Zero, 0, 0))
{
TranslateMessage(ref msg);
DispatchMessage(ref msg);
}
In STA, calls from other threads arrive as “forwarded” work. This loop (the message pump) is what receives that forwarded work and dispatches it for execution.
flowchart TB
accTitle: What the message pump does
accDescr: Diagram showing that the message pump is the repeated cycle of receiving a forwarded call with GetMessage and dispatching it for execution with DispatchMessage.
fwd["Forwarded call"] --> gm["Received by GetMessage"]
gm --> dm["Dispatched for execution by DispatchMessage"]
dm --> gm
Figure 18: Pumping the message loop means exactly this cycle: receive the forwarded call and dispatch it for execution.
6.6. An Example in the Right Direction (Roughly Sketched)
If you want “COM on a background STA,” it looks like this.
var ready = new AutoResetEvent(false);
object comObj = null;
var staThread = new Thread(() =>
{
CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);
comObj = new SomeStaComObject();
ready.Set();
// Pump messages for as long as the STA thread is alive
Application.Run();
CoUninitialize();
});
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
ready.WaitOne();
CallComObject(comObj);
(Note: forgetting to call CoInitializeEx / CoUninitialize is a very real way to hurt yourself. You need the same CoInitializeEx P/Invoke declaration as in 6.3.)
The parameterless form of Application.Run() exists to pump a message loop without owning a form. This is exactly the situation it is meant for, but keep the following two points in mind.
- You need a reference to
System.Windows.Forms. In a console app, add<UseWindowsForms>true</UseWindowsForms>to the project file - This loop never ends on its own. To stop it, call
Application.ExitThread()on that thread (orApplication.Exit()to end the whole application). If you want the example above to reachCoUninitialize(), you need a separate mechanism that signals shutdown to the STA thread and gets it to callExitThread
If you would rather not depend on WinForms, write the GetMessage / DispatchMessage loop from 6.5 yourself, or use MsgWaitForMultipleObjects, which can wait on both messages and synchronization objects. The latter is the standard choice when you want to receive a shutdown signal through an event without dropping any COM calls.
flowchart TB
accTitle: Three ways to pump a loop on a background STA
accDescr: Diagram showing three ways to pump a message loop on a background STA: Application.Run which requires a WinForms reference, writing the GetMessage and DispatchMessage loop yourself, and MsgWaitForMultipleObjects which can wait on both messages and synchronization objects.
want["Pump a loop on a background STA"] --> ar["Application.Run"]
want --> gml["Write the GetMessage loop yourself"]
want --> mw["MsgWaitForMultipleObjects"]
ar -.-> dep["Needs a WinForms reference and a way to stop"]
mw -.-> both["Can wait on both messages and synchronization objects"]
Figure 19: There are three ways to run the loop, and you pick one by how it stops and how heavy the dependency is.
6.7. Another Hang Example: Callbacks During a Synchronous Call
STA is not just about “calls getting forwarded” - depending on the situation, callbacks come back the other way (server to client). Among them, a callback arriving during a synchronous call is the classic deadlock.
sequenceDiagram
participant UI as UI thread (STA)
participant Server as COM server
UI->>Server: DoWork() (synchronous call)
Note over UI: Waiting for DoWork to return<br/>(not processing messages)
Server->>UI: ProgressCallback() (callback)
Note over UI: Waiting, so<br/>cannot receive the callback
Note over Server: Waiting for the callback to complete
Note over UI,Server: Each is waiting on the other → deadlock
Figure 20: The UI thread waiting for a synchronous call to return and the server waiting for the callback to finish end up waiting on each other.
Why this deadlocks so easily:
- The UI thread makes a synchronous (blocking) call to
DoWork() - The UI thread is waiting for the return (not processing messages)
- The server sends
ProgressCallback()to the UI thread - The UI thread is waiting, so it cannot receive the callback
- The server is waiting for the callback to complete
- Each side is waiting on the other, so nothing ever moves
How long the processing takes is irrelevant. The pattern itself - a callback arriving during a synchronous call - is what causes trouble.
Note: COM does have mechanisms that pump messages or allow reentrancy in some situations, and the behavior varies by component and call style. It does not always deadlock, but this pattern is best avoided.
7. A Rough Guide to Choosing
| Situation | What to choose | Why | What else you need |
|---|---|---|---|
| UI is involved (WinForms / WPF) | STA | UI controls have single-thread affinity, and the UI thread has a message loop to begin with (3.1) | Nothing in particular. It is STA by default |
| You want heavy parallel processing | MTA | Multiple threads share one apartment and can call directly with no forwarding (2.2) | A thread-safe design in the COM object. This is the object implementation’s responsibility, not locking on the caller’s side |
| You want to use an STA COM object in the background | STA plus a message loop | Since it is called from another thread, it needs somewhere to receive the forwarded call (6.2) | Application.Run() or a GetMessage loop. Design the shutdown path (ExitThread and so on) along with it (6.6) |
| The COM component you use already dictates the answer | Match the component | The apartment is the call rule itself, and it cannot be changed later (Section 5) | Check the ThreadingModel value. If it is Apartment, build on the assumption of STA |
| You call it at high frequency | Move it into the caller’s apartment | Marshaling is inserted every time a boundary is crossed (2.4) | If that is impractical, design for fewer calls instead by batching arguments into one call |
When you are unsure, the order component requirements > whether UI is involved > parallelism usually settles it. An apartment is fixed by the first initialization and cannot be changed later, so this is the one decision to make before you start implementing.
flowchart LR
accTitle: The order to check when you are unsure
accDescr: Diagram showing that when you are unsure whether to use STA or MTA, checking the requirements of the COM component you use, then whether UI is involved, then parallelism, usually settles it.
p1["Requirements of the component"] -->|"then"| p2["Whether UI is involved"]
p2 -->|"then"| p3["Parallelism"]
Figure 21: When the choice is unclear, check the component’s requirements, then whether UI is involved, then parallelism.
8. Conclusion
STA/MTA is the threading model for COM: STA takes the form of one thread = one apartment, and MTA puts multiple threads in one apartment. Calls that cross apartments are forwarded by COM via proxies/stubs (interfaces outside the standard set need generation and registration with MIDL or the like), but this comes with marshaling overhead, so apartment design deserves careful thought wherever high-frequency calls are expected.
From the hang perspective, it all comes down to one point: an STA thread that receives calls from other threads is expected to pump messages. Calling into an STA thread that is not pumping messages is likely to hang, and the pattern where a callback arrives during a synchronous call easily deadlocks. The UI thread has both “single-thread affinity” and “a message loop” from the start, satisfying these prerequisites with no extra implementation - which is exactly why it gets along so well with STA COM.
9. References
- Apartment Model https://learn.microsoft.com/en-us/windows/win32/com/com-apartments
- CoInitializeEx https://learn.microsoft.com/en-us/windows/win32/api/objbase/nf-objbase-coinitializeex
- Single-Threaded Apartments (why a message loop is required, the hidden window, and the deadlock warning for .NET) https://learn.microsoft.com/en-us/windows/win32/com/single-threaded-apartments
- Multithreaded Apartments https://learn.microsoft.com/en-us/windows/win32/com/multithreaded-apartments
- InprocServer32 (the
ThreadingModelvalues) https://learn.microsoft.com/en-us/windows/win32/com/inprocserver32 - Application.Run method (pumping a message loop without a form, and how to stop it) https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.run
- MsgWaitForMultipleObjects (waiting on messages and synchronization objects at the same time) https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-msgwaitformultipleobjects
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
How the Clipboard and Drag & Drop Work — Handling OLE Data Transfer Correctly in Business Apps
Paste an Excel table and the formatting falls apart; close the source app and you can no longer paste — both come from the clipboard plac...
Windows Shell Integration Today — Context Menus, File Associations, and What Changed in Windows 11
Why a Windows 11 context menu hides items behind "Show more options", explained from the extension → ProgID → verb association basics thr...
Do Business Apps Run on Windows on Arm? — The Reality of x64 Emulation (Prism) and Native DLLs/COM
An answer, aimed at developers and IT staff, to 'will our business app run on Windows on Arm?' Covers how x64 emulation (Prism) works, th...
Windows App Outsourcing and Custom Software Development: What to Sort Out Before You Ask
Before commissioning Windows app outsourcing or custom software development, here is how to sort out existing software modification, devi...
A Developer's Strange Love, or: How I Learned to Stop Worrying and Love Windows
Windows is a hassle. But that hassle is the hassle of an OS that has carried real-world business on its back.
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
ActiveX Migration
Topic page for staged decisions around keeping, wrapping, or replacing COM / ActiveX / OCX assets.
UI Threading & Timers
Topic page for WPF / WinForms UI threading, async flow, Dispatcher usage, and timer decisions.
Where This Topic Connects
This article connects naturally to the following service pages.
Technical Consulting & Design Review
Sorting out STA/MTA, message loops, and marshaling ties directly into pre-implementation responsibility partitioning and thread-boundary reviews.
Legacy Asset Reuse & Migration Support
These fundamentals are hard to avoid when dealing with existing assets that involve COM, so they also pair well with our legacy asset reuse and migration support.
Frequently Asked Questions
Common questions about the topic of this article.
- Should I choose STA or MTA?
- The basic split is STA when UI is involved and MTA for heavy parallel processing. STA has strong thread affinity because it takes the form of one apartment per thread, but it tends to become congested when there are many callers. MTA shares one apartment across multiple threads, so it offers more parallelism, but the COM object itself must be designed to be thread-safe. When neither case applies, the practical answer is to follow whatever the existing libraries or COM servers you use require.
- Why is the UI thread STA?
- Because the UI thread and STA share the same design. UI controls such as buttons and text boxes are not thread-safe and can only be manipulated safely from the thread that created them, and STA is likewise a single-thread-affinity model. On top of that, a UI thread always pumps a message loop to handle window events, so it satisfies STA's prerequisite of a message pump with no extra implementation. That is why UI threads in WinForms and WPF are STA by default.
- Why does calling an STA COM object hang?
- A call to an STA COM object is processed on the STA thread that created it. COM forwards calls from other threads via messages or RPC, but an STA thread that is not pumping a message loop cannot receive the forwarded call, so the caller keeps waiting and hangs. To avoid this, pump a message loop on any STA thread that receives calls from other threads, create and use the object on the UI thread, or use MTA from the start if you do not need STA.
- What is the .NET [STAThread] attribute for?
- It is a wrapper for configuring COM's apartment model. Applied to the Main method, it makes that thread initialize as STA when COM is used. Nothing is initialized until you actually call into COM, however, so it has no effect in an application that never touches COM. It also has no effect on additional threads you create, so set those with Thread.SetApartmentState before the thread starts. Note as well that an apartment is decided by the first initialization and cannot be changed afterwards.