In production, you need visibility into what is hitting your webhooks -- request volume, error rates, and payload patterns.
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)
I build production n8n and Cloudflare automation for teams — the same engineering behind HarperFlow. Fixed-price, escrow-protected, US-based.