api-performance-api-performance-ff2585 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited api-performance-api-performance-ff2585 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: Optimize backend performance through database query optimization (indexes, prepared statements, avoiding N+1), caching strategies (cache-aside, write-through), connection pooling, and non-blocking async patterns. Always measure before optimizing -- run EXPLAIN ANALYZE, check event loop lag, and track cache hit rates before adding complexity.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)(You MUST always release database connections back to the pool using `finally` blocks)
(You MUST use eager loading or batching (DataLoader) to prevent N+1 queries -- never lazy load in loops)
(You MUST set TTL on all cached data to prevent stale data and memory exhaustion)
(You MUST offload CPU-intensive work to Worker Threads -- blocking the event loop degrades all requests)
</critical_requirements>
Detailed Resources:
Auto-detection: connection pool, query optimization, database index, N+1, caching, cache invalidation, prepared statement, worker threads, event loop, CPU-bound, latency, throughput, performance tuning, EXPLAIN ANALYZE, keyset pagination, cache-aside, write-through
When to use:
When NOT to use:
Key patterns covered:
<philosophy>
Backend performance optimization follows one core principle: measure first, optimize second. Premature optimization wastes development time and adds complexity without evidence of benefit.
The Three Pillars of Backend Performance:
When to optimize:
When NOT to optimize:
</philosophy>
<patterns>
Connection pooling reuses database connections instead of creating new ones per request. A PostgreSQL handshake takes 20-30ms -- pooling eliminates this overhead.
Key rules:
pool.query() for simple queries (auto-manages connection lifecycle)pool.connect() and always release in finally// Transaction with guaranteed connection release
async function createUserWithProfile(
userData: UserData,
profileData: ProfileData,
) {
const client = await pool.connect();
try {
await client.query("BEGIN");
const userResult = await client.query(
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
[userData.name, userData.email],
);
await client.query("INSERT INTO profiles (user_id, bio) VALUES ($1, $2)", [
userResult.rows[0].id,
profileData.bio,
]);
await client.query("COMMIT");
return userResult.rows[0];
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release(); // CRITICAL: Always release back to pool
}
}Why good: finally guarantees connection release even on error, preventing pool exhaustion
See examples/core.md for full pool configuration, sizing formula, and external pooler guidance.
The N+1 problem occurs when fetching N records triggers N additional queries for related data. With 100 records, that's 101 database round-trips.
Two solutions:
.with()) -- single query with JOINs for known relationships.load() calls into single query per tick, ideal for GraphQL// Eager loading: single query fetches jobs + companies + skills
const jobs = await db.query.jobs.findMany({
where: and(eq(jobs.isActive, true), isNull(jobs.deletedAt)),
with: {
company: { with: { locations: true } },
jobSkills: { with: { skill: true } },
},
});// BAD: N+1 anti-pattern -- one query per job
for (const job of jobs) {
job.company = await db.query.companies.findFirst({
where: eq(companies.id, job.companyId),
});
}Why bad: 1 query for jobs + N queries for companies, latency grows linearly with data size
See examples/core.md for DataLoader batching pattern.
Indexes speed up queries by avoiding full table scans. Index columns used in WHERE, JOIN, and ORDER BY clauses.
// Strategic indexes on a table definition
export const jobs = pgTable(
"jobs",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull(),
country: varchar("country", { length: 100 }),
employmentType: varchar("employment_type", { length: 50 }),
isActive: boolean("is_active").default(true),
createdAt: timestamp("created_at").defaultNow(),
deletedAt: timestamp("deleted_at"),
},
(table) => [
// Composite index for common filter combination
index("jobs_country_employment_idx").on(
table.country,
table.employmentType,
),
// Partial index -- only indexes active non-deleted jobs
index("jobs_active_idx")
.on(table.isActive, table.createdAt)
.where(sql`${table.deletedAt} IS NULL`),
// Foreign key index for JOIN performance
index("jobs_company_id_idx").on(table.companyId),
],
);Index Decision Framework:
| Column Usage | Index Type | When to Use |
|---|---|---|
| WHERE equality | B-tree (default) | High-selectivity columns |
| WHERE range (>, <, BETWEEN) | B-tree | Date ranges, numeric ranges |
| WHERE multiple columns | Composite | Queries always filter by same columns together |
| WHERE on subset | Partial | Most queries filter on active/non-deleted |
| Full-text search | GIN/GiST | Text search with LIKE, tsvector |
| JSON field access | GIN | JSONB column queries |
Composite index column order: Equality conditions first, range conditions last, high selectivity first.
See examples/core.md for EXPLAIN ANALYZE examples, index monitoring queries, and unused index detection.
The most common caching pattern. Check cache first, fetch from database on miss, store with TTL.
const CACHE_TTL_SECONDS = 300;
const CACHE_PREFIX = "app:user";
async function getUserById(userId: string): Promise<User | null> {
const cacheKey = `${CACHE_PREFIX}:${userId}`;
const cached = await cacheClient.get(cacheKey);
if (cached) return JSON.parse(cached) as User;
const user = await db.query.users.findFirst({ where: eq(users.id, userId) });
if (!user) return null;
await cacheClient.set(cacheKey, JSON.stringify(user), {
EX: CACHE_TTL_SECONDS,
});
return user;
}Why good: TTL prevents stale data accumulation, namespaced keys prevent collisions, early return on cache hit
See examples/caching.md for write-through, tag-based invalidation, key strategies, and TTL guidance.
Node.js uses a single thread for JavaScript. CPU-intensive work blocks ALL concurrent requests.
Rule of thumb:
| CPU Duration | Solution | Rationale |
|---|---|---|
| < 50ms | Keep on main thread | Worker overhead not worth it |
| 50-500ms | setImmediate chunking | Yields to event loop between chunks |
| > 500ms | Worker Threads | Offload completely to separate thread |
// Chunked processing with setImmediate -- yields to event loop between batches
const CHUNK_SIZE = 100;
async function processLargeArray(items: Item[]): Promise<ProcessedItem[]> {
const results: ProcessedItem[] = [];
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
for (const item of chunk) {
results.push(expensiveTransform(item));
}
if (i + CHUNK_SIZE < items.length) {
await new Promise((resolve) => setImmediate(resolve));
}
}
return results;
}See examples/async.md for worker pool implementation, concurrency control with p-limit, and event loop lag monitoring.
OFFSET pagination scans all previous rows -- at OFFSET 100,000 the database reads and discards 100,000 rows. Keyset pagination uses a cursor for constant-time performance.
-- BAD: OFFSET scans all previous rows
SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 100000;
-- GOOD: Keyset pagination -- constant time regardless of position
SELECT * FROM products
WHERE id > :last_seen_id
ORDER BY id LIMIT 20;When to use offset: Small datasets (< 100k rows), need total count, random page access required.
When to use keyset: Large datasets, infinite scroll, real-time data where inserts shouldn't cause duplicates.
See examples/core.md for full TypeScript implementations of both patterns.
</patterns>
<red_flags>
High Priority Issues:
Medium Priority Issues:
pg_stat_user_indexesPromise.all on 10,000 items overwhelms downstream servicesGotchas & Edge Cases:
YEAR(created_at)) prevents index use -- rewrite as range conditionsidleTimeoutMillis can cause "connection terminated unexpectedly" if set too shortSELECT * fetches unnecessary data and prevents covering index optimization -- select specific columns</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST always release database connections back to the pool using `finally` blocks)
(You MUST use eager loading or batching (DataLoader) to prevent N+1 queries -- never lazy load in loops)
(You MUST set TTL on all cached data to prevent stale data and memory exhaustion)
(You MUST offload CPU-intensive work to Worker Threads -- blocking the event loop degrades all requests)
Failure to follow these rules will cause connection pool exhaustion, N+1 performance degradation, memory leaks from unbounded caches, and blocked event loops affecting all concurrent requests.
</critical_reminders>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.