
Monitoring Your Node.js Microservices with Horux
Horux Team
January 5, 2026
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/clientSet 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:
- Track request rates and errors
- Monitor database and external API performance
- Log with structure and context
- 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! 📊