completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

libuv & the thread pool

Learn how Node.js uses libuv readiness notifications and a shared thread pool, while JavaScript callbacks can still block everyone.

By the end, you can
  • 01
    Tell handles, requests, and callbacks apartSay which things keep watching, which jobs finish once, and where your JavaScript runs.
  • 02
    Predict when pool jobs waitUse a small model to see why jobs wait when every worker in the pool is busy.
  • 03
    Keep a server answering everyoneSpot a slow regex, huge JSON, and heavy CPU work, then split it into chunks or move it to a worker.

The waiter and the cooks

Node.js runs your JavaScript on one thread. A thread is one line of work that the computer runs step by step. Yet one Node server can talk to thousands of users at once. It does this with help from libuv, a library that sits under Node. A library is code that other programs use.

libuv talks to the operating system: the main program of the computer, like Linux, macOS, or Windows. Each operating system works in its own way. libuv hides those differences, so Node works the same way on all of them.

The main idea

libuv is a library that Node.js uses to wait for slow work without stopping your JavaScript. Slow work means things like reading a file or waiting for data from the network. libuv gives Node an event loop, a way to hear from the operating system about the network, and a small group of helper threads for some other slow jobs.

libuv splits the work into three jobs. Keep them apart in your head:

  • The event loop runs your JavaScript callbacks, one at a time. A callback is a function you hand over now, for Node to call later.
  • The operating system watches network connections. It tells the event loop when one has data ready.
  • The thread pool is a small group of helper threads, called workers. They do some slow jobs, like reading files, away from the event loop. When a job is done, they tell the event loop.

"Async", short for asynchronous, means a function returns before its work is finished. It does not tell you which of these three paths the work takes.

Three helpers. On the left, the operating system watches network sockets and says when one is ready. On the right, a pool of 4 workers does file and other slow jobs and says when one is done. In the middle, the event loop gets both messages and runs your callbacks.systemsocket readywatches socketsreadyevent loopruns your callbacksdonepool12344 workers
The operating system tells the event loop when a socket is ready. The pool tells it when a job is done. Only the event loop runs your JavaScript callbacks.
Real-life analogyOne waiter and four cooks

Picture a small restaurant with one waiter and four cooks. The waiter takes orders and brings food. The waiter never cooks. A drink comes straight from the fridge, so no cook is needed. When all four cooks are busy, a new meal order waits. If the waiter stops to cook a huge meal, nobody else gets served.

In real life: One waiter takes orders and brings food
In JavaScript: The event loop runs callbacks
In real life: Four cooks make the meals that were ordered
In JavaScript: The thread pool does some slow jobs
In real life: A drink comes straight from the fridge
In JavaScript: The operating system says a socket is ready
In real life: The waiter stops to cook a huge meal
In JavaScript: A long callback blocks the event loop

This lesson builds on The Node.js event loop, where you saw the loop's phases. Here you'll learn three things:

  • which work stays on the event loop,
  • which work waits for a worker in the thread pool,
  • and which work the operating system tells Node about, with no worker at all.

Handles and requests

libuv uses two words for the things it keeps track of:

  • A handle lives a long time and can report many events. A server is a handle: it can get a new connection now, and another one later. A timer is a handle too.
  • A request is one short job that finishes once. Reading one file is a request. Once the file is read, the request is done.
A handle keeps watching; a request finishes oncePop out in the code editor (opens in a new tab)JavaScript
const serverHandle = "watch for connections";const fileRequest = "read one file"; console.log(serverHandle);console.log(fileRequest);

Lines 1 and 2 save two plain strings. They are labels for the idea, not real libuv code. Line 1 stands for a server, a handle that watches for connections. Line 2 stands for one file read, a request. Lines 4 and 5 print watch for connections and read one file.

Handles and requests often work together. A network connection is a handle: Node watches it for as long as it stays open. Sending one reply on it is a request: it is done once the reply is sent. To tell them apart, ask one question: does it keep watching, or does it finish once?

The table below compares the two. Read each row as: this word means this, and here is an example from Node.

Handles and requests compared
WordWhat it meansAn example in Node
HandleSomething that lives a long time and can report many events.A server, or a timer.
RequestOne short job that finishes once.Reading one file, or looking up one website name.

How Node hears about the network

A socket is one network connection, like the link between your server and one user's browser. Data on a socket arrives whenever the other side sends it. Node does not keep a worker waiting at each socket. Instead, libuv gives the operating system a list of sockets to watch.

The operating system tells libuv when a socket is ready. Ready means one of these:

  • it has data to read,
  • it can take more data to send,
  • or it has closed.

This message is called a readiness notification. Because of it, one Node server can keep thousands of connections open with no extra workers.

Hand over callbacks now; they run when data arrivesJavaScript
server.on("connection", (socket) => {  socket.on("data", (chunk) => console.log(chunk.length));});

Line 1 hands Node a callback for each new connection, then moves on at once. Line 2 hands over another callback, for data that arrives later on that connection. When data arrives, line 2 prints how many bytes came in. This code needs a real Node server, so you can read it here but not run it.

Each operating system has its own tool for watching sockets:

  • Linux uses epoll.
  • macOS uses kqueue.
  • Windows uses IOCP.

libuv hides these differences, so your callbacks look the same everywhere. The reading and writing on sockets happens on the event loop's own thread. It does not go to the thread pool.

A ready socket still needs a short callback

Ready means the operating system says the work can go on. It does not mean your callback is free. If the callback that runs next takes a long time, it still blocks the event loop. Every other ready callback has to wait.

The thread pool

Files work differently from sockets. Not every operating system can tell libuv "this file is ready" in the same way. So libuv reads and writes files in its thread pool: a small group of worker threads that the whole program shares. Each worker takes one job, does it, and then takes the next.

The pool also does a few other slow jobs, like looking up a website name. When a worker finishes a job, libuv tells the event loop. Then the event loop runs your callback.

Six file jobs shared by two workers (a model)JavaScript
const jobs = [  { name: "menu.json", duration: 3 },  { name: "orders.json", duration: 2 },  { name: "users.json", duration: 4 },  { name: "prices.json", duration: 1 },  { name: "notes.json", duration: 2 },  { name: "report.json", duration: 3 },]; console.log(schedulePoolJobs(2, jobs));

Lines 1 to 8 list six file jobs. Each has a made-up length of time, called duration. Line 10 calls schedulePoolJobs, a small helper written for this lesson. It shares the jobs between two workers. It is not real libuv code, and it does not time your disk. It shows one rule: when every worker is busy, the next job waits.

The helper gives each job to the first worker that is free. The picture shows the result for two workers.

Two workers and six file jobs, with time going down. Worker 1 runs menu.json until time 3, prices.json until 4, notes.json until 6, and report.json until 9. Worker 2 runs orders.json until 2, then users.json until 6.timeworker 1menu.jsonends 3prices.jsonends 4notes.jsonends 6report.jsonends 9worker 2orders.jsonends 2users.jsonends 6
Each job goes to the first worker that is free. users.json waits until orders.json ends at time 2. report.json waits until time 6 and ends last, at 9.
Step through two workers sharing six file reads
Step 0 of 8Ready
Your turn: follow the blue line

Replay a small model of the thread pool. It shows when jobs wait for a free worker. It does not look inside real libuv workers.

Running in
  1. script
Next: line 1
  { name: "menu.json", duration: 3 },  { name: "orders.json", duration: 2 },  { name: "users.json", duration: 4 },  { name: "prices.json", duration: 1 },  { name: "notes.json", duration: 2 },  { name: "report.json", duration: 3 },]; console.log(schedulePoolJobs(2, jobs));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Press Step to walk through the replay. Each step shows the jobs that end at one time. The replay runs this lesson's model. It doesn't look inside real libuv workers.

By default, libuv's pool has 4 workers. You can change the number with UV_THREADPOOL_SIZE. That is an environment variable: a setting you give a program when you start it. Before you change it, keep in mind:

  • The pool is shared by the whole Node program.
  • More workers use more memory.
  • More workers don't help a slow callback of your own, because callbacks don't run in the pool.

So measure first, before you change it.

Set the pool size before Node startsShell
UV_THREADPOOL_SIZE=8 node server.mjs

This command sets UV_THREADPOOL_SIZE to 8, then starts server.mjs with Node. Set it in the command that starts Node, as here. Once the pool has started, changing the setting does not resize it. More workers can also fight over the same CPU cores and make things slower. Treat the number as a setting you measure, not a quick speed fix.

When every worker is busy

When all workers are busy, the next file read, website-name lookup, crypto job, or compression job waits in a shared line. It takes longer to finish, even if the event loop is free. Sockets don't use the pool, so Node can still handle them during that time. So measure two things separately: how long pool jobs wait, and how long the event loop is busy.

In the playground, change the number of workers. Watch when each job ends, and when the last one ends.

Playground: change the number of workers
The same six file jobsJavaScript
const jobs = [  { name: "menu.json", duration: 3 },  { name: "orders.json", duration: 2 },  { name: "users.json", duration: 4 },  { name: "prices.json", duration: 1 },  { name: "notes.json", duration: 2 },  { name: "report.json", duration: 3 },]; console.log(schedulePoolJobs(2, jobs));
Who runs each job, and whenlast job ends at 5
menu.jsonworker 1: 0 to 3

Takes 3 units of time in the model.

orders.jsonworker 2: 0 to 2

Takes 2 units of time in the model.

users.jsonworker 3: 0 to 4

Takes 4 units of time in the model.

prices.jsonworker 4: 0 to 1

Takes 1 unit of time in the model.

notes.jsonworker 4: 1 to 3

Takes 2 units of time in the model.

report.jsonworker 2: 2 to 5

Takes 3 units of time in the model.

Ends at time 1prices.json

This job ends at this time in the model.

Ends at time 2orders.json

This job ends at this time in the model.

Ends at time 3menu.json, notes.json

These jobs end at the same time in the model.

Ends at time 4users.json

This job ends at this time in the model.

Ends at time 5report.json

This job ends at this time in the model.

Try it yourself
Number of workers

With 4 workers, the last of the six jobs ends at time 5. Change only the number of workers, then look at which jobs had to wait.

This runs the lesson's small model. In real Node, the order in which file reads end can differ, and you can't see which worker ran each job.

Which Node functions use the pool

An async function returns before its work is done. That does not tell you how the work gets done. The function might wait on the operating system, or it might wait for a worker in the pool.

This matters when many pool jobs run at once. A slow batch of file reads can make an unrelated crypto job wait too, because they share the same workers.

readFile returns before the file is readJavaScript
import { readFile } from "node:fs";readFile("menu.json", "utf8", (error, text) => console.log(error ?? text));console.log("request submitted");

Line 1 imports readFile from Node's fs (file system) module. Line 2 asks for one file read and hands over a callback. That is one request for the pool. Line 3 prints request submitted first. The callback runs later, once the read is done. It prints the file text, or the error if the file is missing. This code needs Node and a real file, so you can read it here but not run it.

The table shows where common kinds of Node work go. Read each row as: this work is handled here, and this is why.

Where common Node work goes
What the work isWho handles itWhy
Network socketsThe operating system.epoll, kqueue, or IOCP tells the event loop when a socket is ready.
Most async fs functionsThe libuv thread pool.Not every operating system can say when a file is ready, so a worker does the file work.
dns.lookupThe libuv thread pool.It asks the operating system to turn a website name into a number address, which can take a while.
Async crypto and zlibThe libuv thread pool.Examples are pbkdf2, scrypt, randomBytes, and compression. They are slow math jobs.
Your callbackThe event loop.Your JavaScript runs until it ends, or until it gives the event loop a turn.

Node's docs list these users of the pool:

  • Most async fs functions, which read and write files.
  • dns.lookup, which turns a website name into a number address.
  • Async zlib functions, which make data smaller (compress it) or unpack it again.
  • Some async crypto functions, which do security math: pbkdf2, scrypt, and async randomBytes.

Network sockets do not use the pool. The operating system tells Node when they are ready.

Five crypto jobs share four default workersJavaScript
import { pbkdf2 } from "node:crypto";

const jobs = Array.from({ length: 5 }, (_, index) => new Promise((resolve, reject) => {
  pbkdf2("password", "salt", 1_000, 16, "sha256", (error) => {
    if (error) reject(error);
    else resolve(index + 1);
  });
}));

console.log("submitted", jobs.length);
await Promise.all(jobs);
console.log("all finished");

Line 1 imports pbkdf2, a slow function that turns a password into a secret key. Lines 3 to 8 start five pbkdf2 jobs and wrap each one in a promise. Line 10 prints submitted 5. Line 11 waits for all five jobs. Line 12 prints all finished.

The pool has four workers by default, so at least one job must wait for a free worker. Which job ends first, and how long each one takes, can change from run to run. Don't write code that depends on them.

Where does this work go?
  • A network socket gets new data.
  • An async file read.
  • dns.lookup("example.com").
  • A timer callback that is due.
  • Async crypto.pbkdf2.
  • A while loop in a request handler.
Try it yourself
0 of 6 correct

Sort each card by who handles the work first. Then read the explanation.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

JavaScript can still block

The thread pool does not run your JavaScript callbacks. Once Node calls a callback, that callback runs to the end without a break. While it runs, the event loop can do nothing else. We say the callback blocks the event loop.

A long loop, a huge piece of JSON to read, or a slow regular expression can all block it. Then timers, new network data, and every other callback have to wait.

A busy loop delays a timerPop out in the code editor (opens in a new tab)JavaScript
setTimeout(() => console.log("timer"), 0);const end = Date.now() + 30;while (Date.now() < end) {}console.log("busy work done");

Line 1 sets a timer with a delay of 0 milliseconds. Line 2 picks an end time, 30 milliseconds from now. Line 3 loops and does nothing until that time comes, which keeps JavaScript busy. Line 4 prints busy work done. Only after the whole script ends can the timer's callback run and print timer. This code works the same way in a web page.

The printed order. Card 1, the busy loop on lines 2 and 3, keeps JavaScript busy for 30 milliseconds. Card 2, busy work done, prints first from line 4. Card 3, the timer from line 1, prints second.lines 2 and 3busy loopbusy for 30 msline 4busy work doneprints 1stline 1's timertimerprints 2nd
The timer from line 1 is ready almost at once. It still waits, because the busy loop keeps the event loop busy until the script ends.

The lesson only promises the order, not exact times: the busy code always finishes before the timer callback runs. On a server, this is a real danger. Say the time a callback takes grows with the size of its input. Then an attacker can send a huge input on purpose, and the server stops answering everyone else. This is called a denial of service (DoS) attack. It can happen even though the rest of the server uses async code.

Slow patterns and huge JSON

A regular expression, or regex, is a pattern for matching text. For example, /^a+$/ matches text made only of one or more letter as. Some patterns take a very long time on certain inputs. An attacker can use that to block the server. This is called ReDoS, short for regular expression denial of service.

A warning sign is a repeat inside a repeat, like (a+)+. Take the pattern /^(a+)+$/ and a string of as that ends in !. The match must fail because of the !. But before it gives up, the regex tries every way to split the as into groups. Each extra a can double the work.

A risky pattern on a short inputPop out in the code editor (opens in a new tab)JavaScript
const input = "aaaa!";console.log(/^(a+)+$/.test(input));

Line 1 uses a short input, so it finishes quickly here. Line 2 prints false, because the ! at the end doesn't fit the pattern. The danger is not this short input. The danger is taking a long input from a stranger and expecting the match to be quick.

Huge JSON is a simpler problem. JSON.parse turns JSON text into an object, and JSON.stringify turns an object into JSON text. Both run from start to finish without a break. Their time grows in step with the size of the text. Still, a huge text can block the event loop for a long time. This is called JSON DoS. To protect against it:

  • Limit the size of a request body before you parse it.
  • Keep the JSON shapes you accept simple.
  • Move truly slow work out of the callback.
Check the size before parsingPop out in the code editor (opens in a new tab)JavaScript
const body = '{"user":"Asha"}';console.log(body.length <= 100 ? JSON.parse(body).user : "too large");

Line 1 is a short JSON text. Line 2 checks that it is at most 100 characters long before it parses it. The text is short enough, so line 2 prints Asha. A real server should check the size while it reads the request. Checking after a huge body is already stored in memory is too late.

Split long work, or move it to a worker

Some work is simple but long, like adding up a long list of numbers. You can split it into steps:

  1. Do a small piece of the work, called a chunk.
  2. Remember how far you got.
  3. Give the event loop a turn before the next chunk.

This is called partitioning. Between chunks, the event loop can run other waiting callbacks. Splitting doesn't make the work faster, and it doesn't use a second CPU core.

Add two numbers at a time, then give the loop a turnJavaScript
function sumInChunks(numbers, done) {  let index = 0;  let total = 0;  function nextChunk() {    const stop = Math.min(index + 2, numbers.length);    while (index < stop) total += numbers[index++];    if (index < numbers.length) return setImmediate(nextChunk);    done(total);  }  nextChunk();} setTimeout(() => console.log("timer"), 0);sumInChunks([1, 2, 3, 4], (total) => console.log("sum", total));

Line 1 defines sumInChunks. Line 5 picks where this chunk stops: two numbers on, or the end of the list. Line 6 adds those numbers to the total. Line 7 checks if any numbers are left. If so, it calls setImmediate(nextChunk). setImmediate asks Node to run a function soon, after other waiting work gets a turn. When no numbers are left, line 8 hands the total to the done callback.

Line 13 sets a timer, and line 14 starts the sum. The timer can run between the two chunks. It may print before or after sum 10, depending on timing. What matters is that it gets a chance between chunks. This code needs Node, so you can read it here but not run it.

Adding 1, 2, 3, and 4 in two chunks. Chunk 1 adds 1 plus 2, total 3. Then the event loop gets a turn, and the timer from line 13 may run here. Chunk 2 adds 3 plus 4, total 10. Then the code prints sum 10.chunk 11 + 2total 3a turntimermay run herechunk 23 + 4total 10donesum 10prints
Each chunk adds only two numbers. setImmediate gives the event loop a turn between chunks, so the timer from line 13 can run before chunk 2.

For heavy CPU work, use worker_threads. This Node module starts a worker thread: a second thread with its own JavaScript, which can run on another CPU core. Moving work there is called offloading. It has a cost:

  • The data you send is copied or moved to the worker.
  • The result comes back to the event loop as a message, and the event loop still sends the reply.

But the main callback no longer does the whole calculation, so other users don't wait for it.

Send the heavy work to a worker threadJavaScript
import { Worker } from "node:worker_threads"; const worker = new Worker(new URL("./price-worker.mjs", import.meta.url));worker.postMessage({ prices: [3, 4, 5] });worker.once("message", (total) => console.log(total));

Line 1 imports Worker from Node's worker module. Line 3 starts one worker, which runs the file price-worker.mjs. Line 4 sends it a list of prices. Line 5 waits for one message back, the total, and prints it.

Don't use the libuv thread pool for your own JavaScript work. It is meant for Node's built-in file, crypto, and other jobs. Use worker threads for your own heavy work.

Good habits for a Node server

Start with short callbacks and limits on input size. Async file, crypto, website-name, and compression jobs share the same pool, so they can wait in line behind each other. A slow pool job leaves fewer workers for the next job. A slow JavaScript callback is worse. It blocks every callback on the event loop, even ones whose data has already arrived.

  • Use the async file and crypto functions when you handle requests. Avoid the Sync versions, like readFileSync.
  • Put limits on request bodies, regex input, the size of each job, and how big caches can grow.
  • Measure how long jobs wait, and how busy the CPU is, before you change UV_THREADPOOL_SIZE.
  • Split simple loops into chunks. Use worker_threads for long, heavy CPU work.
  • Keep socket callbacks short, even after the operating system says a socket is ready.

This is why Node is good for servers that mostly wait for files and the network. One event loop can look after many waiting sockets. But your code has to be fair. If a request handler does too much work for one user, every other user waits for the same event loop or pool.

Common misconceptions

  • “Async means a new thread.” Many network jobs use no extra thread at all. The operating system tells the event loop when a socket is ready.
  • “The pool runs my callback.” A pool worker only does the job, like reading the file. Your callback then runs on the event loop.
  • “More pool workers fix slow JavaScript.” A busy while loop, regex, or JSON.parse still blocks the event loop. Pool workers don't run it.
  • “A timer of zero runs right away.” It runs only after the current JavaScript finishes and the event loop gets a turn.
  • “ReDoS needs a huge regex.” A short pattern with a repeat inside a repeat can be enough, if the input is long and has no size limit.

The table compares words that sound alike. Read each row as: this word means this, but not that.

Words that sound alike but mean different things
WordWhat it meansWhat it does not mean
Asynchronous (async)Your code can go on while the work is still going.It never uses the thread pool.
Non-blocking network workThe operating system says when a socket is ready to read or write.A worker waits at every socket.
Thread poolA small, shared group of workers for some slow jobs.One new thread for every request.
UV_THREADPOOL_SIZEA setting, given when Node starts, for the number of pool workers.A cure for slow JavaScript callbacks.

Practice exercises

Exercise 1 · Warm-upPredict the first line

Type the first line this program prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
setTimeout(() => console.log("timer"), 0);
const end = Date.now() + 30;
while (Date.now() < end) {}
console.log("busy work done");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upHandle or request?

    A server can accept another connection later. Is it a handle or a request?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const handle = "timer";
    const request = "one file read";
    console.log(handle, request);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFollow one worker

      This uses the lesson's model with one worker. Type the end times it prints, with a comma between them.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      console.log(schedulePoolJobs(1, [{ name: "a", duration: 2 }, { name: "b", duration: 1 }]).map((job) => job.finish).join(","));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeA safe pattern

        What does this safe pattern print?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const input = "aaaa!";
        console.log(/^a+!$/.test(input));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeProtect a server from huge JSON

          Your server has a web address that accepts product lists as JSON from outside your company. Name one way to protect it before you parse the body.

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply it to a real app

            A dashboard makes a heavy image report for each customer. How should you handle that heavy CPU work?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, first name the path: a callback on the event loop, a ready socket from the operating system, or a job in the thread pool.

              libuv and thread-pool quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the event loop do while this busy loop runs?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                setTimeout(() => console.log("timer"), 0);
                const end = Date.now() + 1;
                while (Date.now() < end) {}
                console.log("done");

                Choose an answer to see the explanation.

              2. Question 2 of 7Which one is a handle, in plain words?

                Choose an answer to see the explanation.

              3. Question 3 of 7Who usually says that a network socket is ready?

                Choose an answer to see the explanation.

              4. Question 4 of 7How many workers does libuv's thread pool have by default?

                Choose an answer to see the explanation.

              5. Question 5 of 7Which one uses the thread pool, according to Node's docs?

                Choose an answer to see the explanation.

              6. Question 6 of 7Why can /^(a+)+$/ be dangerous when a stranger sends the input?

                Choose an answer to see the explanation.

              7. Question 7 of 7When is splitting work into chunks a good first choice?

                Choose an answer to see the explanation.

              Key takeaways

              • A handle lives a long time and can report many events. A request is one short job that finishes once.
              • The operating system tells Node when a network socket is ready, with tools like epoll, kqueue, and IOCP.
              • libuv's shared thread pool has four workers by default. It does file, website-name lookup, crypto, and compression jobs.
              • A long loop, a slow regex, or a huge JSON.parse still blocks the event loop.
              • Split simple work into chunks. Move long, heavy CPU work to worker threads.

              Remember the one-liner.
              Node can wait for files and the network without stopping, but every callback and every pool job must finish quickly, so others get a turn.

              Coming next: Browser architecture, where you'll see how a browser splits its work into separate programs, called processes, and threads.

              CompleteFrontend Clear concepts. Working examples.