{"post":{"slug":"the-event-loop-explained-2026","title":"The event loop explained in 2026: how async runtimes actually work","excerpt":"The event loop is the engine behind Node.js, Python asyncio, and browser JavaScript. Understanding it ends most async debugging mysteries.","category":"code","date":"March 3, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">T</span>he event loop is the scheduling mechanism that powers every async runtime: Node.js, Python's asyncio, browser JavaScript, Rust's Tokio, and Deno. It enables a single OS thread to handle thousands of concurrent I/O operations without spawning a thread per connection. Understanding how it works resolves most async debugging confusion — why callbacks fire in a certain order, why a CPU-heavy function freezes your server, and why `await` does not mean \"runs right now.\"\n\n## What changed in 2026\n\n- **Runtimes now expose event loop metrics.** Node.js 20+ and asyncio 3.12+ expose event loop lag as a built-in metric. Production observability tools (Datadog, Honeycomb) alert on loop lag > 50 ms automatically.\n- **Tokio (Rust) event loop handles 10M+ events/sec** per thread on modern hardware — the practical ceiling for I/O event throughput has moved far beyond what most applications need.\n- **Deno 2 stabilized its event loop API**, and Bun's Zig-based loop showed that the underlying syscall interface (epoll/kqueue/io_uring) is more important than the language layer.\n- **io_uring on Linux 6.x** provides a submit-and-poll interface that reduces syscall overhead by batching I/O completions, and Node.js, Bun, and async Rust runtimes all use it in 2026.\n\n## The core loop\n\nStripped to its essence, an event loop does one thing repeatedly:\n\n```\nwhile (there_are_tasks) {\n    1. Run all ready microtasks (Promise callbacks, await continuations)\n    2. Pick the next macrotask (setTimeout, I/O callback, etc.)\n    3. Run it to completion (sync code, no yielding mid-task)\n    4. Poll the OS for new I/O events (epoll_wait / kevent / IOCP)\n    5. Queue callbacks for any ready events\n    6. Repeat\n}\n```\n\n\"Run to completion\" is the key property: a callback runs entirely before the loop picks the next one. There are no preemptive interruptions.\n\n## How non-blocking I/O works\n\nWhen you call `await fetch(url)` in JavaScript or `await aiohttp.get(url)` in Python:\n\n1. The socket is opened in non-blocking mode.\n2. The connect/write is initiated; the OS kernel begins the operation.\n3. The coroutine suspends and **returns control to the event loop**.\n4. The loop calls `epoll_wait()` (Linux), `kevent()` (macOS/BSD), or `IOCP` (Windows) — a syscall that blocks the OS thread until at least one I/O event is ready.\n5. When the socket has data, the OS returns the event; the loop resumes the coroutine.\n\n```python\nimport asyncio\nimport aiohttp\n\nasync def fetch(url: str) -> str:\n    async with aiohttp.ClientSession() as session:\n        async with session.get(url) as resp:\n            return await resp.text()  # yields here while waiting for bytes\n\nasync def main():\n    # These run concurrently on ONE thread — no threads spawned\n    a, b, c = await asyncio.gather(\n        fetch(\"https://api.example.com/users\"),\n        fetch(\"https://api.example.com/products\"),\n        fetch(\"https://api.example.com/orders\"),\n    )\n```\n\n## Node.js event loop phases\n\nNode.js has a specific phase order per loop iteration:\n\n```\ntimers        → execute setTimeout / setInterval callbacks\npending       → I/O callbacks deferred from previous iteration\nidle/prepare  → internal use\npoll          → retrieve new I/O events; execute I/O callbacks\ncheck         → setImmediate callbacks\nclose         → close event callbacks (e.g., socket.on('close'))\n```\n\n**Microtasks** (`Promise.then`, `queueMicrotask`) drain completely between each phase — they are not a phase themselves.\n\n```javascript\nsetImmediate(() => console.log(\"setImmediate\"));\nsetTimeout(() => console.log(\"setTimeout 0\"), 0);\nPromise.resolve().then(() => console.log(\"microtask\"));\nconsole.log(\"sync\");\n\n// Output:\n// sync\n// microtask      ← microtask queue drains before next macrotask\n// setTimeout 0   ← or setImmediate; order between these two is not guaranteed\n// setImmediate\n```\n\n## Microtasks vs macrotasks\n\n| Type | Queue | Examples | Priority |\n| --- | --- | --- | --- |\n| Microtask | Microtask queue | `Promise.then`, `queueMicrotask`, `MutationObserver` | High — runs after each task |\n| Macrotask | Task queue | `setTimeout`, `setInterval`, I/O callback, `setImmediate` | Normal — one per loop iteration |\n\nA cascade of microtasks (a `Promise` chain that never yields to macrotasks) can starve I/O callbacks indefinitely — a real problem in CPU-intensive recursive promise chains.\n\n## Blocking the event loop\n\nThe single biggest event loop mistake:\n\n```javascript\n// BAD: synchronous CPU work blocks all other tasks for duration\napp.get(\"/compute\", (req, res) => {\n    const result = expensiveSyncComputation(); // 500 ms of CPU\n    res.json({ result });\n    // Every other pending request is frozen for 500 ms\n});\n\n// GOOD: offload to a worker thread\nconst { Worker } = require(\"worker_threads\");\napp.get(\"/compute\", async (req, res) => {\n    const result = await runInWorker(expensiveSyncComputation);\n    res.json({ result });\n});\n```\n\nIn Python asyncio, the equivalent is calling a blocking library function without `asyncio.to_thread`:\n\n```python\n# BAD\n@app.get(\"/data\")\nasync def handler():\n    data = requests.get(\"https://api.example.com/data\")  # blocks the loop!\n    return data.json()\n\n# GOOD\n@app.get(\"/data\")\nasync def handler():\n    async with httpx.AsyncClient() as client:\n        r = await client.get(\"https://api.example.com/data\")\n    return r.json()\n```\n\n## Measuring event loop lag\n\n```javascript\n// Node.js: measure loop lag with perf_hooks\nconst { monitorEventLoopDelay } = require(\"perf_hooks\");\nconst h = monitorEventLoopDelay({ resolution: 20 });\nh.start();\nsetInterval(() => {\n    console.log(`Loop lag p99: ${h.percentile(99) / 1e6} ms`);\n}, 5000);\n```\n\n```python\n# asyncio: log slow callbacks (built-in since 3.2)\nimport asyncio\nloop = asyncio.get_event_loop()\nloop.slow_callback_duration = 0.05  # warn if callback takes > 50 ms\n```\n\n## How to design around the event loop\n\n1. **Never perform synchronous I/O in async code** — use async-native libraries.\n2. **Offload CPU work to threads** — `worker_threads` (Node.js), `asyncio.to_thread` (Python), `tokio::task::spawn_blocking` (Rust).\n3. **Keep individual callbacks short** — yield with `await asyncio.sleep(0)` or `setImmediate` in long loops.\n4. **Monitor loop lag** — alert at 50 ms, investigate at 10 ms.\n5. **Use `Promise.all` / `asyncio.gather` for concurrent I/O** — sequential awaits serialize work unnecessarily.\n\n## Common mistakes\n\n**Sequential awaits that should be parallel.** Two independent `await fetch(urlA)` then `await fetch(urlB)` calls run sequentially. Use `Promise.all([fetch(urlA), fetch(urlB)])` to run them concurrently.\n\n**Mixing sync and async APIs.** `fs.readFileSync` inside an async Express handler blocks the loop for the duration of the read.\n\n**Unhandled promise rejections.** In Node.js 15+, unhandled rejections crash the process. Attach `.catch()` or use a top-level try/catch in `async/await`.\n\n**Forgetting to `await` a promise.** The function returns immediately with a pending Promise; the work runs later, and any errors are silently swallowed.\n\n**Using `setTimeout(fn, 0)` to yield.** This delays by at least one macrotask cycle (~4 ms minimum in browsers). Use `queueMicrotask` for immediate scheduling or `setImmediate` (Node.js) for after-I/O scheduling.\n\n## What to skip\n\n- **Async for CPU-bound tasks without worker threads** — async does not help; it still runs on one thread.\n- **Deeply nested callback pyramids** — async/await has been the standard since 2017; there is no reason to write callback-based code in new projects.\n- **Rolling your own event loop** — Node.js, asyncio, Tokio, and libuv are battle-tested; custom loops have edge cases that take years to discover.\n\n## FAQ\n\n**Why does Node.js use a single thread if CPUs have many cores?**\nThe event loop itself is single-threaded, but Node.js uses libuv's thread pool (default 4 threads) for file I/O and crypto, and `worker_threads` for user CPU work. The single-threaded event loop is a choice for simplicity of programming model, not a technical constraint.\n\n**What is the difference between `process.nextTick` and `Promise.then` in Node.js?**\nBoth are microtasks, but `process.nextTick` callbacks drain first (before Promise callbacks), regardless of registration order. In practice, prefer `Promise.then` / `queueMicrotask` for predictable ordering.\n\n**Does Python asyncio use epoll?**\nYes, on Linux. On macOS it uses kqueue. On Windows it uses ProactorEventLoop (IOCP). Python 3.12 switched the default Windows event loop to ProactorEventLoop to match the platform's native async I/O model.\n\n**Can two event loops run on different threads?**\nYes. Node.js worker threads each have their own event loop. Python allows running separate asyncio event loops in different threads. Communication between loops goes through queues or thread-safe callbacks.\n\n## Where to go next\n\n- [Concurrency explained in 2026](/blog/concurrency-explained-2026)\n- [Threads vs processes explained in 2026](/blog/threads-vs-processes-explained-2026)\n- [Middleware explained in 2026](/blog/middleware-explained-2026)","hue":317,"metaTitle":"The Event Loop Explained in 2026: How Async Runtimes Work","metaDescription":"Plain-language guide to the event loop in 2026. How epoll/kqueue, microtasks, macrotasks, and async/await work in Node.js, Python asyncio, and browsers.","keywords":["event loop explained 2026","how event loop works nodejs","python asyncio event loop","microtasks vs macrotasks 2026","non-blocking io explained","epoll kqueue explained","async await internals 2026","nodejs event loop phases","javascript event loop 2026","event driven programming 2026"],"tldr":["**The event loop is a single-threaded poll-dispatch cycle** — check for ready I/O events, run their callbacks, repeat; no thread switching needed.","**Non-blocking I/O is the key insight** — instead of a thread waiting for a socket, the OS notifies the loop when data is ready (epoll / kqueue / IOCP).","**Microtasks (Promise callbacks) run before the next macrotask** — this ordering is the source of most surprising async execution behavior.","**Blocking the event loop for > 10 ms stalls all other tasks** — CPU-heavy synchronous code in Node.js or asyncio is the single most common performance mistake.","**Skip:** using `setTimeout(fn, 0)` as a yield mechanism in production code — use `queueMicrotask`, `setImmediate`, or `asyncio.sleep(0)` based on the exact semantics you need."],"readTime":"6 min read","wordCount":1275},"related":[{"slug":"sync-vs-async-2026","title":"Sync vs async in 2026: when to use each and why it matters","excerpt":"Synchronous code is simpler; async code handles concurrency. Choosing wrong either blocks your server or creates callback hell. This is the 2026 guide.","category":"code","date":"April 21, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":314},{"slug":"what-is-an-event-loop-2026","title":"What Is an Event Loop in 2026? Plain-English Guide","excerpt":"A clear explanation of what an event loop is in 2026, how it handles many tasks without blocking, why it powers JavaScript, and where it can trip you up.","category":"code","date":"May 8, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":222},{"slug":"async-await-explained-2026","title":"Async/await explained in 2026: how it works and when to use it","excerpt":"Async/await is the dominant concurrency model in modern JavaScript, Python, and Rust — but most developers still misuse it. Here is the definitive 2026 guide.","category":"code","date":"February 18, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":319}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"the-eisenhower-matrix-explained-2026","title":"The Eisenhower Matrix explained in 2026: prioritize what actually matters","excerpt":"The Eisenhower Matrix sorts tasks by urgency and importance so you stop reacting and start doing work that moves your life forward.","category":"guides","date":"March 3, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":85},"older":{"slug":"the-seinfeld-method-explained-2026","title":"The Seinfeld method explained in 2026: build habits with a chain you will not break","excerpt":"Jerry Seinfeld's \"do not break the chain\" system is one of the simplest habit tools ever devised. Here is how it works, what the science says, and how to apply it in 2026.","category":"guides","date":"March 3, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":305}}