This website uses cookies

Our website, platform and/or any sub domains use cookies to understand how you use our services, and to improve both your experience and our marketing relevance.

Go hands-off. Autoscale your WordPress site on Cloudways Autonomous.Start Your Free Trial

How to Deploy a Node.js WebSocket App with PM2 and Fastify

Updated on August 5, 2026

11 Min Read
Dark code editor window labeled server.js showing a Fastify server with WebSocket setup, on a blue gradient background with a small pm2 list panel.

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

Command Prompt showing the PATH command pointing to the Node.js folder

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

Terminal output after running npm init to create the project's package.json

Now I’ll install the Fastify framework and the WebSocket plugin using this command:

npm install fastify @fastify/websocket

Terminal output after installing Fastify and the WebSocket plugin

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.

VS Code showing the newly created fastify-pm2-demo project folder

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();

VS Code editor showing the completed server.js WebSocket code

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>

VS Code editor showing the index.html dashboard code

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.

Live dashboard showing connected status with uptime and memory updating

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.

Dashboard status badge turning red after the WebSocket server stops

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

Dashboard automatically reconnecting after the server restarts

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/

Text editor showing the .gitignore file with node_modules added

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.

GitHub repository page showing the pushed Fastify project 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.

Cloudways dashboard showing the Velocity menu and Get Started button

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

Cloudways Velocity plan selection screen with the Starter plan chosen

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.

Cloudways screen for connecting a GitHub, GitLab, or Bitbucket repository

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

Cloudways repository dropdown showing the fastify-pm2-demo project selected

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

Cloudways deployment settings screen with the Deploy Now button

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.

Cloudways deployment progress screen while the app deploys under PM2

Step 7: Final Live Test

Once it’s done deploying, Cloudways hands me a temporary URL for the app.

Cloudways screen showing the temporary URL for the deployed application

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.

Deployed Fastify WebSocket dashboard loaded in the browser over wss

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

Live telemetry dashboard streaming uptime and memory data after deployment

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.

Share your opinion in the comment section. COMMENT NOW

Share This Article

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.

×

Webinar: How to Get 100% Scores on Core Web Vitals

Join Joe Williams & Aleksandar Savkovic on 29th of March, 2021.

Do you like what you read?

Get the Latest Updates

Share Your Feedback

Please insert Content

Thank you for your feedback!

Do you like what you read?

Get the Latest Updates

Share Your Feedback

Please insert Content

Thank you for your feedback!

Want to Experience the Cloudways Platform in Its Full Glory?

Take a FREE guided tour of Cloudways and see for yourself how easily you can manage your server & apps on the leading cloud-hosting platform.

Start my tour