Measuring Traffic in DynamoDB and Detecting Abuse
Nikos Katsikanis - 29 July 2025
This post shows how a Remix app on AWS logs every request in DynamoDB. We roll up the numbers each day and week to keep costs down and catch bad actors.
Capturing Events
Every page view, login and other action creates a metric containing the event name, timestamp, optional user information and the caller’s IP address. The app uses Architect with Remix so each write happens inside a short-lived Lambda running on AWS. All data is written using a lightweight DynamoDB helper.
Each metric item is immutable and stored in a single table alongside a flag indicating whether it has been aggregated. This single-table approach keeps writes simple and follows common NoSQL guidance to store data in the shape you need to read it.
export enum MetricEvent {
Authenticate = 'authenticate',
VisitComePage = 'visitSomePage',
DoSomeAction = 'doSomeAction',
// ... etc
}
export type Metric = {
id: string;
event: MetricEvent;
timestamp: string;
userId: string | null;
ip: string | null;
aggregated: boolean;
metadata?: Record<string, unknown>;
};
Metrics are recorded through a helper function:
export const recordMetric = async (
event: MetricEvent,
userId: string | null,
metadata?: Record<string, unknown>,
ip?: string | null,
userAgent?: string | null,
): Promise<void> => {
const metric: Metric = {
id: '',
event,
timestamp: new Date().toISOString(),
userId,
ip: ip ?? null,
aggregated: false,
};
await setMetric(metric);
};
Aggregating Daily Metrics
A nightly Lambda scans the previous day’s metrics using a date prefix on the sort key. It sums up the counts for each event and stores the results in a separate daily table along with a rough read cost estimate. Once processed, the original records are flagged so they won’t be picked up again.
This pattern of writing summary items avoids large table scans later and is a practical example of preparing data for the exact queries you need.
const result = await searchMetric({
term: date,
type: 'text',
compare: 'begin',
category: 'default',
pageSize: 1000,
});
const stats = aggregateMetrics(metrics);
const costEstimate = parseFloat(((totalScanned / 1_000_000) * 1.25).toFixed(4));
await setMetricDaily({ id: date, date, stats, costEstimate });
Computing Cost Savings and Detecting Abuse
An aggregation script merges all the daily summaries. It calculates how many reads the rollup process saved compared with scanning every raw event. The same pass groups metrics by IP address and flags bursts of activity that fire faster than a human could click. Those IPs are considered suspicious for further review.
const unaggregatedCost = parseFloat(((totalEvents / 1_000_000) * 1.25).toFixed(4));
const queryCost = parseFloat(((result.stats.scanned / 1_000_000) * 1.25).toFixed(4));
merged.totalSaved = parseFloat((unaggregatedCost - (totalCost + queryCost)).toFixed(4));
const item = {
ip,
count: info.count,
userId: info.userId,
avgDuration: avg,
suspicious: info.count > 20 && avg < 1000,
};
Weekly IP Aggregation
Once a week a separate process gathers seven days of metrics. It groups them by geographic location and IP address, storing only the top results. This makes it easy to spot accounts that appear to share the same IP across different regions.
for (let i = 0; i < 7; i++) {
const result = await searchMetric({ term: dateStr, ... });
metrics.push(...result.items.filter((m) => m.timestamp.startsWith(dateStr)));
}
const topIpsByLocation = await aggregateIps(metrics);
const costEstimate = parseFloat(((totalScanned / 1_000_000) * 1.25).toFixed(4));
await setMetricWeekly({ id: weekId, week: weekId, topIpsByLocation, costEstimate });
Visualizing in the Admin UI
The admin dashboard charts each metric type and lists any flagged IPs. It also shows the estimated DynamoDB cost per day so administrators can see how much the rollups save compared to scanning raw events.
<DailyEventChart labels={costLabels} counts={costValues} datasetLabel="Cost ($)" />
<p class="text-sm text-gray-600 mt-1">
Total cost so far: ${totalCost.toFixed(4)} – Saved approx. ${totalSaved.toFixed(4)}
</p>
Conclusion
Summarizing metrics every day and week keeps DynamoDB costs predictable and surfaces unusual traffic quickly. Storing immutable events and writing precomputed summaries exemplifies a NoSQL mindset—design tables around your access patterns and avoid expensive queries.