IO In Loop¶
Rule details
| Rule ID | io-in-loop |
| Severity | WARNING — likely performance problem |
| Confidence | HIGH (unambiguous IO) · MEDIUM (candidate match) |
| Category | Loop amplifiers |
| Complexity | O(n·IO) → O(1·IO + n) |
The problem¶
A network, database, or file system call inside a loop body. Each iteration incurs a full I/O round-trip — network latency, disk seek, query execution — so the total wall-clock time scales linearly with the loop size. The computational cost of the loop might be trivial, but the wall-clock cost is anything but.
A loop that makes 100 HTTP calls at 50ms each takes 5 seconds minimum, regardless of how fast the server responds. A single batch call takes 50ms + a bit of extra payload processing. The gap only widens with more iterations.
Unlike CPU-bound performance problems, this one doesn't show up in profiler flame graphs the way you'd expect. The thread is waiting, not working. You see it in latency metrics, not CPU utilization.
Where this shows up¶
- In Spring services that call
RestTemplate.getForObject()per item in a list - In data migration scripts that write files or make API calls one record at a time
- In JavaScript backends that
await fetch()sequentially inside aforloop - In Kotlin coroutines that call
httpClient.get()inside aforEachwithout parallelism - In JDBC code that prepares and executes a statement per ID instead of batching
The pattern¶
The fix¶
String placeholders = String.join(",", Collections.nCopies(orderIds.size(), "?"));
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM orders WHERE id IN (" + placeholders + ")");
for (int i = 0; i < orderIds.size(); i++) {
ps.setString(i + 1, orderIds.get(i));
}
ResultSet rs = ps.executeQuery(); // single query
Use coroutineScope with async and awaitAll to run all calls concurrently:
Use Promise.all to fire all requests concurrently:
Suggestion variants¶
Batch the IO operation outside the loop, or use a bulk API
The suggestion text adapts to the detected I/O pattern — HTTP calls get batch endpoint advice, database calls get bulk query guidance, file operations get batching suggestions.
When to ignore this¶
Some I/O has to happen per iteration. Rate-limited APIs that only accept one item at a time, file-per-record output requirements, or explicit per-item transaction boundaries are all valid reasons to suppress. Also fine when the loop is bounded to a small constant (processing 3 config files, not 3,000 records).
// algorilla:ignore io-in-loop — rate-limited API, one call per item required
for (Order order : orders) {
rateLimiter.acquire();
externalApi.submit(order);
}
Covered IO categories¶
The rule detects I/O methods from YAML semantics files. Coverage includes:
- Database: JDBC (
executeQuery,prepareStatement), Groovy SQL (eachRow,rows) - HTTP: Spring RestTemplate (
getForObject,exchange, etc.), Ktor Client,fetch()in JS - File system: Kotlin file extensions (
readText,writeText), Node.js sync fs operations
Framework overlays (Spring, Ktor, Node) extend the base language methods with framework-specific I/O calls.
Related rules¶
I/O cluster — each rule catches a specific flavor of "too many I/O calls":
- N+1 Query — specifically targets repository fetch patterns like
findByIdin a loop - Lazy Loading In Loop — ORM lazy-loaded associations triggered per iteration
- Bulk Load for Single Lookup — loading all records when only one is needed
Loop overhead cluster — when the cost inside the loop is CPU rather than I/O:
- Nested Lookup — linear search in a loop (CPU-bound O(n²))
- String Concat in Loop — string copying in a loop (memory-bound O(n²))
Language-specific examples:
- JavaScript — sequential
await fetch()in a loop - Kotlin — Ktor HTTP client calls per iteration
Configuration¶
Disable globally:
Suppress inline: