Memory Leak in Express with Dynamodb
Memory Leak in Express with Dynamodb — how this specific combination creates or exposes the vulnerability
A memory leak in an Express application that uses DynamoDB typically arises from unbounded data accumulation on the server side and from improper handling of asynchronous resources. In this combination, each incoming HTTP request may open DynamoDB streams, long-lived query cursors, or retain references to request-scoped objects in global caches. Because DynamoDB operations are asynchronous and may return large result sets, failing to consume or close streams and not releasing references can cause the Node.js event loop and heap to retain objects beyond their intended lifetime.
Unlike CPU-bound leaks, DynamoDB-related leaks often manifest as gradual increases in resident memory and eventual process restarts or latency spikes under sustained load. Common patterns include storing query results in module-level arrays, attaching DynamoDB document objects to the request or response beyond the request lifecycle, and leaving scan or query pagination tokens unresolved. The unauthenticated scan capabilities of middleBrick can surface these patterns by flagging missing resource cleanup and unmanaged data exposure as part of its Data Exposure and Unsafe Consumption checks, helping you detect server-side accumulation risks without authentication.
Another angle is the use of DynamoDB DocumentClient or low-level clients in Express route handlers without proper stream consumption or error handling. For example, a paginated query that uses LastEvaluatedKey to loop across pages but retains each page’s items in an outer array will steadily grow memory. Similarly, attaching raw DynamoDB responses to res.locals or global objects prevents garbage collection. These patterns align with OWASP API Top 10 categories such as Excessive Data Exposure and Security Misconfiguration, and they can be reflected in middleBrick’s findings as high-severity items with remediation guidance.
Instrumentation and observability are essential to surface these issues. Logging the size of cached arrays, monitoring open streams, and tracking the lifecycle of objects referenced in closures help correlate memory growth with specific DynamoDB interactions. Because the leak is unauthenticated in nature, middleBrick’s black-box scanning can probe public endpoints to reveal data exposure paths that exacerbate memory retention, especially when combined with missing rate limiting or inefficient query design.
Finally, the Express layer can inadvertently amplify DynamoDB-side issues through middleware that caches responses or request contexts in memory without bounds. Without explicit cleanup in teardown or error handlers, these caches grow as traffic increases. middleBrick’s checks around Property Authorization and BFLA/Privilege Escalation can highlight endpoints where data is retained or exposed beyond intended access boundaries, supporting more targeted remediation.
Dynamodb-Specific Remediation in Express — concrete code fixes
To remediate memory leaks when using DynamoDB in Express, adopt patterns that limit object lifetimes, consume streams fully, and avoid attaching large payloads to long-lived scopes. Below are concrete, realistic code examples for Express routes that interact with DynamoDB safely.
Paginated query with controlled accumulation
Instead of accumulating all pages into a module-level or outer-scope array, process items per page and release references after each iteration. Use async/await with a loop that reads LastEvaluatedKey and avoids retaining prior pages.
import { DynamoDBClient, QueryCommand } from "@aws-sdk/client-dynamodb";
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
import express from "express";
const client = new DynamoDBClient({ region: "us-east-1" });
const app = express();
app.get("/items", async (req, res) => {
const TABLE_NAME = process.env.TABLE_NAME;
let exclusiveKey;
let pageCount = 0;
try {
do {
const command = new QueryCommand({
TableName: TABLE_NAME,
KeyConditionExpression: "pk = :pk",
ExpressionAttributeValues: marshall({ ":pk": req.query.pk }),
ExclusiveStartKey: exclusiveKey ? marshall({ lastKey: exclusiveKey }) : undefined,
});
const response = await client.send(command);
// Process items immediately without storing all pages
for (const item of response.Items || []) {
const data = unmarshall(item);
// Perform lightweight transformation or stream to client
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
exclusiveKey = response.LastEvaluatedKey ? unmarshall(response.LastEvaluatedKey).lastKey : null;
pageCount += 1;
} while (exclusiveKey && pageCount < 100); // safety cap
res.end();
} catch (err) {
console.error("DynamoDB query error:", err);
res.status(500).json({ error: "Internal server error" });
}
});
Stream-based scan with cleanup and caps
For scans, avoid storing entire results in memory. Use pagination limits and ensure no references persist after the response ends. Close logical loops by releasing variables when the response finishes.
import { ScanCommand } from "@aws-sdk/client-dynamodb";
app.get("/scan-limited", async (req, res)>
const command = new ScanCommand({
TableName: process.env.TABLE_NAME,
Limit: 100, // bound the page size
});
try {
const response = await client.send(command);
// Do not accumulate across multiple requests
const items = (response.Items || []).map(unmarshall);
res.json({ count: items.length, items });
// items go out of scope after response, enabling GC
} catch (err) {
console.error("Scan failed:", err);
res.status(500).json({ error: "Scan error" });
}
});
Avoid attaching DynamoDB objects to request or global state
Do not assign raw DynamoDB responses or client instances to req, res, or global variables. Instead, extract only required fields and keep the response lifecycle short.
app.get("/user", async (req, res) => {
const cmd = new GetCommand({
TableName: process.env.TABLE_NAME,
Key: marshall({ userId: req.params.id }),
});
const { Item } = await client.send(cmd);
if (!Item) return res.sendStatus(404);
// Extract only needed fields
res.json({ id: unmarshall(Item.id), name: unmarshall(Item.name) });
// Item reference released after response
});
Middleware cleanup and error handling
Ensure that any temporary caches or references are cleared in response finish or error events to prevent retention across requests.
app.use((req, res, next) => {
res.on("finish", () => {
// No-op placeholder for explicit cleanup if needed
});
next();
});
By combining bounded pagination, immediate processing, and avoiding global or long-lived references, you reduce the risk of memory growth. middleBrick can validate these patterns by scanning endpoints for unsafe consumption and data exposure findings, providing prioritized remediation guidance tied to frameworks such as OWASP API Top 10.