Docs menuNetwork analysis

DocsPerformance analysis

Network analysis

Find network and database calls, and get region-aware latency advice.

A function can be fast on its own and still slow in production because of the calls it makes over the network: a query per row, a request per item waited for one at a time, a thousand requests fired at once, a read that returns the whole table. Lamied reads your code's structure, finds the network and database calls it recognises, works out how often each one runs, and explains what to change.

It has its own tab and button in the Complexity Dashboard, separate from time and space complexity. It runs on your computer and is free: no AI and no account needed.

Run it

  1. Open View → Complexity Dashboard, choose the Network tab and click Run network analysis.
  2. Only files that make network or database calls are listed, worst first. Each card shows Req (requests per call), Rows (rows read) and how many issues it has. A critical finding makes the file's score critical.
  3. Open a file's card to see its Network I/O section. Add to AI chat puts this network analysis in the chat, and Explain with AI (Pro) explains it. The optional Network context fields (your server and user regions) sit on this tab too.
  4. A run checks the whole project, then lists 25 files at a time. The line under the button says what it checked, for example 256 files checked · 481 ms · 38 with network calls, and the bar at the bottom of the panel shows the rest: Showing 25 of 38 files with network calls · Load 13 more. Scroll to the end of the list or click the bar for the next 25.
  5. Running it again re-analyses only the files that changed. The two tabs run independently: running one doesn't clear the other's results.

Reading the Network I/O section

PartWhat it tells you
Requests per callHow the number of network and database calls grows each time the function runs. O(1): a fixed number. O(n): one per item of some input. O(n^2): one per pair, from nested loops.
Rows readHow much data the file's database reads return. O(n) means a read returns more rows as your table grows.
FindingsEach problem, with its severity, line, what happens at runtime, and how to fix it.
Call sitesEvery call found, with how many times it runs and how: once, one after another, all at once or not awaited. Calls that take the same path, like one helper used from several places, share a row listing their lines.

What it finds

N+1 queries (critical)

A database query inside a loop: one query for the list, then one more for every item.

Before: 1 + n queries, each waiting for the last
for (const order of orders) {
  order.customer = await prisma.customer.findUnique({ where: { id: order.customerId } });
}
After: 2 queries, whatever the size of the list
const ids = orders.map((order) => order.customerId);
const customers = await prisma.customer.findMany({ where: { id: { in: ids } } });

In Django, use select_related or prefetch_related; in SQL, WHERE id IN (…) or a JOIN. When the queries run all at once (inside Promise.all), it's a warning rather than critical: faster, but still n queries.

Round trips one after another (warning)

An awaited request in a loop. Each waits for the previous one, so the total time is n × the round-trip time: 200 items at 80 ms each is 16 seconds.

Fix it with a batch endpoint if the API has one, or run the requests concurrently with a cap, for example p-limit in JavaScript or an asyncio.Semaphore in Python.

All requests at once (warning)

await Promise.all(items.map(fetch)) or asyncio.gather(…) over a list starts every request at the same moment. That's quick for 10 items, but for 10,000 it can hit rate limits or exhaust connection pools. Cap the concurrency, or use a batch endpoint.

Requests nobody waits for (warning)

forEach doesn't wait for async callbacks
userIds.forEach(async (id) => {
  await axios.post("/notify", { id });   // fires for every id at once; errors are lost
});

Use for…of with await to run them one at a time, or await Promise.all(ids.map(…)) to run them together and see failures.

Async callback in filter, some, every or find (critical)

These methods need a true/false answer immediately. An async callback returns a promise, which always counts as true, so users.filter(async (u) => …) returns every user. Resolve the values first with Promise.all, then filter the results.

Independent round trips in a row (tip)

3 round trips where 1 would do
const profile = await fetch("/api/profile");
const invoices = await fetch("/api/invoices");
const tickets = await fetch("/api/tickets");

None of these calls uses another's result, so they can run together: await Promise.all([…]) in JavaScript, or asyncio.gather(…) in Python. Calls that depend on an earlier result are left alone.

Reads every matching row (warning)

A read with no limit, such as findMany() without take, Django's .objects.all() without a slice, or SELECT without LIMIT, returns more data as your table grows, and so does any response built from it. Paginate with take/limit and a cursor, and select only the fields you need. Reads that return one row (findUnique, .get(), .first()) and limited reads are fine.

I/O inside a recursive function (warning)

A request made at every level of recursion, for example while walking a tree of categories. The count grows with the depth and size of your data. Fetch the whole structure in one call (a recursive SQL query, or an endpoint that returns the tree) and walk it in memory.

Through your own functions

Real code rarely calls the database from the loop itself. It calls a helper that does. Lamied follows those calls across files, through as many helpers as it takes:

src/customers.ts
export async function loadCustomer(id: string) {
  return prisma.customer.findUnique({ where: { id } });
}
src/report.ts: no database code here, still an N+1
import { loadCustomer } from "./customers";

for (const order of orders) {
  order.customer = await loadCustomer(order.customerId);
}

The card for report.ts shows N+1 query through a helper and names the route: loadCustomer() runs a query (prisma.customer.findUnique in src/customers.ts:2). Growth multiplies at every hop: a helper that makes one request per item, called in a loop, is O(n^2).

Calls are matched to the right function through your imports, not by name alone:

  • named, renamed and namespace imports, require(…), and this.method() within a class;
  • relative paths, with or without an extension (including ./api.js for api.ts), index files, and @/ or ~/ path aliases;
  • Python relative imports (from .repo import get_author as author_of), dotted modules, and self.method().

Recursion across functions counts once per level, O(n). Cross-file findings appear a moment after the scan finishes, when every file is known.

Objects passed in, injected, or typed

A method call on an object is followed through the object's type: a typed parameter (repo: UserRepo), a local made with new, or a typed field. That covers constructor injection, as in NestJS or Angular (constructor(private users: UsersService) then this.users.findOne()), and Python's self.repo set from a typed __init__ parameter. Inherited methods are found through extends. When the type is an interface or an abstract class, the call reaches every class that implements it.

Callbacks handed around

The helper loops; the query is in the callback
// util.ts
export async function eachInTurn(ids, visit) {
  for (const id of ids) await visit(id);
}

// report.ts: an N+1 at this line
await eachInTurn(ids, (id) => db.query("SELECT * FROM users WHERE id = $1", [id]));

A function passed to a helper counts once per loop the helper runs it in, whether it's written inline or passed by name (eachInTurn(ids, loadUser)). Passed to an array method, ids.map(loadUser), it runs once per item. A callback given to code Lamied can't see counts as called once. It counts once per loop turn only when that call is awaited, so await retry(() => fetch(url)) in a loop is flagged but an event handler registered in a loop isn't. Registering a handler (addEventListener, setTimeout, on) isn't listed as a request: the requests inside the handler are listed where they are.

Installed packages

Lamied doesn't read installed packages. Like code-analysis tools such as CodeQL, it uses models of well-known networked SDKs: which of their calls go over the network. A client counts under any name, however it was created: const storage = new S3Client(), cur = conn.cursor(), with httpx.Client() as c, or a parameter typed with the package's class (pool: Pool from pg). Local calls such as conn.cursor(), doc() or event listeners don't count.

KindPackages
Hosted APIsStripe, OpenAI, Anthropic, Google Gen AI, Groq, Mistral, Cohere, Octokit, Twilio, SendGrid, Resend, Algolia, Slack, Notion, Nodemailer
HTTP clientsaxios and ky instances, got, httpx, requests, aiohttp, graphql-request, Apollo Client
CloudAWS SDK v2 and v3 (DynamoDB as a database), boto3, Firebase and Firestore, firebase-admin, Google Cloud
Databases and cachesPrisma, pg, mysql2, MariaDB, Neon, Vercel Postgres, PlanetScale, psycopg, asyncpg, PyMySQL, MongoDB, PyMongo, Motor, Supabase, Redis, ioredis, Upstash, Elasticsearch

What doesn't count as growth

Only loops that walk over data count. Loops over a fixed number of turns, or over time, are ignored, so these are never flagged:

  • retry loops like for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++);
  • loops over a constant or a literal list: for (const region of REGIONS), ["us", "eu"].map(…);
  • Python for attempt in range(3);
  • reconnect, polling and retry-until-done loops: while (!signal.aborted), while (Date.now() < deadline), for (let attempt = 0; ; attempt++).

A while loop counts when it drains a collection, such as while (queue.length).

A loop's own list is read once, so in for (const row of await db.query(…)) the query counts once, not per row.

Libraries it recognises

KindJavaScript and TypeScriptPython
HTTPfetch, axios, got, ky, superagent, Node http/https, jQuery ajax, Angular HttpClient (this.http.get)requests, httpx, aiohttp / client sessions, urlopen
DatabasePrisma, raw SQL clients (pool.query, pg, mysql, knex), Sequelize and Mongoose models, TypeORM repositories, MongoDB collections, Supabase, RedisDB-API cursors, SQLAlchemy sessions, Django ORM, PyMongo
LanguageAnalysis
TypeScript, JavaScript, PythonFull: parsed with Lamied's native engine, loop-aware, every finding above
Other languagesCall sites found by name only; the card says so, and no growth is estimated

Region-aware advice from AI

With Pro, Explain with AI on a Network card sends the analysis above, including every finding, with the file. The answer is about its network and database calls only, in three sections: Requests (what triggers which calls, and how they grow), Round trips & latency (calls that can run in parallel or be batched, and N+1 fixes) and Payload & caching.

For latency advice, open Network context at the top of the Network tab and enter your Deployment region (where your server runs, for example us-east-1) and your Primary user region (for example eu-west-1, or “Ethiopia”). With both filled in, the answer weighs the round-trip cost of that distance, multiplied by the sequential calls it found, and whether another region, the edge or a CDN would serve your users better. The fields are remembered and are free text.

Add to AI chat puts the same findings in the assistant's message box, and the assistant's own analysis tool reports them too, so you can ask it to fix an N+1 directly.

Limits

Use the findings to know where to look, then measure with realistic data. For front-end apps, Scenario Lab can replay slow and empty API responses.