Monitoring Your Node.js Microservices with Horux

Monitoring Your Node.js Microservices with Horux

Horux Team

January 5, 2026

• 10 min read

The Microservices Monitoring Challenge

Monitoring a monolith is relatively straightforward. You have one application, one database, one set of logs. Easy.

Microservices? Not so much. Suddenly you have 10, 20, maybe 50 different services all talking to each other. A single user request might touch six different services before completing. When something breaks, figuring out where it broke becomes a detective game.

I learned this the hard way when we split our monolith into microservices. Within a week, we had our first "everything is green but users can't log in" incident. Fun times.

This guide is the monitoring setup I wish I had back then.

What We're Building

We're going to instrument a Node.js microservice with Horux monitoring. By the end, you'll know:

  • What metrics actually matter (spoiler: not as many as you think)
  • How to instrument Express or Fastify apps
  • Patterns for tracking database queries, external API calls, and queue processing
  • How to structure logs so they're actually useful
  • What to alert on (and what to ignore)

Install the TypeScript SDK

First, grab the Horux client:

npm install @horux/client

Set up your client once, probably in a monitoring.ts file:

import { HoruxClient } from '@horux/client';

export const horux = new HoruxClient({
  apiToken: process.env.HORUX_API_TOKEN!,
  serviceId: process.env.HORUX_SERVICE_ID!,

  // Optional: configure batching
  maxBatchSize: 100,
  flushInterval: 10000, // 10 seconds

  // Auto-collect instance metadata
  autoAddInstanceLabels: true
});

The SDK batches metrics automatically. You don't need to worry about hammering our API—it'll buffer metrics in memory and flush them every 10 seconds or when the batch fills up.

The Core Metrics Every Service Needs

Let's start with the essentials. These four metrics will catch 90% of your problems:

1. Request Rate and Status Codes

Track how many requests you're handling and what status codes you're returning:

import express from 'express';
import { horux } from './monitoring';

const app = express();

// Middleware to track all requests
app.use((req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    // Use path instead of route since route is matched later
    const routePath = req.route?.path || req.path || 'unknown';

    // Increment request counter
    horux.metrics.counter('http.requests.total', 1, {
      method: req.method,
      route: routePath,
      status: res.statusCode.toString()
    });

    // Record response time
    horux.metrics.gauge('http.request.duration', duration, {
      method: req.method,
      route: routePath
    });
  });

  next();
});

Now you can see your request rate, success vs error rates, and slow endpoints. This alone is incredibly valuable.

2. Error Rate

Track application errors separately from HTTP 5xx responses:

// Error handler middleware
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  const routePath = req.route?.path || req.path || 'unknown';

  // Log the error
  horux.logs.error('Application error', {
    error: err.message,
    stack: err.stack,
    route: routePath,
    method: req.method
  });

  // Track error metric
  horux.metrics.counter('errors.total', 1, {
    type: err.name,
    route: routePath
  });

  res.status(500).json({ error: 'Internal server error' });
});

3. Database Query Performance

If you're using Prisma, TypeORM, or raw SQL, track your query times:

async function getUserById(id: string) {
  const start = Date.now();

  try {
    const user = await prisma.user.findUnique({ where: { id } });

    const duration = Date.now() - start;
    horux.metrics.gauge('db.query.duration', duration, {
      operation: 'findUnique',
      model: 'user'
    });

    return user;
  } catch (error) {
    horux.metrics.counter('db.errors.total', 1, {
      operation: 'findUnique',
      model: 'user'
    });
    throw error;
  }
}

Slow database queries are often the root cause of performance issues. Track them early.

4. External API Calls

Track calls to third-party APIs:

async function fetchUserFromAuth0(userId: string, accessToken: string) {
  const start = Date.now();

  try {
    const response = await fetch(`https://api.auth0.com/users/${userId}`, {
      headers: { Authorization: `Bearer ${accessToken}` }
    });

    const duration = Date.now() - start;

    horux.metrics.gauge('external.api.duration', duration, {
      service: 'auth0',
      endpoint: '/users',
      status: response.status.toString()
    });

    if (!response.ok) {
      horux.metrics.counter('external.api.errors', 1, {
        service: 'auth0',
        status: response.status.toString()
      });
    }

    return response.json();
  } catch (error) {
    horux.metrics.counter('external.api.errors', 1, {
      service: 'auth0',
      error: 'network_error'
    });
    throw error;
  }
}

When Auth0 (or Stripe, or whatever) is having issues, you'll know immediately.

Business Metrics That Actually Matter

Technical metrics tell you how your system is performing. Business metrics tell you if it's working.

Here are some examples:

// Track user signups
horux.metrics.counter('business.signups.total', 1, {
  plan: 'free',
  source: 'organic'
});

// Track revenue events
horux.metrics.gauge('business.mrr', monthlyRecurringRevenue);

// Track feature usage
horux.metrics.counter('feature.usage', 1, {
  feature: 'export_csv',
  user_type: 'premium'
});

// Track queue depth (if using Bull or similar)
horux.metrics.gauge('queue.depth', await queue.count(), {
  queue: 'email_notifications'
});

These metrics help you understand if your application is actually doing what it's supposed to do, not just that it's up.

Structured Logging Done Right

Logs are useless if you can't search them. Here's how to structure logs in a way that makes debugging actually possible:

// Good: structured with context
horux.logs.info('Order created', {
  orderId: order.id,
  userId: user.id,
  amount: order.total,
  paymentMethod: order.paymentMethod,
  // Add trace IDs for distributed tracing
  traceId: req.headers['x-trace-id'],
  spanId: generateSpanId()
});

// Bad: unstructured string
console.log(`User ${user.id} created order ${order.id} for $${order.total}`);

With structured logs, you can search for things like:

  • All orders by a specific user
  • All failed payment attempts
  • All requests with trace ID xyz

What to Alert On

This is where most teams mess up. They alert on everything and end up with alert fatigue.

Here's what actually deserves an alert:

Critical Alerts (wake someone up):

  • Error rate > 5% for 5 minutes
  • P95 response time > 2 seconds for 5 minutes
  • Service is down (health check failing)
  • Database connection errors

Warning Alerts (check Slack in the morning):

  • Error rate > 1% for 10 minutes
  • Slow database queries (> 1 second)
  • Queue depth growing (sign of backed up jobs)
  • External API latency increasing

Info (just log it, no alert):

  • Individual request errors (they happen)
  • Cache misses (normal)
  • Slow individual requests (outliers exist)

Real-World Example: Express API

Here's a complete example of a monitored Express service:

import express from 'express';
import { horux } from './monitoring';

const app = express();

// Request tracking middleware
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    const routePath = req.route?.path || req.path || 'unknown';
    horux.metrics.gauge('http.request.duration', Date.now() - start, {
      method: req.method,
      route: routePath,
      status: res.statusCode.toString()
    });
  });
  next();
});

// Health check endpoint
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// Business logic
app.post('/api/orders', async (req, res) => {
  try {
    const order = await createOrder(req.body);

    // Track business metric
    horux.metrics.counter('orders.created', 1, {
      paymentMethod: order.paymentMethod
    });

    res.json({ order });
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : 'Unknown error';

    horux.logs.error('Failed to create order', {
      error: errorMessage,
      userId: req.user?.id
    });

    res.status(500).json({ error: 'Failed to create order' });
  }
});

// Error handler
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  const routePath = req.route?.path || req.path || 'unknown';

  horux.metrics.counter('errors.total', 1, {
    route: routePath
  });

  horux.logs.error('Unhandled error', {
    error: err.message,
    stack: err.stack
  });

  res.status(500).json({ error: 'Internal server error' });
});

app.listen(3000, () => {
  horux.logs.info('Server started', { port: 3000 });
});

Common Patterns and Tips

1. Use Middleware for Cross-Cutting Concerns

Don't sprinkle monitoring code everywhere. Use middleware for things that apply to all requests.

2. Be Generous with Labels, But Not Too Generous

Labels help you slice and dice your metrics. But too many unique label combinations will explode your data volume.

Good: { method: 'GET', status: '200' } (limited cardinality) Bad: { userId: '123456' } (high cardinality, expensive)

3. Track What You Alert On

If you're alerting on error rate, make sure you're actually tracking errors. Sounds obvious, but I've seen teams alert on metrics they're not even collecting.

4. Measure Distributions, Not Just Averages

For response times, averages can be misleading. While we used gauge in the examples above for simplicity (which captures the latest value), for production systems you should consider using histograms to capture the full distribution of latencies (P95, P99). The Horux SDK supports pushing pre-aggregated histograms if your application calculates them.

5. Correlate Metrics with Logs

Include trace IDs in both metrics and logs. When an alert fires, you can immediately jump to the relevant logs.

Wrapping Up

Monitoring microservices doesn't have to be overwhelming. Start with the basics:

  1. Track request rates and errors
  2. Monitor database and external API performance
  3. Log with structure and context
  4. Alert on things that actually matter

Once you have that foundation, you can add more sophisticated monitoring as you need it.

The Horux TypeScript SDK makes all of this straightforward. No complicated configuration, no vendor lock-in, just send your data and get insights.

Questions? Check out our full SDK documentation or reach out at contact@horux.io.

Now go monitor something! 📊