Tips > Data, APIs & Webhooks

Monitor Webhook Health by Logging All Incoming Requests

In production, you need visibility into what is hitting your webhooks -- request volume, error rates, and payload patterns.

TipIntermediate2 min read

In production, you need visibility into what is hitting your webhooks -- request volume, error rates, and payload patterns. Add a logging branch early in your workflow that writes every incoming request to a Google Sheet or database table. This gives you an audit trail and helps debug issues.

Real-world example: Log every incoming webhook request to a Postgres table for monitoring and debugging, without slowing down the main processing pipeline.

Add a Code node immediately after the Webhook node that prepares the log entry, then split into two branches: one for logging, one for processing.

// Code node: "Prepare Log Entry"
const input = $input.first().json;

const logEntry = {
  received_at: new Date().toISOString(),
  method: input.method || 'POST',
  path: input.path || 'unknown',
  source_ip: input.headers['x-forwarded-for']
    || input.headers['x-real-ip']
    || 'unknown',
  user_agent: input.headers['user-agent'] || 'unknown',
  content_type: input.headers['content-type'] || 'unknown',
  payload_size: JSON.stringify(input.body || {}).length,
  idempotency_key: input.headers['x-idempotency-key'] || null,
  event_type: input.body?.type || input.body?.event || 'unknown',
  status: 'received'
};

// Pass both the log entry and original data downstream
return [{
  json: {
    log: logEntry,
    originalData: input
  }
}];
```text
Logging branch (Postgres node):

```sql
-- Use parameterized queries to prevent SQL injection
INSERT INTO webhook_log (
  received_at, method, path, source_ip,
  user_agent, content_type, payload_size,
  idempotency_key, event_type, status
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
-- Query Parameters:
-- $1: {{ $json.log.received_at }}
-- $2: {{ $json.log.method }}
-- $3: {{ $json.log.path }}
-- $4: {{ $json.log.source_ip }}
-- $5: {{ $json.log.user_agent }}
-- $6: {{ $json.log.content_type }}
-- $7: {{ $json.log.payload_size }}
-- $8: {{ $json.log.idempotency_key || null }}
-- $9: {{ $json.log.event_type }}
-- $10: {{ $json.log.status }}
```text
For a Google Sheets alternative (simpler setup):

```json
{
  "operation": "append",
  "sheetId": "your-sheet-id",
  "range": "WebhookLog!A:J",
  "values": [
    "={{ $json.log.received_at }}",
    "={{ $json.log.method }}",
    "={{ $json.log.path }}",
    "={{ $json.log.source_ip }}",
    "={{ $json.log.event_type }}",
    "={{ $json.log.payload_size }}",
    "={{ $json.log.status }}"
  ]
}
```text
Then build a scheduled monitoring workflow:

```sql
-- Run daily: check for anomalies
SELECT
  DATE(received_at) as day,
  COUNT(*) as total_requests,
  COUNT(*) FILTER (WHERE status = 'failed') as failed,
  ROUND(
    COUNT(*) FILTER (WHERE status = 'failed')::numeric / COUNT(*)::numeric * 100, 2
  ) as error_rate_pct
FROM webhook_log
WHERE received_at > NOW() - INTERVAL '7 days'
GROUP BY DATE(received_at)
ORDER BY day DESC;
```text
This logging adds minimal overhead (one async database insert) but gives you full visibility into webhook traffic patterns and errors over time.

**Related:** [Set a Unique Encryption Key and Back It Up](../security-best-practices/01-set-a-unique-encryption-key-and-back-it-up.md) | [Use the HTTP Request Node as a Universal Connector](../integration-patterns/01-use-the-http-request-node-as-a-universal-connector.md)

Want this running in your stack?

I build production n8n and Cloudflare automation for teams — the same engineering behind HarperFlow. Fixed-price, escrow-protected, US-based.