MAD Study Hub

Home / MAD2 / Week 11

Week 11 · Performance: measuring and caching

Official syllabus line: Performance
About 90 minEnd Term material (see exam radar)BSCS2006 (+ project BSCS2006P) · Thejesh G N, Nitin Chandrachoodan
Before you start: MAD2 weeks 1 to 10 (Flask API, Vue, Celery and Redis basics, week 10 on background jobs).

The one-minute story

Think of it like this

A tea stall at a railway station. Every customer orders "one chai". If the stall-owner boils fresh water, adds tea leaves and waits four minutes each time, the queue grows and customers walk away. So the owner makes a big pot in the morning and pours from it. That pot is a cache: a ready answer to a question that gets asked again and again.

But the pot gets cold (the answer becomes stale), so after an hour the owner throws it away and brews again (expiry, or TTL). If the price of sugar changes, he must brew a new pot at once (invalidation).

Before you buy a bigger stove, you measure: how long does one chai take, how many chai per hour, which step is slow? Maybe the real problem is a customer who keeps asking for 300 cups one at a time (the N+1 problem), or a slow tap (a missing index). Performance work is: measure, find the bottleneck, fix that one thing, measure again.

By the end of this week you can:

  • name the numbers that describe speed (latency, throughput, percentiles, TTFB, page load) and the tools that measure them;
  • find a slow spot: N+1 queries, a missing index, a heavy page;
  • use browser caching headers, Flask-Caching with Redis, @cache.cached and @cache.memoize, and predict response times in exam code;
  • design cache keys, choose an expiry and decide when to invalidate;
  • speed up a Vue front end (lazy loading, compression, debounce) and add pagination;
  • run a before and after measurement.

Words you need

Words you need
  • Latency - the time one request takes, from sending to receiving the answer.
  • Throughput - how many requests per second the server finishes.
  • Percentile (p50, p95, p99) - p95 = 95 percent of requests were faster than this number. It shows the slow tail that an average hides.
  • TTFB (time to first byte) - time until the first byte of the response arrives.
  • FCP / TTI / TBT / CLS - Lighthouse metrics: First Contentful Paint, Time to Interactive, Total Blocking Time, Cumulative Layout Shift.
  • Profiling - measuring which part of the code takes the time.
  • N+1 queries - one query for a list plus one extra query for each item.
  • Index - a lookup structure that lets the database find rows without scanning the whole table.
  • Cache - stored copies of answers, reused instead of recomputing.
  • Hit / miss / hit ratio - found in cache / not found / hits divided by all lookups.
  • TTL (time to live) - seconds after which a cache entry expires.
  • Invalidation - removing or refreshing a cache entry when its data changed.
  • Memoisation - caching a function's return value by its arguments.
  • CDN - Content Delivery Network: servers around the world that deliver your static files from a place near the user.
  • Debounce - wait until the user stops typing before acting.

Idea 1: What to measure, and with what

"The site is slow" is not a measurement. Pick numbers.

The idea
  • Latency / response time: one request, in milliseconds.
  • Throughput: requests per second under load.
  • Percentiles: report p50 (median), p95, p99. An average of 100 ms can hide a p99 of 3 seconds.
  • TTFB: how long the server (plus network) took to start answering.
  • Page load: the whole page, shaped mostly by the number of HTTP requests and the size of files (images, JavaScript).

Lighthouse Performance score is built from FCP, TTI (Time to Interactive), TBT, Speed Index and layout shift (CLS). It does not include "database query time" or "HTTPS usage".

ToolWhat it doesTypical use
Browser dev tools, Network tabevery request, size, time, headers, cache statuswhich file is big or slow?
Dev tools, Performance tabflame chart of scripting, renderingwhy is the page janky?
Lighthouse (in Chrome dev tools)scores and advice; can emulate throttled network and mobile deviceswhole-page audit
Apache Bench abfires many requests at one URLsimple throughput/latency test
LocustPython-scripted virtual usersrealistic load with logins
time / time.perf_counter()stopwatch for a command or codeone function
Flask debug toolbar, cProfilewhich Python function is slowprofiling

Apache Bench example (100 requests, 10 at a time):

ab -n 100 -c 10 http://127.0.0.1:5000/api/books

Read Requests per second (throughput), Time per request (latency) and the Percentage of the requests served within a certain time table (percentiles).

A tiny stopwatch in Python:

import time

def timed(fn, *a):
    t = time.perf_counter()
    result = fn(*a)
    return result, round((time.perf_counter() - t) * 1000, 1)   # milliseconds
Your turn: 1. Which number?

(a) "Most users wait under 400 ms but a few wait 5 s." Which metric exposes this? (b) "The server handled 800 requests in 10 seconds." What is the throughput? (c) Which Lighthouse item is not a performance metric: FCP, TTI, TBT, DQT?

Need a hint?

Percentiles; divide; one of them is invented.

Show the full solution

(a) p95 or p99 (the tail), not the average. (b) 800 / 10 = 80 requests per second. (c) DQT (Database Query Time) is not a Lighthouse metric.

Idea 2: Finding the bottleneck

Do not guess. Measure the layers in order: network (dev tools), server (profiling, logs), database (query count and time), front end (scripts).

N+1 queries

You load 3 authors, then for each author you touch author.books. That is 1 query for the list plus 3 more: N+1. With 1000 authors it is 1001 queries.

from sqlalchemy import select
from sqlalchemy.orm import selectinload

# slow: lazy loading, one query per author
for a in db.session.scalars(select(Author)):
    print(a.name, len(a.books))

# fast: load all books for all authors in one extra query
stmt = select(Author).options(selectinload(Author.books))
for a in db.session.scalars(stmt):
    print(a.name, len(a.books))

I counted the SQL statements with SQLAlchemy's before_cursor_execute event on 3 authors with 2 books each: the first loop ran 4 queries, the second only 2. A related fix is a SQL JOIN.

Missing indexes

Without an index the database reads every row (a "full table scan") to find WHERE author_id = 7. An index on that column lets it jump straight to the rows.

class Book(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    author_id = db.Column(db.Integer, db.ForeignKey("author.id"), index=True)
    title = db.Column(db.String(100))

In SQLite you can ask for the plan: EXPLAIN QUERY PLAN SELECT * FROM book WHERE author_id = 7; shows SEARCH ... USING INDEX when an index is used and SCAN when it is not. Indexes speed up reads but slow down writes a little and take space, so index the columns you filter or join on, not everything.

Other usual suspects

  • Returning too much data (a full table instead of one page): use pagination.
  • Heavy work in the request: sending mail, generating a PDF, a CSV of 100000 rows. Move it to a Celery task (week 10).
  • Big images and scripts on the front end.
  • Repeating the same expensive answer for every user: cache it.
Watch out - real exam trap
  • Optimising before measuring. The slow part is often not the one you suspect.
  • Judging by the average. Look at p95 and p99.
  • Testing on your fast laptop with a tiny database. Use realistic data volume.

Idea 3: Cache layers

A request may be answered at several layers. The earlier, the cheaper.

LayerWhereExampleShared by users?
Browser cachethe user's browser (a private cache)Cache-Control: max-ageno, one per client
CDN / shared cache / reverse proxyservers between user and youCDN serving your JS and imagesyes
Application cachememory or Redis, near your Flask codeFlask-Cachingyes
Database cachethe database's own buffers or a stored resultquery cache, a summary tableyes

A CDN is a group of servers that delivers web content fast, usually from a server near the client (usually, not always). A shared cache may be used for many users, so it must not hold personalised responses. A private cache is tied to one client. A reverse proxy (such as Nginx) can cache responses for similar later requests. A forward proxy protects clients, not servers, so it is a different thing.

The main benefits of caching: lower latency, lower load on the origin server, less bandwidth, better handling of traffic spikes (scalability). It does not give encryption, DDoS protection or remove the need for the database altogether.

Browser caching headers

The server tells the browser how to cache with response headers.

HeaderMeaning
Cache-Control: max-age=3600reuse for 3600 seconds without asking
Cache-Control: no-storedo not store at all (sensitive data)
Cache-Control: no-cachemay store, but revalidate before each reuse
Cache-Control: public / privateshared caches may / may not store it
Expiresan older absolute date for expiry
ETaga fingerprint of the content
Last-Modifiedwhen the content last changed

Revalidation: the browser sends If-None-Match: <etag> (or If-Modified-Since). If nothing changed, the server replies 304 Not Modified with an empty body, and the browser reuses its copy. Cheap, but still one round trip.

from flask import Flask, make_response, request, jsonify
app = Flask(__name__)

@app.get("/data")
def data():
    r = make_response(jsonify(x=1))
    r.headers["Cache-Control"] = "public, max-age=60"
    r.add_etag()
    return r.make_conditional(request)

I tested this with the Flask test client: the first request returned 200 with an ETag; repeating it with If-None-Match: <that etag> returned 304 and an empty body.

Related facts that appear in exams: localStorage has more space than cookies and keeps data after the browser closes; sessionStorage is cleared when the tab or browser closes. A hard refresh bypasses the cache for the page's files but does not delete the whole browser cache of the site. Browsers can cache JavaScript files, and normally only GET requests (without a body) are cached.

Your turn: 2. Headers

Which of these control caching: Cache-Control, Expires, Bearer, Content-Type, ETag? What status code means "your copy is still valid"?

Need a hint?

One of them is about login, one about format.

Show the full solution

Cache-Control, Expires and ETag (validation). Bearer belongs to Authorization (tokens) and Content-Type states the data format. The status is 304 Not Modified.

Idea 4: Application caching with Flask-Caching and Redis

pip install flask-caching redis
import time
from flask import Flask
from flask_caching import Cache

app = Flask(__name__)
app.config.update(
    CACHE_TYPE="RedisCache",                 # or "SimpleCache" (in-process memory) for tests
    CACHE_REDIS_URL="redis://localhost:6379/1",
    CACHE_DEFAULT_TIMEOUT=300,               # seconds, applies when a decorator gives no timeout
)
cache = Cache(app)

SimpleCache keeps entries in the Python process (lost on restart, not shared between processes), good for learning. RedisCache stores them in Redis: shared between workers, survives an app restart.

@cache.cached caches the whole view

@app.get("/time")
@cache.cached(timeout=5)
def get_time():
    return str(time.time())

The first request runs the function and stores the response. Requests within 5 seconds get the stored copy: the function is skipped. After 5 seconds the entry has expired and the function runs again.

Solved example: 1. Predict the timestamps

Question (like a real End Term question). CACHE_DEFAULT_TIMEOUT = 5 and @cache.cached() with no argument on get_time, which returns the current time. Request 1 at 00:00:00, request 2 at 00:00:02, request 3 at 00:00:06. What does each return?

Solution.

  • Request 1: miss, runs, returns 00:00:00 and stores it (expires at 00:00:05).
  • Request 2: 2 s later, still fresh: hit, returns 00:00:00.
  • Request 3: 6 s later, expired: miss, runs again, returns 00:00:06.

Answer: 00:00:00, 00:00:00, 00:00:06. I checked the same logic with SimpleCache and a 2 second timeout: two quick calls matched, a call after the timeout differed.

The key matters: cached vs memoize

The idea

@cache.cached(timeout=60) caches a view. By default its key is the URL path (request.path). Two different paths are two different entries. But if you pass key_prefix='compute', the key is that fixed string, so different URLs share one entry!

@cache.memoize(timeout=60) caches a function's return value and builds the key from the function and its arguments. get_data("a") and get_data("b") are separate entries; get_data("a") twice is one computation. This is memoisation.

calls = []

@cache.memoize(timeout=60)
def square(n):
    calls.append(n)
    return n * n

square(3); square(3); square(4)
print(calls)                       # [3, 4]  (the second square(3) was a hit)
cache.delete_memoized(square, 3)   # invalidate only square(3)
square(3)
print(calls)                       # [3, 4, 3]

I ran exactly this and got [3, 4] and then [3, 4, 3].

Solved example: 2. Cached with a key_prefix: a classic trap

Question.

@app.route('/compute/<int:x>/<int:y>')
@cache.cached(timeout=60, key_prefix='compute')
def compute(x, y):
    return jsonify({'result': x * y})

/compute/10/20 is requested, then /compute/5/10 within 60 seconds. What are the two responses?

Solution. The key is the fixed string compute, not the URL. The first call stores {"result": 200} under that key. The second URL finds the same key and gets {"result": 200} again, although 5 times 10 is 50. So the answers are 200 and 200. Without key_prefix (default key from the path) they would be 200 and 50.

Solved example: 3. Total time with memoize

Question.

@cache.memoize(timeout=300)
def compute_data(user_id):
    time.sleep(5)
    return f"Data for user {user_id}"

Requests for user ids 1, 2, 1 arrive within 5 minutes. Total time?

Solution. id 1: miss (5 s). id 2: a different argument, miss (5 s). id 1 again: hit (about 0 s). Total 10 seconds.

Variation: a view @cache.cached(timeout=40) that sleeps 30 s, visited by user 1, and by user 2 three minutes later: the entry (40 s) has expired, so user 2 also waits 30 s, and the difference in response times is about 0 seconds. With timeout=120 also 3 minutes later, again expired. And "two requests within 50 seconds to a view cached for 100 seconds with a 25 s sleep": the first takes 25 s, the second about 0, so the difference is 25 seconds. Always compare the gap between requests with the timeout.

A more careful look at how long entries live

  • The timeout in the decorator overrides CACHE_DEFAULT_TIMEOUT.
  • A memoize timeout of 180 s with visits at 0, 150 s and 210 s: the first is a miss, the second a hit, the third is only 60 s after the second but 210 s after the store, so it is a miss. (TTL counts from when the entry was stored, not from the last visit.)
  • Expiry is a time to live and does not refresh on a hit.
Watch out - real exam trap
  • "@cache.cached caches by function arguments." No: it caches a view by its path (or by key_prefix); memoize uses the arguments. The statement that "memoize does not include parameters in the key" is false.
  • "Caching removes the need for a database." No, only for repeated lookups.
  • A cached personalised page (a page showing "Hello Asha") returned to a second user is a bug and a privacy leak. Put the user id in the key, or do not cache it.
  • TTL runs from the store time.
  • Time-based questions: draw a timeline, mark the expiry, and then see which requests land before it.

Idea 5: Cache keys, expiry and invalidation

Key design

A good key is unique for every different answer and identical for every repeat of the same answer.

Answer depends onKey should include
nothing about the user (a list of public books)books:page:2
one useruser:7:dashboard
a filterbooks:genre:sci:page:2
a score per countryscore:country:India

Bad keys: too coarse (all users share one entry), or including something that changes every time (a timestamp), which gives a hit ratio of zero.

Expiry (TTL)

Pick it by how stale the data may be. A dashboard that can be 5 minutes old: TTL 300. A social feed or stock price: short TTL. Caching a feed for a long time is a bad idea (the question that calls it "recommended" is false). Rarely changing data (list of countries): long TTL.

Invalidation strategies

StrategyHowGoodBad
Time-basedjust let the TTL expiresimpleststale until expiry
Delete on writewhen data changes, cache.delete(key) (or delete_memoized)always fresh next readyou must remember every write path
Write-throughon write, update the database and the cachereads stay fast and freshmore code, two stores to keep consistent
@app.post("/books")
def add_book():
    # ... save to the database ...
    cache.delete("view//books")       # or cache.delete_memoized(list_books)
    return {"ok": True}, 201

To find the key of a cached view: with the default settings it is view/ plus the path, for example view//books (the double slash is correct, as the path starts with /). Easier and safer: use cache.delete_memoized(function) for memoised functions, or give views an explicit key_prefix.

Hit ratio and stampede (concept)

Hit ratio = hits / (hits + misses). 90 hits and 10 misses is 0.9. A low ratio means bad keys or a TTL that is too short.

Cache stampede: a popular entry expires and 500 requests arrive at the same moment. All see a miss and all hit the database together. Remedies at concept level: one request recomputes while the others wait (a lock), refresh a little before expiry, add a small random amount to each TTL so entries do not all expire together, or warm the cache from a background job.

Your turn: 3. Design the key and the invalidation

GET /scores/countries/<country_name> returns the total of all players' scores for a country. (a) Where can you cache it? (b) Give a key. (c) When must it be invalidated?

Need a hint?

There is more than one layer. Think about what changes the answer.

Show the full solution

(a) On the backend (Redis) and also in the browser with a short Cache-Control: max-age. Both help. (b) score:country:<country_name>, for example score:country:India. (c) Whenever a score is added or changed for a player of that country: delete that key on write (or accept slightly stale data with a short TTL, such as 60 s).

Your turn: 4. Predict
@cache.cached(timeout=100)
def home():
    time.sleep(30)
    return "hello"

User 1 visits. User 2 visits 3 minutes later. What is the difference in their response times? Same question if user 2 visited after 50 seconds.

Need a hint?

Compare 180 s and 50 s with the timeout.

Show the full solution

After 180 s the entry expired (timeout 100), so user 2 also waits 30 s: difference about 0. After 50 s it is a hit: user 2 waits about 0, user 1 waited 30, difference 30 seconds.

In your project

Examiners ask "what do you cache and what happens when data changes?" Have a one-sentence answer for each cached item, for example: "dashboard summary, key user id, 5 minutes TTL, deleted when the user adds a task". Also demonstrate it: open dev tools or print a timestamp to show the second call is fast. A cache with no expiry and no invalidation will be questioned.

Idea 6: Front-end performance and pagination

Most of the delay a user sees is in the browser: many requests, big files, and heavy scripts.

TechniqueWhat it does
Lazy loading routes / componentsload a page's JavaScript only when the user visits it
Code splittingthe build tool breaks the app into chunks; the initial download is smaller
Minificationstrips spaces and shortens names in JS/CSS
Compression (gzip, brotli)the server sends the file compressed; the browser unpacks it
Image optimisationright size, modern format (WebP), loading="lazy" on images
Fewer requestsbundle small files, use a CDN for libraries
Debouncewait for a pause in typing before calling the API

Lazy-loaded route in Vue 2 + Vue Router 3 (this needs a build setup such as Vue CLI; it will not work from a CDN script tag):

const routes = [
  { path: "/", component: Home },
  { path: "/reports", component: () => import("./views/Reports.vue") }   // downloaded on first visit
];

Debounce, so typing "abc" in a search box makes one API call, not three. I ran this in Node:

function debounce(fn, ms) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), ms);
  };
}
const search = debounce((q) => console.log("search", q), 100);
search("a"); search("ab"); search("abc");   // quick typing
setTimeout(() => search("abcd"), 300);     // later

It printed search abc and then search abcd. The first three calls collapsed into the last one.

Pagination

Never send 50000 rows. Send a page at a time.

@app.get("/api/books")
def books():
    page = request.args.get("page", 1, type=int)
    p = db.paginate(select(Book).order_by(Book.id), page=page, per_page=20, error_out=False)
    return jsonify(items=[b.title for b in p.items], page=p.page, pages=p.pages, has_next=p.has_next)

With 6 books and per_page=4, page 2 holds 2 items, pages is 2, has_next is False (I ran it). Always add an order_by so pages are stable.

Async jobs for heavy work

Anything slower than a second or two (PDF, CSV export, sending mail to everyone) goes to a Celery task: the API replies at once with a task id, and the page polls the status or is told when it finishes (see week 10).

Solved example: 4. Before and after, a measurement exercise

Task. The endpoint /api/authors is slow. Improve it and prove it.

  1. Measure before: ab -n 200 -c 10 http://127.0.0.1:5000/api/authors. Write down requests per second and the 95 percent time. Suppose you get 12 requests/s, p95 = 900 ms.
  2. Find the cause: count queries (the N+1 listener from Idea 2). 1 + 50 queries per request: N+1.
  3. Fix 1: selectinload(Author.books). Measure again: for example 40 requests/s, p95 = 250 ms.
  4. Fix 2: add @cache.cached(timeout=60). Measure again: for example 600 requests/s, p95 = 20 ms.
  5. Check correctness: add an author, confirm the list updates (delete on write or wait 60 s).

Record a small table of before, after 1, after 2 in your report. (The numbers above are only an illustration; your machine gives its own.) One change at a time, so you know what helped.

Your turn: 5. Front-end advice

A Vue app loads one 3 MB bundle and a search box calls the API on every key press. List three fixes.

Need a hint?

Lazy loading, compression, debounce.

Show the full solution

(1) Lazy-load routes with () => import(...) so the first download is small. (2) Turn on minification and gzip (and optimise images). (3) Debounce the search input (for example 300 ms). Optional: paginate the search results.

Exam radar

Exam radar

The evidence file has about 42 distinct matching questions. Keyword counts: about 134 of 1471 End Term, 12 of 608 Quiz 2 and 6 of 848 Quiz 1 questions match (rough; several matches are really Vue Router questions from neighbouring weeks, and weeks 10 to 12 have few matched questions overall). Honest reading: this is an End Term topic, and the pattern is very stable.

What the real questions test:

  • Predict the time with Flask-Caching (about 10 questions). Code with @cache.cached(timeout=...) or @cache.memoize(timeout=...), a time.sleep(N), and two or three visits at given times. You choose "0 seconds", "N seconds" or "difference N". Also the time.time() version with request timestamps.
  • @cache.cached vs @cache.memoize: view vs function arguments; memoisation definition.
  • Caching in general (about 8): benefit (lower server load, lower latency, scalability), shared vs private cache, CDN, what to cache or not, hard refresh, "a browser cannot cache JavaScript" (false).
  • HTTP headers (about 3): Cache-Control, Expires, ETag, no-store; and the "which statement is false" with sessionStorage.
  • Lighthouse (about 3): FCP, TTI, Layout Shift count; "Database Query Time" is not one; it can emulate throttling and devices.
  • Page load factors: number of HTTP requests and file sizes.
  • Redis as a cache store; Celery for async work and CSV or reports (week 12 evidence).
Watch out - real exam trap
  • Compare the gap between requests with the timeout, not with the sleep time.
  • key_prefix makes different URLs share an entry.
  • Different arguments to a memoised function are different entries.
  • no-store prevents caching; no-cache means "revalidate".
  • sessionStorage does not survive closing the browser.
  • Caching does not add security.
  • A shared cache must not store personalised responses.

Quick quiz

Quick quiz

@cache.cached(timeout=100) wraps a view that sleeps 25 seconds. A client requests the same URL twice within 50 seconds. The approximate difference between the two latencies is...

  • 0 seconds
  • 25 seconds
  • 50 seconds
  • 100 seconds
The first request is a miss (25 s), the second is a hit (near 0 s), a difference of about 25 s.

A memoised function with time.sleep(5) is called for ids 1, 2, 1 within its timeout. Total time?

  • 5 seconds
  • 10 seconds
  • 15 seconds
  • 0 seconds
ids 1 and 2 are misses, the repeated 1 is a hit.

Select all TRUE statements. (select all that apply)

  • @cache.memoize builds the key from the function arguments.
  • A CDN is a group of servers that delivers content quickly.
  • sessionStorage data stays after the browser is closed and reopened.
  • A shared cache is suitable for personalised responses.
sessionStorage is cleared when the session ends; shared caches must not hold personal data.

Which is NOT a Lighthouse performance metric?

  • First Contentful Paint
  • Time to Interactive
  • Total Blocking Time
  • Database Query Time
Lighthouse measures what the browser experiences, not the database.

With @cache.cached(timeout=60, key_prefix='compute') on /compute/<x>/<y>, what do /compute/10/20 then /compute/5/10 return (multiplication)?

  • 200 then 50
  • 200 then 200
  • 50 then 200
  • 100 then 100
The fixed key means the second URL reuses the first stored result.

Which fixes an N+1 problem?

  • Adding @cache.cached to a Python print
  • Loading related rows with selectinload or a join
  • Using no-store
  • Increasing the timeout
N+1 is about many queries; load the related rows in one extra query.

Viva: say it out loud

Viva: say it out loud
How did you measure your app's performance?

I use the browser Network tab for request sizes and times, Lighthouse for the page, and Apache Bench or Locust for load. I look at latency percentiles and throughput, not only the average. I measured before and after each change, one change at a time.

What did you cache and how do you keep it fresh?

I cache expensive read endpoints, such as the dashboard summary, with Flask-Caching on Redis. The key includes the user id, the TTL is a few minutes, and I delete the key when that user writes data. So readers get fast answers and writers see their changes.

Difference between cache.cached and cache.memoize?

cached caches a whole view and by default uses the URL path as the key. memoize caches a function's return value, with the key built from the function and its arguments, so different arguments get different entries.

What is a cache stampede?

When a popular entry expires, many requests miss together and all hit the database at once. We reduce it with a lock so only one request recomputes, with random extra TTL, or by refreshing before expiry.

How do browser caching headers work?

Cache-Control: max-age lets the browser reuse a file without asking. ETag or Last-Modified let it ask "has this changed?" and the server answers 304 with no body if not. no-store stops caching of sensitive responses.

Name three things you did to make the front end faster.

I lazy-load the heavier routes, serve minified and compressed files, optimise images, debounce the search box, and paginate long lists so the browser never receives thousands of rows.

Why is a heavy task like CSV export done in Celery?

It can take tens of seconds and would block the request. The API starts a task and returns at once, and the page polls the task status, then downloads the file.

Recap

Remember this
  • Measure first: latency, throughput, p95/p99, TTFB. Tools: dev tools, Lighthouse, ab, Locust, profilers.
  • Common bottlenecks: N+1 queries, missing indexes, too much data, heavy work inside requests, big front-end files.
  • Cache layers: browser (private), CDN and proxies (shared), application (Redis), database.
  • @cache.cached = view, key by path (or key_prefix); @cache.memoize = function, key by arguments. TTL counts from the store time.
  • Keep caches fresh with TTL, delete on write or write-through; watch the hit ratio and stampedes.
  • Front end: lazy routes, code splitting, minify, gzip, images, debounce. Server: pagination, Celery for heavy jobs.
Code and facts to keep
  • ab -n 100 -c 10 URL (requests, concurrency)
  • Hit ratio = hits / (hits + misses); throughput = requests / seconds
  • Cache-Control: max-age=60, no-store, no-cache, public, private; ETag plus If-None-Match gives 304
  • Config: CACHE_TYPE="RedisCache", CACHE_REDIS_URL="redis://localhost:6379/1", CACHE_DEFAULT_TIMEOUT=300
  • @cache.cached(timeout=60); @cache.cached(timeout=60, key_prefix="k"); @cache.memoize(timeout=60)
  • cache.delete("key"), cache.delete_memoized(fn, *args), cache.clear()
  • N+1 fix: select(Author).options(selectinload(Author.books))
  • Index: db.Column(db.Integer, index=True)
  • Pagination: db.paginate(select(Book).order_by(Book.id), page=2, per_page=20)
  • Lazy route: component: () => import("./views/X.vue")
  • Next: week 12, the project; earlier: week 10
Connected weeks (learn them together)

Practise with real exam questions

These come straight from past papers (with the real figures and the official answer key). Open 60 real questions on this week   All End Term papers