PowerShell has no built-in MQTT cmdlets, but there are two reliable ways to talk to a broker from a Windows script: call the mosquitto_pub / mosquitto_sub executables and treat their output as a pipeline, or load the MQTTnet .NET library straight into PowerShell 7. The first is perfect for one-liners, health checks and scheduled tasks; the second gives you a persistent connection and full control from a single .ps1 file.
This guide covers both, including the two traps that catch almost everyone — quotes disappearing from JSON payloads and event handlers that crash with "no Runspace available". All examples use broker.connectmq.com; keep the online MQTT client open to see the messages arrive.
Which approach?
| mosquitto clients | MQTTnet in PowerShell 7 | |
|---|---|---|
| Install | Mosquitto Windows installer | One DLL from NuGet |
| Works in Windows PowerShell 5.1 | Yes | Not recommended |
| Persistent connection | No (one process per call) | Yes |
| Receiving messages | Read stdout line by line | Queue + polling loop |
| Best for | Alerts, health checks, quick tests | Long-running scripts, tooling, glue code |
If you end up writing more than a page of PowerShell around MQTTnet, a small C# console app is usually the better tool — see MQTT in C# with MQTTnet.
Option 1: mosquitto_pub and mosquitto_sub
Download the Windows installer from mosquitto.org. It installs the broker (you can skip the service) and the command-line clients into C:\Program Files\mosquitto. Add that folder to your PATH and test the connection:
$env:Path += ';C:\Program Files\mosquitto' # current session only
$env:MQTT_USER = 'username'
$env:MQTT_PASS = 'password'
Test-NetConnection broker.connectmq.com -Port 1883 | Select-Object TcpTestSucceeded
mosquitto_pub -h broker.connectmq.com -u $env:MQTT_USER -P $env:MQTT_PASS `
-t 'testmqtt/ps/hello' -m 'Hello from PowerShell' -q 1
"exit code: $LASTEXITCODE"$LASTEXITCODE is 0 on success and non-zero when the connection or authentication fails, which is all a script needs. If TcpTestSucceeded is False, the problem is the network rather than MQTT — check whether the port is open and the Windows firewall. If you want to run a broker on the same machine, MQTT broker on Windows covers the service setup.
Sending JSON without losing the quotes
This is the most common PowerShell-specific MQTT bug. In Windows PowerShell 5.1 and PowerShell 7.0–7.2, embedded double quotes are stripped when arguments are passed to a native .exe, so the subscriber receives {device:pc-01} instead of valid JSON. PowerShell 7.3 fixed argument passing, but the robust solution works everywhere: build the object with ConvertTo-Json and pipe it into mosquitto_pub -s, which reads the message from standard input.
$payload = @{
device = $env:COMPUTERNAME
cpu = (Get-CimInstance Win32_Processor | Measure-Object LoadPercentage -Average).Average
ts = (Get-Date).ToUniversalTime().ToString('o')
} | ConvertTo-Json -Compress
$payload | mosquitto_pub -h broker.connectmq.com -u $env:MQTT_USER -P $env:MQTT_PASS `
-t "testmqtt/ps/$env:COMPUTERNAME/status" -q 1 -sPowerShell adds a trailing newline when piping to a native program; JSON parsers ignore it. Use -l instead of -s to send every input line as a separate message.
Processing incoming messages in a pipeline
mosquitto_sub -v prints topic payload per line, and PowerShell turns each line into a pipeline object:
mosquitto_sub -h broker.connectmq.com -u $env:MQTT_USER -P $env:MQTT_PASS `
-t 'testmqtt/ps/+/status' -v |
ForEach-Object {
$topic, $json = $_ -split ' ', 2
$status = $json | ConvertFrom-Json
if ($status.cpu -gt 90) {
Write-Warning "$($status.device) CPU at $($status.cpu)%"
}
}For a check that waits for exactly one message, add -C 1 -W 10: mosquitto_sub exits after the first message or after ten seconds, with a non-zero exit code on timeout. More flags are listed in mosquitto_pub and mosquitto_sub examples.
Option 2: MQTTnet inside PowerShell 7
MQTTnet is the most widely used .NET MQTT library. PowerShell 7 runs on modern .NET, so it can load the DLL directly. Download a 4.x package from NuGet and extract the netstandard2.0 build:
$version = '4.3.7.1207' # any MQTTnet 4.x version listed on nuget.org
$dir = Join-Path $HOME 'mqttnet'
Invoke-WebRequest "https://www.nuget.org/api/v2/package/MQTTnet/$version" -OutFile "$env:TEMP\mqttnet.zip"
Expand-Archive "$env:TEMP\mqttnet.zip" -DestinationPath $dir -Force
$dll = Join-Path $dir 'lib\netstandard2.0\MQTTnet.dll'
Add-Type -Path $dllConnecting and publishing work directly from PowerShell. Async methods return tasks, so call .GetAwaiter().GetResult() to wait for them (it also surfaces the real exception instead of an AggregateException):
$factory = [MQTTnet.MqttFactory]::new()
$client = $factory.CreateMqttClient()
$options = [MQTTnet.Client.MqttClientOptionsBuilder]::new()
.WithTcpServer('broker.connectmq.com', 1883)
.WithClientId("testmqtt-ps-$env:COMPUTERNAME")
.WithCredentials($env:MQTT_USER, $env:MQTT_PASS)
.Build()
$none = [Threading.CancellationToken]::None
$connect = $client.ConnectAsync($options, $none).GetAwaiter().GetResult()
"connect: $($connect.ResultCode)"
$message = [MQTTnet.MqttApplicationMessageBuilder]::new()
.WithTopic('testmqtt/ps/hello')
.WithPayload('Hello from MQTTnet')
.WithQualityOfServiceLevel([MQTTnet.Protocol.MqttQualityOfServiceLevel]::AtLeastOnce)
.Build()
$result = $client.PublishAsync($message, $none).GetAwaiter().GetResult()
"publish: $($result.ReasonCode)"The leading-dot method chaining requires PowerShell 7. A failed login throws an exception such as Connecting with MQTT server failed (BadUserNameOrPassword). or (NotAuthorized); the MQTT reason code lookup explains each value.
Receiving messages without the runspace crash
The obvious approach — $client.add_ApplicationMessageReceivedAsync({ ... }) — fails sooner or later with:
There is no Runspace available to run scripts in this thread.
MQTTnet raises the event on a .NET thread-pool thread, and a PowerShell script block can only run on a thread that owns a runspace. The clean fix is a few lines of C# that put each message in a thread-safe queue; your PowerShell loop takes them out on its own thread.
Add-Type -ReferencedAssemblies $dll -TypeDefinition @'
using System.Collections.Concurrent;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MQTTnet.Client;
using MQTTnet.Protocol;
public static class MqttInbox
{
public static readonly BlockingCollection<string[]> Messages = new BlockingCollection<string[]>();
public static void Attach(IMqttClient client)
{
client.ApplicationMessageReceivedAsync += e =>
{
var seg = e.ApplicationMessage.PayloadSegment;
var text = seg.Array == null ? "" : Encoding.UTF8.GetString(seg.Array, seg.Offset, seg.Count);
Messages.Add(new[] { e.ApplicationMessage.Topic, text });
return Task.CompletedTask;
};
}
public static MqttClientSubscribeResult Subscribe(IMqttClient client, string filter)
{
return client.SubscribeAsync(filter, MqttQualityOfServiceLevel.AtLeastOnce, CancellationToken.None)
.GetAwaiter().GetResult();
}
public static void Disconnect(IMqttClient client)
{
client.DisconnectAsync().GetAwaiter().GetResult();
}
}
'@
[MqttInbox]::Attach($client) # attach before subscribing
[MqttInbox]::Subscribe($client, 'testmqtt/ps/#') | Out-Null
Write-Host 'Listening on testmqtt/ps/# - Ctrl+C to stop'
try {
while ($true) {
$msg = $null
if ([MqttInbox]::Messages.TryTake([ref]$msg, 500)) {
'{0,-35} {1}' -f $msg[0], $msg[1]
}
}
}
finally {
[MqttInbox]::Disconnect($client)
$client.Dispose()
}The 500 ms timeout keeps the loop responsive to Ctrl+C, and the finally block sends a clean DISCONNECT so the broker does not fire a Last Will. Add-Type can define a type only once per session; running the script twice in the same console gives Cannot add type. The type name 'MqttInbox' already exists. — start a new pwsh session, or guard the call with if (-not ('MqttInbox' -as [type])).
Publish to testmqtt/ps/hello from the online client and the line should appear within a second.
Send a message to your PowerShell listener
TLS
Switch to port 8883 and enable TLS in the options builder. MQTTnet validates the broker certificate against the Windows certificate store, so a broker with a public certificate needs nothing else:
$options = [MQTTnet.Client.MqttClientOptionsBuilder]::new()
.WithTcpServer('broker.connectmq.com', 8883)
.WithTls()
.WithClientId("testmqtt-ps-$env:COMPUTERNAME")
.WithCredentials($env:MQTT_USER, $env:MQTT_PASS)
.Build()A scheduled heartbeat
Combining the pieces: a script that publishes machine status, and a scheduled task that runs it every five minutes.
$payload = @{
device = $env:COMPUTERNAME
freeGB = [math]::Round((Get-PSDrive C).Free / 1GB, 1)
ts = (Get-Date).ToUniversalTime().ToString('o')
} | ConvertTo-Json -Compress
$payload | mosquitto_pub -h broker.connectmq.com -p 8883 --cafile 'C:\mqtt\ca.pem' `
-u $env:MQTT_USER -P $env:MQTT_PASS -t "testmqtt/ps/$env:COMPUTERNAME/status" -q 1 -s
if ($LASTEXITCODE -ne 0) {
$line = '{0:o} mosquitto_pub failed with exit code {1}' -f (Get-Date), $LASTEXITCODE
Add-Content -Path 'C:\mqtt\heartbeat.log' -Value $line
exit $LASTEXITCODE
}$action = New-ScheduledTaskAction -Execute 'pwsh.exe' `
-Argument '-NoProfile -NonInteractive -File C:\mqtt\heartbeat.ps1'
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 5)
Register-ScheduledTask -TaskName 'MQTT heartbeat' -Action $action -Trigger $trigger--cafile points to a PEM file containing the CA that signed your broker's certificate. Failures go to a log file, and the non-zero exit code shows up as the task's "Last Run Result" in Task Scheduler. The task above runs as you while you are logged in — for unattended servers, register it with a service account and store the credentials as that account's environment variables or with the SecretManagement module. Subscribe to testmqtt/ps/+/status to watch every machine report in.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
mosquitto_pub : The term 'mosquitto_pub' is not recognized | Install folder not on PATH | Add C:\Program Files\mosquitto to PATH |
Error: Connection refused | Wrong port, or the broker refused the connection | Test-NetConnection -Port; check port vs TLS |
Unable to find type [MQTTnet.MqttFactory] | DLL not loaded, or a 5.x DLL with different namespaces | Run Add-Type -Path first; use a 4.x package |
Connecting with MQTT server failed (BadUserNameOrPassword). | Wrong credentials or env vars empty | Print $env:MQTT_USER; check for typos |
There is no Runspace available to run scripts in this thread. | Script block used as an MQTTnet event handler | Use the C# queue shown above |
| JSON arrives without quotes | Native argument passing in PS 5.1 / 7.0–7.2 | Pipe into mosquitto_pub -s |
PowerShell is a perfectly good MQTT client once you know its quirks: pipe JSON through stdin, check $LASTEXITCODE, and keep MQTTnet events on the .NET side of a queue. For the same automation on Linux, see MQTT in Bash scripts.
Frequently asked questions
Can PowerShell publish MQTT messages without extra software?
Not natively — PowerShell has no MQTT cmdlets. The two practical options are the mosquitto_pub and mosquitto_sub executables from the Mosquitto Windows installer, or loading the MQTTnet .NET library into PowerShell 7 with Add-Type.
Why do I get "There is no Runspace available to run scripts in this thread"?
You attached a PowerShell script block to an MQTTnet event. MQTTnet raises events on .NET thread-pool threads that have no PowerShell runspace, so the script block cannot run. Queue messages in a small C# helper and read the queue from your PowerShell loop instead.
Why does mosquitto_pub receive my JSON without quotes in PowerShell?
Windows PowerShell 5.1 and PowerShell 7.0 to 7.2 strip embedded double quotes when passing arguments to native programs. Upgrade to PowerShell 7.3 or newer, or pipe the JSON into mosquitto_pub -s so it is read from standard input.


