Best Practices
Optimize your Enrich.sh integration for maximum performance, reliability, and cost efficiency.
Performance
1. Batch Your Events ⚡
Single-event requests are inefficient. Always batch events together.
| Approach | Requests/sec | Events/sec | Efficiency |
|---|---|---|---|
| 1 event per request | 200 | 200 | ❌ Poor |
| 10 events per request | 200 | 2,000 | ⚠️ OK |
| 50–100 events per request | 200 | 10,000–20,000 | ✅ Optimal |
Using the SDK (batching is automatic):
import { Enrich } from 'enrich.sh'
const enrich = new Enrich('sk_live_your_key')
// Just call track() — batching and flush timing are handled for you
enrich.track('events', { event: 'page_view', url: '/home' })
enrich.track('events', { event: 'click', element: 'signup_btn' })Custom batching (without SDK):
const buffer = []
function track(event) {
buffer.push({ ...event, ts: Date.now() })
if (buffer.length >= 100) flush()
}
async function flush() {
if (buffer.length === 0) return
const events = buffer.splice(0, buffer.length)
await fetch('https://enrich.sh/ingest', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ stream_id: 'events', data: events }),
})
}
// Flush periodically and before page unload
setInterval(flush, 5000)
window.addEventListener('beforeunload', flush)2. Keep Payloads Lean
Only send data you need. Every byte costs storage and processing.
// ❌ Bad: Sending entire DOM
{ event: 'click', page_html: '<!DOCTYPE html>...' }
// ✅ Good: Send only relevant data
{ event: 'click', element_id: 'signup_btn', page: '/pricing' }Schema Strategy
1. Choose the Right Schema Mode
| Scenario | Mode | Why |
|---|---|---|
| Prototyping / getting started | flex | Accept everything, iterate fast |
| SaaS API / ERP integration | evolve | Detect upstream schema changes automatically |
| Financial data / compliance | strict | Reject anything that doesn't match |
| ML inference logging | evolve | Schemas grow as models evolve |
| IoT sensor data | evolve | New device types add fields |
TIP
Start with flex or evolve during development. Switch to strict for production pipelines where data contracts matter.
2. Monitor Schema Changes
When using evolve mode, check the Observability → Schema Events page regularly for:
- New fields — a source started sending data you weren't expecting
- Missing fields — a source stopped sending required data
- Type changes — a field changed from integer to string (common with ERP exports)
# Check schema events via API
curl "https://enrich.sh/streams/events/schema-events?days=7" \
-H "Authorization: Bearer sk_live_your_key"3. Use the Dead Letter Queue
With strict mode, rejected events go to the DLQ — nothing is lost. Review DLQ events to:
- Fix upstream data issues
- Update your schema to accept new fields
- Reingest corrected events
# Check DLQ events
curl "https://enrich.sh/streams/transactions/dlq?days=7" \
-H "Authorization: Bearer sk_live_your_key"Reliability
1. Handle Errors Gracefully
Implement retry logic with exponential backoff:
async function sendWithRetry(events, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch('https://enrich.sh/ingest', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ stream_id: 'events', data: events }),
})
if (response.ok) return await response.json()
// Don't retry client errors (4xx)
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error: ${response.status}`)
}
// Retry server errors (5xx)
if (attempt < maxRetries) {
await new Promise(r => setTimeout(r, 2 ** attempt * 1000))
}
} catch (err) {
if (attempt === maxRetries) throw err
}
}
}TIP
The SDK handles retries automatically.
2. Use Local Buffering
For mobile apps or offline-first clients, persist events locally:
function trackEvent(event) {
const buffer = JSON.parse(localStorage.getItem('event_buffer') || '[]')
buffer.push({ ...event, ts: Date.now() })
localStorage.setItem('event_buffer', JSON.stringify(buffer))
if (buffer.length >= 100) flushToServer()
}
async function flushToServer() {
const buffer = JSON.parse(localStorage.getItem('event_buffer') || '[]')
if (buffer.length === 0) return
try {
await sendWithRetry(buffer)
localStorage.removeItem('event_buffer') // Clear only on success
} catch (err) {
console.error('Failed to flush, will retry later:', err)
}
}3. Use beacon() for Page Unload
The SDK's beacon() method uses fetch({ keepalive: true }) to survive page unloads:
window.addEventListener('beforeunload', () => {
enrich.beacon() // fire-and-forget, survives tab close
})Security
1. Never Expose API Keys Client-Side
// ❌ NEVER do this in browser code
const API_KEY = 'sk_live_abc123...'
// ✅ Use a proxy endpoint
fetch('/api/track', { body: JSON.stringify(events) })
// Your server forwards to Enrich.sh with the secret key2. Use Separate Keys
| Environment | Key Prefix | Usage |
|---|---|---|
| Development | sk_test_ | Local testing |
| Staging | sk_test_ | Pre-production |
| Production | sk_live_ | Real data |
Data Quality
1. Use Consistent Field Names
// ❌ Inconsistent naming
{ eventType: 'click', EventTime: 123, user_ID: 'abc' }
// ✅ Consistent snake_case
{ event_type: 'click', event_time: 123, user_id: 'abc' }2. Always Include Timestamps
{
event: 'purchase',
amount: 99.99,
ts: Date.now() // Unix timestamp (milliseconds)
}3. Use Enrichment Templates
Let Enrich.sh add consistent metadata automatically:
curl -X POST https://enrich.sh/streams \
-H "Authorization: Bearer sk_live_your_key" \
-d '{ "stream_id": "clicks", "template": "clickstream" }'4. Define Field Types
Even in flex mode, defining field types improves query performance:
{
"stream_id": "events",
"schema_mode": "flex",
"fields": {
"ts": { "name": "timestamp", "type": "int64" },
"amount": { "type": "float64" },
"active": { "type": "boolean" }
}
}Without field types, everything is stored as string. With types, DuckDB and ClickHouse can read the data more efficiently.
Checklist
- [ ] Batch events (50–100 per request, or use SDK auto-batching)
- [ ] Choose a schema mode (
flex→evolve→strictas you mature) - [ ] Monitor schema events in the Observability UI
- [ ] Review DLQ regularly if using
strictmode - [ ] Implement retry logic with exponential backoff
- [ ] Use local buffering for unreliable connections
- [ ] Keep payloads lean (only send what you need)
- [ ] Never expose API keys in client-side code
- [ ] Use consistent field naming (snake_case)
- [ ] Always include timestamps
- [ ] Define field types for better query performance
- [ ] Connect your warehouse via Dashboard → Connect
