Key Takeaways
- Fastify’s low overhead makes it well-suited for holding large numbers of open WebSocket connections without running into memory issues.
- PM2 keeps a Node.js process alive in production with instant crash recovery, zero-downtime restarts, and clean log management.
- A mini telemetry dashboard built with Fastify and a WebSocket route demonstrates how live data streams to a browser without polling.
- Cloudways Velocity handles PM2 daemonization and reverse proxy routing automatically, so a Node.js WebSocket app can go from GitHub to production with no manual server config.
Building a real-time Node.js application on your local machine is fairly straightforward. Install a package, open a port, and start streaming data to a client. Deploying that same application to production, however, is where things become more challenging.
Unlike standard HTTP requests that are completed in milliseconds, WebSockets keep connections open for as long as the client and server need to communicate. If your application crashes or the Node.js process stops, every connected client loses its connection instantly.
Keeping a Fastify application reliable in production requires more than just a lightweight framework. You also need a process manager that keeps your application running in the background and automatically restarts it if something goes wrong.
In this guide, I’ll explain how Fastify handles WebSockets, why PM2 for Node.js is an important part of running applications in production, build a simple Node.js WebSocket project, and then deploy the entire stack to Cloudways Velocity, where PM2 is built in.
How WebSockets Affect Your Server Infrastructure
Standard HTTP REST architectures operate on a strict request-response lifecycle. A client initiates a TCP handshake, sends headers and body data, waits for the server payload, and closes the connection.
If a client needs updated state every few seconds, it has to repeat that full handshake over and over.
WebSockets flip traditional HTTP on its head. Instead of opening and closing a brand new request every single time you need data, they start with a quick HTTP handshake, upgrade the protocol, and leave that pipe wide open.
| HTTP Connection | WebSocket Connection |
| Client → Request → Server | Client → Upgrade Request → Server |
| Client ← Response ← Server | Client ← 101 Switching Protocols ← Server |
| Connection closes | Connection stays open |
| A new connection is created for every request | The same connection is reused |
| Communication is request → response only | Client and server can exchange data in both directions at any time |
Now, holding a connection open forever sounds great until you look at what it actually does to your server behind the scenes:
Memory builds up fast. Every single socket you keep open takes up RAM. If your framework is bloated, your server runs out of headroom way before you reach serious scale.
One crash drops everyone. With standard REST APIs, if an unhandled error happens on a request, one user sees a 500 error. But if an unmanaged WebSocket server crashes? Boom. The whole process dies, and thousands of active users get disconnected at the exact same millisecond.
Proxies love killing idle connections. Load balancers, firewalls, and cloud proxies will silently cut TCP lines if nothing moves across them for a couple of minutes. You have to send periodic “ping/pong” heartbeats just to keep the channel alive.
This is precisely where Fastify shines. Because its core routing tree and JSON parsing have practically zero bloat, it uses way less memory per connection than older frameworks—making it ideal for keeping massive amounts of sockets open.
Why PM2 is Mandatory for Node.js in Production
Since Node runs on a single event loop thread, an unhandled rejection will crash your entire application. Period.
And obviously, you can’t just SSH into a live server, run node server.js in a terminal window, and close your laptop. The second your terminal session ends, your app goes down with it.
That’s where PM2 comes in. Think of it as a supervisor sitting right above your Node app, making sure it stays alive no matter what:
Background execution: It daemonizes your process so it runs quietly in the background 24/7.
Instant crash recovery: If an unexpected error sneaks through and kills the thread, PM2 immediately revives the instance in milliseconds.
Zero-downtime updates: You can roll out fresh code and restart instances sequentially, so zero users drop connections mid-flight.
Clean logging: It splits your standard logs and error streams into clean, persistent files without you wiring up custom streams.
Normally, setting all this up on a raw Linux box means dealing with systemd service files, building custom ecosystem.config.js scripts, setting up log rotation, and manually configuring Nginx reverse proxies. With Cloudways Velocity, though, you don’t have to touch any of that—PM2 daemonization and proxy routing are handled automatically under the hood when you deploy.
Skip the DevOps Work, Ship Your App Instead
Cloudways Velocity handles PM2 process management and reverse proxying automatically, so your app stays online without manual setup.
Mini Project: Real-Time Node.js WebSocket App with Fastify
To show how Fastify WebSocket deployment works, I’ll build a mini project as an example. I’m going to keep it simple and build a live telemetry dashboard that pulls the server’s memory usage and uptime and pushes it to a webpage.
For this setup, I’ll build and test everything locally first using VS Code and my local Node environment. Then I’ll push it to GitHub and deploy it live on Cloudways Velocity.
Step 1: Setting Up the Fastify Environment
Time to get the folder set up. Since I have IT restrictions on my work laptop, I’m using a standalone Node.js binary. What this means is my Command Prompt doesn’t actually know where my Node tools are by default.
To fix that, I’ll open CMD and tell it exactly where I unzipped my Node folder. In my case, it is here:
set PATH=%PATH%;C:\Users\abdulrehman\Downloads\node-v24.18.0-win-x64\node-v24.18.0-win-x64

After pointing to the Node folder, I’ll create my project folder and initialize the Node app.
mkdir fastify-pm2-demo cd fastify-pm2-demo npm init -y

Now I’ll install the Fastify framework and the WebSocket plugin using this command:
npm install fastify @fastify/websocket

Once that’s done, I’ll add some scripts to the package.json file so I can start the application with npm run dev during development and npm start in production.
{
"name": "fastify-pm2-demo",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"@fastify/websocket": "^11.3.0",
"fastify": "^5.11.0"
}
}
And with that, the basic environment for my app is ready.
Step 2: Creating the WebSocket Server
I’ll now open the newly created project folder in VS Code.

Inside the folder, I’ll create a file called server.js. This will be the file that’ll contain all the backend logic.
Here is the code I’ll add to my server.js file:
const Fastify = require('fastify');
const fastifyWebsocket = require('@fastify/websocket');
const fs = require('fs');
const path = require('path');
const fastify = Fastify({ logger: true });
fastify.register(fastifyWebsocket, {
options: { maxPayload: 1048576 }
});
// Serve the HTML dashboard on the root URL
fastify.get('/', (req, reply) => {
reply.type('text/html').send(fs.createReadStream(path.join(__dirname, 'index.html')));
});
fastify.register(async function (app) {
app.get('/ws', { websocket: true }, (socket, req) => {
app.log.info('Client connected to WebSocket stream');
socket.isAlive = true;
socket.on('pong', () => {
socket.isAlive = true;
});
const interval = setInterval(() => {
if (socket.readyState === 1) {
const memoryUsage = process.memoryUsage();
socket.send(JSON.stringify({
timestamp: new Date().toISOString(),
uptime: Math.floor(process.uptime()),
heapUsedMb: (memoryUsage.heapUsed / 1024 / 1024).toFixed(2)
}));
}
}, 1000);
socket.on('close', () => {
app.log.info('Client disconnected');
clearInterval(interval);
});
socket.on('error', (err) => {
app.log.error(`Socket error: ${err.message}`);
clearInterval(interval);
});
});
});
const pingInterval = setInterval(() => {
if (!fastify.websocketServer) return;
fastify.websocketServer.clients.forEach((socket) => {
if (socket.isAlive === false) {
return socket.terminate();
}
socket.isAlive = false;
socket.ping();
});
}, 30000);
fastify.addHook('onClose', (instance, done) => {
clearInterval(pingInterval);
done();
});
const start = async () => {
try {
const port = process.env.PORT || 3000;
const host = process.env.HOST || '0.0.0.0';
await fastify.listen({ port: Number(port), host });
console.log(\`Server running on http://host:{host}: host:{port}\`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();

What this code is going to do is open a route at /ws. And every second, it will check the server’s memory and broadcast that data to anyone connected.
Oh and I also added a ping mechanism. By that I mean it checks every 30 seconds to drop dead connections, so the server memory doesn’t just leak and crash over time.
Step 3: Building the HTML Dashboard
Now I’m going to build the logic to display the data that’ll get pulled from the server.
For this, I’ll create an index.html file right next to my server file. I’m going to write some basic JavaScript that connects to the backend. Basically, when it receives a message, it will drop the values into some HTML elements styled with CSS.
So…instead of just letting it fail if the server restarts, I added a reconnect logic. If the connection drops, it will try to reconnect every three seconds.
This is the code I’ll add to my index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fastify WebSocket Dashboard</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: #f4f4f5; padding: 2rem; color: #333; }
.card { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); max-width: 400px; margin: 0 auto; }
h1 { font-size: 1.25rem; margin-top: 0; margin-bottom: 1.5rem; text-align: center; }
.status-container { text-align: center; margin-bottom: 1.5rem; }
.status { font-weight: bold; padding: 6px 12px; border-radius: 999px; display: inline-block; font-size: 0.875rem; }
.status.connected { background: #dcfce7; color: #166534; }
.status.disconnected { background: #fee2e2; color: #991b1b; }
.metric { display: flex; justify-content: space-between; margin-bottom: 0.75rem; border-bottom: 1px solid #e4e4e7; padding-bottom: 0.75rem; }
.metric:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
</style>
</head>
<body>
<div class="card">
<h1>Server Dashboard</h1>
<div class="status-container">
<div id="status" class="status disconnected">Disconnected</div>
</div>
<div class="metric">
<span>Server Uptime</span>
<strong id="uptime">0s</strong>
</div>
<div class="metric">
<span>Heap Memory Usage</span>
<strong id="heap">0 MB</strong>
</div>
<div class="metric">
<span>Last Ping</span>
<strong id="ping">--:--:--</strong>
</div>
</div>
<script>
// Dynamically grab the current host/port so it works on localhost, 127.0.0.1, or a live server
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${window.location.host}/ws`;
let ws;
function connect() {
console.log(`Attempting to connect to ${wsUrl}...`);
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket connected!');
const statusEl = document.getElementById('status');
statusEl.textContent = 'Connected';
statusEl.className = 'status connected';
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// Update the DOM with the live data
document.getElementById('uptime').textContent = data.uptime + 's';
document.getElementById('heap').textContent = data.heapUsedMb + ' MB';
const date = new Date(data.timestamp);
document.getElementById('ping').textContent = date.toLocaleTimeString();
} catch (err) {
console.error("Error parsing WebSocket message:", err);
}
};
ws.onclose = () => {
console.log('WebSocket disconnected. Retrying in 3 seconds...');
const statusEl = document.getElementById('status');
statusEl.textContent = 'Disconnected';
statusEl.className = 'status disconnected';
// Auto-reconnect if the connection drops
setTimeout(connect, 3000);
};
ws.onerror = (error) => {
console.error('WebSocket Error:', error);
};
}
// Initialize connection
connect();
</script>
</body>
</html>

Step 4: Running and Testing Locally
Time to test. Back in CMD, I’ll start the development server:
npm run dev
Then I’ll open http://localhost:3000 in my browser.
If it all worked, the dashboard turns green the moment the page loads and the numbers start ticking up every second.

The way this works, the JavaScript opened one persistent connection to the Fastify server, and now the server just pushes fresh data down that connection every second. The browser isn’t asking for updates, they’re being sent to it.
Want to see the disconnect logic kick in? I’ll stop the server in the terminal. The dashboard flips to red on its own, no refresh needed.

Start it back up, and it reconnects by itself. Still no refresh.

Step 5: Pushing the Code to GitHub
The code works locally, so it’s going up to GitHub next.
One thing first, I’ll add a .gitignore file with node modules in it. Otherwise I’d be dumping thousands of dependency files into the repo, which I don’t want.
node_modules/

Then it’s just the usual Git commands to get everything pushed up:
git init git add . git commit -m "Initial commit for Fastify WebSocket application" git branch -M main git remote add origin https://github.com/your-username/fastify-pm2-demo git push -u origin main
A refresh of the repo page, and there’s all my files.

Step 6: Deploying on Cloudways Velocity
With the project sitting on GitHub, I can get it deployed on Cloudways Node.js hosting (Velocity).
I’ll log into Cloudways, find Velocity in the left-side menu, and click Get Started.

The Starter plan is more than enough here, so I’ll pick that and hit Proceed.

Next it wants to know where my code lives. Cloudways gives you GitHub, GitLab, or Bitbucket, and mine’s on GitHub, so that’s the one I’ll connect.

From the dropdown, I’ll grab my fastify-pm2-demo repo and click Continue.

Cloudways detects the project and fills in the settings itself. I’ll give them a quick look and click Deploy Now.

From there it pulls the code, starts the app under PM2, and sets up the reverse proxy to forward WebSocket traffic to the Node app. No manual PM2 setup, no touching Nginx, which is the whole point of using Velocity for this.

Step 7: Final Live Test
Once it’s done deploying, Cloudways hands me a temporary URL for the app.

I’ll copy that and open it in my browser. Since the frontend JavaScript checks the protocol on its own, the jump to a secure wss:// connection happens automatically, nothing for me to configure.

The page loads, the badge goes green, and the telemetry data starts streaming, exactly the way it did locally.

And that’s a production-ready Fastify WebSocket app, running with PM2 keeping it stable, live on Cloudways Node.js hosting (Velocity).
Take Your Node.js App to Production Today
Get PM2 crash recovery and automatic WebSocket proxying out of the box with Cloudways Velocity.
Wrapping Up
So that’s how you take a real-time app from your laptop to an actual production server. I went over how Fastify gives you the speed to hold a lot of open WebSocket connections without much overhead, and how PM2 is the part that keeps those connections from dropping every time the app hits an error or the process falls over.
The mini project pulled it together, a live telemetry stream running locally first, then deployed on Cloudways Velocity where the PM2 side is already handled, so there’s no manual config to deal with.
The full source code is up on my GitHub if you want to clone it. And if you get stuck anywhere, Fastify setup, the WebSocket parts, or the deployment itself, drop a comment below and I’ll help out.
Q. What is Fastify used for?
It’s a lightweight Node.js framework for building APIs and dynamic web pages. Fast, low overhead, and built around plugins, so you only pull in the pieces your app actually needs.
Q. What is PM2 used for?
It keeps Node.js apps running in the background and restarts them if they crash. You also get a built-in load balancer, zero-downtime reloads, and automatic log handling, basically all the stuff you’d rather not manage by hand on a live server.
Q. Is Fastify really faster than Express?
In most benchmarks, yeah. Express usually sits around 10,000 to 20,000 requests per second, and Fastify gets closer to 45,000 to 50,000. Most of that comes from its JSON serialization and radix tree routing. Whether you’d ever notice depends on your traffic, but the gap is real.
Q. How to enable PM2 watching?
Add the –watch flag when you start the app: pm2 start app.js –watch. Or set it permanently by putting watch: true in your ecosystem config file.
Q. Is Fastify good for production?
It is. The schema validation helps a lot, it throws out malformed requests before they hit your logic, so you get a bit of a security and speed win for free. Between that, the plugins, and native HTTP/2, it holds up fine under real load.
Q. Which companies use Fastify?
A fair few big ones, Capital One, Walmart, American Express, Voodoo, and Handsontable among them.
Start Growing with Cloudways Today.
Our Clients Love us because we never compromise on these
Abdul Rehman
Abdul is a tech-savvy, coffee-fueled, and creatively driven marketer who loves keeping up with the latest software updates and tech gadgets. He's also a skilled technical writer who can explain complex concepts simply for a broad audience. Abdul enjoys sharing his knowledge of the Cloud industry through user manuals, documentation, and blog posts.