MAD Study Hub

Home / MAD2 / Week 10

Week 10 · Messaging: webhooks, server-sent events and email

Official syllabus line: Inter-Service Messaging and Webhooks
About 90 minEnd Term material (see exam radar)BSCS2006 (+ project BSCS2006P) · Thejesh G N, Nitin Chandrachoodan
Before you start: MAD2 weeks 1 to 9 (Flask APIs, Vue basics, tokens), and a first look at Celery and Redis from the earlier weeks.

The one-minute story

Think of it like this

Three ways to hear about a parcel.

Polling is you walking to the gate every five minutes to ask "has my parcel come?". Most trips are wasted.

A webhook is you leaving your phone number with the courier: "call me the moment it arrives." One call, exactly when something happens. The courier (a server) calls your number (a server). Nobody walks anywhere.

Server-Sent Events (SSE) is a radio in your room tuned to the courier's channel. You switch it on once and the courier keeps announcing news. You can only listen; you cannot talk back.

A WebSocket is a phone call: both sides can talk at any time.

A message queue is the post office sorting room: a sender drops a letter in a box and goes away; a worker picks it up later. Email is the old, reliable letter that every app can send.

This week is about services telling each other things, and the app telling the user things without the user pressing refresh.

By the end of this week you can:

  • explain synchronous vs asynchronous messaging, and queue vs publish/subscribe;
  • write a webhook sender (Python requests) and a webhook receiver (Flask) that checks a signature;
  • write a Flask SSE endpoint and the browser EventSource that reads it, and choose between SSE, WebSockets, polling and long polling;
  • send an HTML email from Flask and test it with MailHog;
  • schedule a daily reminder and a monthly report with Celery beat, and export a PDF or CSV;
  • predict the answer to the typical "which statement about webhooks / SSE is true" question.

Words you need

Words you need
  • Synchronous - the caller waits for the answer before doing anything else.
  • Asynchronous - the caller sends the message and carries on; the answer (if any) comes later.
  • Message queue - a list where producers put messages and workers take them out, one worker per message (Celery with Redis or RabbitMQ).
  • Publish/subscribe (pub/sub) - a publisher sends to a named channel; every current subscriber of that channel gets a copy. The publisher does not know the subscribers.
  • Message broker - the server that holds queues or channels (Redis, RabbitMQ, Kafka).
  • Webhook - an HTTP POST that a server sends to a URL you registered, when an event happens. Also called a "reverse API" or HTTP callback.
  • Polling - the client asks again and again ("short polling") whether there is something new.
  • Long polling - the client asks once; the server holds the request open until there is news, then answers; the client asks again.
  • SSE (Server-Sent Events) - one long-lived HTTP GET over which the server keeps pushing text events to the browser. One direction only.
  • WebSocket - a persistent two-way (full-duplex) connection, started with an HTTP GET handshake and then upgraded.
  • SMTP - the protocol used to send (deliver) email. IMAP is used to read (retrieve) email.
  • MailHog - a fake local mail server: it catches every email your app sends so you can look at it in a web page. Nothing is really delivered.
  • HMAC signature - a hash made with a secret key, sent with a webhook so the receiver can check who sent it.
  • Idempotent - doing it twice has the same effect as doing it once.

Idea 1: Services talking to each other

A real application is rarely one program. In your project there is a Flask API, a Celery worker, Redis, maybe a mail server. They need to pass messages. There are two styles.

SynchronousAsynchronous
Callerwaits for the replysends and moves on
Examplerequests.get(url) in a view; a normal API calltask.delay(), a message put in a queue, a webhook
Failurethe caller sees the error at oncethe caller may not know; you need retries and logs
Good for"give me this data now"slow work, notifications, fan-out to many services

Queue vs publish/subscribe

The idea

Queue (point to point). A producer puts a message in a queue. Exactly one consumer takes it. Use it to share work among workers. Celery tasks are queue messages.

Publish/subscribe. A publisher sends to a channel. Every subscriber currently listening gets its own copy. Use it to broadcast an event ("order placed") to many services.

In both, the sender does not need to know who receives it, and they communicate asynchronously. A broker sits in the middle, which also makes the system easy to scale: add more consumers without touching the producer.

Brokers you may hear about: Redis (queues, lists and pub/sub, the one used in your course), RabbitMQ (a full message broker, a common Celery broker), Kafka (a durable log for very high volume). For the course you only need to know what they are for.

Redis pub/sub in a few lines

This needs a Redis server running (see the week 12 lesson for Windows tips). The redis package is pip install redis.

# subscriber.py (run first, in terminal 1)
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
p = r.pubsub()
p.subscribe("orders")
for msg in p.listen():
    if msg["type"] == "message":
        print("got:", msg["data"])
# publisher.py (terminal 2)
import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
n = r.publish("orders", "order 42 placed")
print("delivered to", n, "subscribers")

Terminal 1 prints got: order 42 placed. Terminal 2 prints delivered to 1 subscribers. If no one is subscribed, the number is 0 and the message is lost: Redis pub/sub does not store messages. (A Celery queue, in contrast, keeps the task until a worker takes it.)

Watch out - real exam trap
  • "In pub/sub the publisher must know all subscribers" is false. It is exactly the thing pub/sub removes.
  • "Pub/sub communication is synchronous" is false; it is asynchronous.
  • A message broker between services makes them loosely coupled and easier to scale compared with point-to-point calls.
  • A queue gives each message to one worker; pub/sub gives it to all subscribers.
Your turn: 1. Queue or pub/sub?

Choose queue or pub/sub: (a) 1000 report-generation jobs shared among 4 workers; (b) one "user signed up" event that must reach the email service, the analytics service and the CRM; (c) a video upload that must be processed once.

Need a hint?

Ask: should one receiver get it, or all receivers?

Show the full solution

(a) Queue (each job is handled once, by one worker). (b) Pub/sub (every subscriber needs a copy). (c) Queue (processed once; this is the typical Celery/RabbitMQ scenario).

Idea 2: Webhooks

The idea

A webhook is an HTTP POST that service A sends to a URL of service B when an event happens in A. B registered that URL earlier. The body (the payload) is usually JSON.

  • It is event-driven and server to server (machine to machine).
  • It uses the HTTP protocol, normally POST. It does not use WebSockets.
  • It is one way: A sends, B answers with a small status code (200 means "got it").
  • It replaces polling: B no longer has to keep asking A "anything new?".
  • Webhook receiver and sender do not have to be on the same origin (CORS is a browser rule; it does not apply between servers).

Webhook vs API call vs polling

API callPollingWebhook
Who starts itthe client, when it wantsthe client, again and againthe server, when an event happens
Wasted requestsnonemanynone
Delaynoneup to one intervalnear real time

"A webhook is triggered by an event, while an API call requires a client request" is the textbook difference. A webhook is not the same as short polling, and not the same as a WebSocket.

Sending a webhook with Python requests

Google Chat and Slack both give you an incoming webhook URL. You POST a small JSON message to it and the text appears in a chat room. The URL is a secret: anyone who has it can post. Keep it in an environment variable, never in Git.

import os
import requests

URL = os.environ["CHAT_WEBHOOK_URL"]   # looks like https://chat.googleapis.com/v1/spaces/.../messages?key=...

def notify(text):
    r = requests.post(URL, json={"text": text}, timeout=5)
    r.raise_for_status()               # turns 4xx/5xx into an exception
    return r.status_code

notify("Daily report is ready: 12 users inactive.")

json= sets the body to JSON and the header Content-Type: application/json. Google Chat accepts {"text": "..."}; Slack incoming webhooks also take {"text": "..."}. Always pass a timeout, otherwise a dead server can hang your worker forever.

Receiving a webhook in Flask, with a signature check

Anyone on the internet can POST to your receiver URL. How do you know it was really the sender? The sender and you share a secret. The sender computes an HMAC of the raw body with the secret and sends it in a header. You recompute and compare.

import hmac, hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET = b"s3cret-shared-key"          # in real code: os.environ

def sign(body: bytes) -> str:
    return hmac.new(SECRET, body, hashlib.sha256).hexdigest()

@app.post("/hooks/payment")
def payment_hook():
    body = request.get_data()                        # the RAW bytes
    got = request.headers.get("X-Signature", "")
    if not hmac.compare_digest(got, sign(body)):
        return jsonify(error="bad signature"), 401
    event = request.get_json()
    print("event:", event["type"])
    return jsonify(ok=True), 200                     # answer quickly

I ran this with Flask's test client: a body signed with the right secret gives 200, a body with no signature gives 401.

Why these details:

  • Sign the raw bytes (request.get_data()), not a re-dumped JSON, because spaces and key order change the hash.
  • hmac.compare_digest compares in constant time, so an attacker cannot learn the signature by timing.
  • Return 200 fast (within a few seconds). Do slow work in a Celery task: process_event.delay(event). Senders give up on slow receivers.
  • Use HTTPS, so the secret payload is not readable on the way.

Retries and idempotency

The idea

Networks fail, so a good sender retries when it does not get a 2xx answer (often with growing waits: 1 min, 5 min, 30 min). This means your receiver may get the same event twice. Webhooks therefore do not guarantee exactly-once or in-order delivery. Make your receiver idempotent: each event carries a unique id; store the ids you have handled and ignore a repeat.

processed = set()          # in real code: a database table or Redis set

def handle(event):
    if event["id"] in processed:
        return "duplicate, ignored"
    processed.add(event["id"])
    # ... do the work once ...
    return "done"

Calling handle({"id": "evt_1"}) twice returns done then duplicate, ignored.

Watch out - real exam trap
  • Webhooks do not guarantee ordering or exactly-once delivery (a real exam option, marked false). Retries are done by the sender and only if it was built to; it is not automatic.
  • A webhook is not a WebSocket and does not keep a connection open.
  • Webhooks are push (the source calls you); polling is pull. "A webhook pulls messages from an application" is false.
  • A webhook receiver is just an ordinary HTTP endpoint, so it needs authentication: signature, a secret token in the URL or header, HTTPS.
Your turn: 2. Which snippet is the receiver?

Snippet A calls requests.get(url) every 10 seconds in a loop. Snippet B defines @app.post("/hook") that reads request.json. Snippet C does r.publish("chan", msg). Snippet D opens EventSource. Which is a webhook receiver, which is pub/sub, which is polling?

Need a hint?

Receiver = an endpoint that waits for someone else's POST.

Show the full solution

B is the webhook receiver (a POST endpoint). C is pub/sub (publish to a channel). A is polling. D is SSE on the client.

In your project

Examiners like "how does the daily reminder reach the user?" If you post to a Google Chat webhook as an extra reminder channel, say: the URL is a secret kept in an environment variable; it is called from a Celery task, not from the request; and a failure is logged but does not break the page.

Idea 3: Server-Sent Events (SSE)

Polling asks again and again. A webhook goes server to server. But the browser is not a server, it cannot receive a POST. How does a page learn "your export is done" without reloading? One answer is SSE.

The idea

SSE keeps one HTTP connection open. The browser sends a normal GET; the server answers with Content-Type: text/event-stream and never finishes: it writes small text blocks whenever it has news.

  • One direction: server to client only.
  • Plain HTTP (no special protocol). Works through normal proxies.
  • The browser object is EventSource; you register an onmessage handler.
  • It reconnects automatically if the connection drops.
  • Message format: lines data: ..., ended by a blank line (\n\n).

The wire format

data: Order placed

data: Preparing

event: done
data: {"status": "delivered"}

Each event ends with an empty line. A line data: x fires onmessage. A line event: done gives it a name, and the browser must listen with addEventListener("done", ...). An optional id: 7 line lets the browser tell the server where it stopped (the Last-Event-ID header on reconnect). A line retry: 3000 sets the reconnect delay in milliseconds.

Flask streaming generator

import time
from flask import Flask, Response

app = Flask(__name__)

def event_stream():
    for i in range(5):
        yield f"data: Message {i}\n\n"     # note the blank line
        time.sleep(1)

@app.get("/events")
def events():
    return Response(event_stream(), mimetype="text/event-stream")

A view that returns a generator inside a Response streams each yield to the client as it is produced. I checked that the response header is text/event-stream and the body is the five data: blocks.

The browser side (plain JavaScript and Vue 2)

const es = new EventSource("http://localhost:5000/events");
es.onmessage = (event) => console.log(event.data);   // "Message 0", "Message 1", ...
es.onerror = () => console.log("lost connection, the browser will retry");
// es.close();   // stop listening
// inside a Vue 2 component
export default {
  data() { return { messages: [] }; },
  mounted() {
    this.es = new EventSource("http://localhost:5000/events");
    this.es.onmessage = (e) => { this.messages.push(e.data); };
  },
  beforeDestroy() { if (this.es) this.es.close(); }
};

event.data is always a string: send JSON text and use JSON.parse(event.data).

Solved example: 1. Predict the page

Question (the shape of a real End Term question). The Flask code above runs on port 5000 and the Vue page opens an EventSource to it. The server yields 5 messages with a one second sleep. What does the user see?

Solution. The browser sends a GET, the server keeps the connection open and flushes one message per second. The page receives 5 messages, one per second (not all at once, and the server does not crash: Flask can stream). If the page is served from a different origin, the server also needs CORS headers (see week 12), but the question's answer is "5 messages, one per second". After the generator ends the connection closes, and EventSource will reconnect and start the stream again unless you call close().

A tiny practical note: the real exam code wrote yield f"data: Message {i}" without the two newlines. A real browser needs the blank line to fire onmessage, so write \n\n in your own code.

SSE with many clients and Redis

A generator like the one above is tied to one process. If a worker or another request must push an event to many connected browsers, they need a shared channel. The Flask-SSE extension does this: you call sse.publish({"msg": "low stock"}, type="alert") anywhere, and Redis acts as the shared channel (pub/sub) that carries the message to every web process holding open connections. Redis here is a message channel, not a login checker, and not mandatory storage of old events. Flask-SSE needs a streaming server (gunicorn with gevent), not the plain development server with many clients.

SSE vs WebSockets vs polling vs long polling

Short pollingLong pollingSSEWebSocket
ProtocolHTTPHTTPHTTPstarts as HTTP GET, then upgrades
Directionclient asksclient asksserver to clientboth ways (full duplex)
Connectionsa new request each timeone request held open, then repeatedone long connectionone long connection
Auto reconnectyou write ityou write itbuilt inyou write it
Best forrare, simple checksolder push emulationlive feeds, progress bars, notificationschat, games, collaboration
  • Long polling: the client makes a single request and the server holds it open until new data exists, then answers; the client immediately asks again. (Not "repeated requests at fixed intervals": that is short polling.)
  • When people just say "polling" they mean short polling, over HTTP.
  • SSE is not an extension of WebSockets and does not need service workers or HTTP/2. It cannot send data from the client, but the client can use a normal fetch POST for that.
  • SSE and the WebSocket handshake both start with an HTTP GET.
  • A food-delivery tracker (server pushes "Preparing", "Out for delivery") is a textbook SSE case: one direction is enough.
Your turn: 3. Choose the technology

Pick polling, long polling, SSE, WebSocket or webhook: (a) a live cricket score on a page, server pushes only; (b) a two-player online game; (c) Razorpay-style "payment done" call to your server; (d) a page that checks every minute if a Celery export finished, simplest code.

Need a hint?

Ask: who talks, and in which direction?

Show the full solution

(a) SSE. (b) WebSocket (both directions, low delay). (c) Webhook (server to server). (d) Short polling with setInterval and fetch is the simplest code.

Watch out - real exam trap
  • SSE is one-way (server to client); WebSockets are two-way. Options that swap them are false.
  • SSE uses GET, webhooks use POST.
  • SSE is based on HTTP; "SSE switches to WebSocket automatically" is false.
  • The browser does reconnect automatically in SSE. An answer that says "SSE does not reconnect" is false.
  • SSE suits sending updates to many clients at once.

Idea 4: Sending email from Flask

Email is the oldest "message to a human". Two protocols: SMTP sends (delivers) mail from your app to a mail server; IMAP (and POP3) lets a mail program read it. Mixing these up is a classic exam trick: "SMTP retrieves email" is false.

MailHog for local testing

Never test with real addresses. MailHog is a fake mail server: it listens for SMTP on port 1025 and shows every message in a web page on port 8025 (http://localhost:8025). Download its single executable, run it, and point Flask-Mail at it. Nothing is delivered to the outside world.

Flask-Mail

pip install flask-mail
from flask import Flask
from flask_mail import Mail, Message

app = Flask(__name__)
app.config.update(
    MAIL_SERVER="localhost",
    MAIL_PORT=1025,            # MailHog's SMTP port
    MAIL_USE_TLS=False,
    MAIL_USE_SSL=False,
    MAIL_DEFAULT_SENDER="noreply@quizmaster.local",
)
mail = Mail(app)

def send_reminder(to, name):
    msg = Message(
        subject="Your daily reminder",
        recipients=[to],
        body=f"Hi {name}, you have not visited today.",              # plain-text part
        html=f"<h2>Hi {name}</h2><p>You have <b>not</b> visited today.</p>",  # HTML part
    )
    mail.send(msg)

with app.app_context():
    send_reminder("student@example.com", "Asha")

With MailHog running, the message appears in its inbox page. body is the plain text version, html the rich one; mail programs show the HTML if they can. Flask-Mail needs an application context (inside a view, or with app.app_context() in a script or Celery task). For real Gmail you would use smtp.gmail.com, port 587, MAIL_USE_TLS=True, and an app password kept in an environment variable (never in code). The standard library smtplib does the same job with more lines.

Rendering the HTML body from a Jinja template keeps the code clean:

from flask import render_template
html = render_template("email/reminder.html", name="Asha", pending=3)

Daily reminders and monthly reports with Celery beat

Sending 500 mails inside a request would freeze the page, so email goes in a Celery task, and Celery beat (the scheduler) starts it at fixed times.

from celery import Celery
from celery.schedules import crontab

celery = Celery("app", broker="redis://localhost:6379/0", backend="redis://localhost:6379/1")

@celery.task
def daily_reminder():
    # in a real app: query users who have not visited, then mail each
    return "reminders sent"

@celery.task
def monthly_report():
    return "reports sent"

celery.conf.beat_schedule = {
    "daily-6pm": {"task": "app.daily_reminder", "schedule": crontab(hour=18, minute=0)},
    "monthly-1st": {"task": "app.monthly_report",
                    "schedule": crontab(day_of_month=1, hour=8, minute=0)},
}
celery.conf.timezone = "Asia/Kolkata"

Run three processes: Redis, a worker (celery -A app.celery worker -l info; on Windows add -P solo) and beat (celery -A app.celery beat -l info). The task name defaults to module.function, here app.daily_reminder because the file is app.py. A crontab(hour=18, minute=0) means "every day at 18:00".

PDF reports and CSV exports

A monthly report is usually HTML turned into a PDF. The pipeline: data, then Jinja2 template to HTML, then a library renders the PDF, then the PDF is attached to the email.

from flask import render_template
from weasyprint import HTML        # pip install weasyprint (pdfkit needs the wkhtmltopdf program instead)

def make_pdf(user):
    html = render_template("report.html", user=user, stats=compute_stats(user))
    HTML(string=html).write_pdf(f"reports/{user.id}.pdf")

Attach it with msg.attach("report.pdf", "application/pdf", pdf_bytes).

A CSV export needs no extra library:

import csv, io
from flask import Response

@app.get("/export.csv")
def export_csv():
    out = io.StringIO()
    w = csv.writer(out)
    w.writerow(["id", "title", "score"])
    w.writerow([1, "Quiz 1", 8])
    return Response(out.getvalue(), mimetype="text/csv",
                    headers={"Content-Disposition": "attachment; filename=export.csv"})

Content-Disposition: attachment makes the browser download the file. For big exports, run it as a Celery task (it can take 20 to 40 seconds), then tell the page when it is done. The page should poll the task state every few seconds, or listen via SSE; guessing "wait 41 seconds with setTimeout" is wrong because the time varies.

Solved example: 2. Daily reminder end to end

Question. Describe, in order, what happens at 18:00 when the daily reminder fires.

Solution.

  1. Celery beat sees the schedule entry and puts a daily_reminder message into the Redis queue.
  2. A Celery worker takes it, runs the task, queries the database for users who did not visit.
  3. For each user the task renders reminder.html and sends it with Flask-Mail through SMTP.
  4. In development MailHog catches the mails; you open localhost:8025 and see them.
  5. If the worker is down the message waits in Redis, and runs when the worker returns.
Your turn: 4. Fix the mail code

A student's view does for u in User.query.all(): mail.send(make_msg(u)) and the page takes 40 seconds with 1000 users. Fix it.

Need a hint?

Move the slow work out of the request.

Show the full solution

Create a Celery task send_all_reminders that loops over users and sends; in the view call send_all_reminders.delay() and return 202 or a message "queued" at once. (For parallel speed you could also create one task per user, send_one.delay(u.id); pass the id, not the ORM object, because tasks need JSON-friendly arguments.)

Your turn: 5. SMTP or IMAP

True or false: (a) SMTP delivers mail; (b) IMAP is used to send mail; (c) MailHog sends your mail to Gmail; (d) Flask-Mail html= carries the rich version.

Need a hint?

Send vs read.

Show the full solution

(a) True. (b) False: IMAP retrieves. (c) False: MailHog only catches mail locally. (d) True.

In your project

Examiners often ask "show me the daily reminder". Have MailHog open in a tab, trigger the Celery task by hand once (daily_reminder.delay()) and show the mail arriving, then show the beat schedule entry. Say which user gets reminded and why (for example "no visit today").

Idea 5: Security and design of messaging endpoints

The idea

Every endpoint that accepts messages from outside is a door. Lock it.

  1. HTTPS only.
  2. Verify the sender: HMAC signature header, or a shared token, or an allow-list of IPs.
  3. Validate the payload: expected fields and types; never trust it.
  4. Answer fast, process later in Celery.
  5. Idempotent handling, with event ids.
  6. Secrets (webhook URLs, SMTP password, signing key) in environment variables, not in Git or the frontend.
  7. Limit what an event may do; never run SQL built from payload text (SQL injection).

Webhook secrets and SMTP passwords must never reach the browser. This week's exam also showed a login example mixing three mistakes: building SQL by string formatting (SQL injection), storing and comparing plain-text passwords, and returning no token. The right answer there was "all of these". Use parameterised queries, store password hashes, and return a token (see the earlier token weeks).

Your turn: 6. Weak receiver

A receiver does data = request.json; db.execute(f"UPDATE orders SET s='{data['s']}' WHERE id={data['id']}") and has no signature check. List three problems and the fixes.

Need a hint?

One problem is about trust, one about SQL, one about retries.

Show the full solution

(1) Anyone can call it: add an HMAC or token check. (2) SQL injection: use bound parameters, UPDATE orders SET s=:s WHERE id=:id. (3) A retried event applies twice: store the event id and skip repeats (here a status update is naturally idempotent, but increments are not). Also validate that s is one of the allowed statuses.

Exam radar

Exam radar

This week's evidence file has about 37 distinct matching questions (keyword matching is rough and some are about Vue or JavaScript from neighbouring weeks). Counts by paper: roughly 104 of 1471 End Term questions, 4 of 608 Quiz 2 and 5 of 848 Quiz 1 match this week's keywords, so it is mainly an End Term topic and rare in the quizzes. Weeks 10 to 12 have few matched questions, so treat the numbers as a guide only.

What the real questions test, in order of frequency:

  • Webhooks, about 13 questions: HTTP POST, event driven, server to server, not polling, not WebSocket, no same-origin rule, not guaranteed in order or exactly once, not automatically retried.
  • SSE and WebSockets, about 10: one way vs two way, HTTP GET, onmessage on EventSource, automatic reconnect, "what happens when the user opens this Flask SSE page", Flask-SSE with Redis as the shared channel.
  • Polling and long polling, about 4: long polling holds the connection until data exists.
  • Pub/sub and queues, about 4: publisher need not know subscribers, asynchronous, Celery/RabbitMQ for video processing, "match technologies to uses".
  • Email, about 3: SMTP sends, IMAP reads.
  • Celery CSV export, about 2: use polling on the task state, not a guessed delay. One question compared one Celery task per user with one task looping over users (answers were disputed in the key; know both sides: comparable with enough workers, a single big task serialises).

A few Vue and JavaScript questions (this in arrow functions, filter with endsWith, localStorage after submit) landed in this file only through the word "email"; they belong to the Vue weeks.

Watch out - real exam trap
  • Picking the option that swaps "one way" and "two way" for SSE and WebSockets.
  • Thinking webhooks need a polling client or keep a connection open.
  • Saying SMTP retrieves mail.
  • Believing webhooks arrive exactly once and in order.
  • Forgetting that SSE and webhooks are both plain HTTP, and that SSE uses GET but a webhook uses POST.

Quick quiz

Quick quiz

Select all TRUE statements about webhooks. (select all that apply)

  • They are typically sent as HTTP POST requests.
  • They are mainly for server-to-server communication.
  • They require the receiver to poll the sender.
  • They guarantee ordered, exactly-once delivery.
A webhook is an event-triggered POST. Retries can cause duplicates, and ordering is not guaranteed.

Which statement about long polling is true?

  • The client sends a request every few seconds regardless of news.
  • The client sends one request and the server holds it until data is available.
  • It uses a WebSocket.
  • It lets the server send many events over one request and the client never asks again.
That is the definition. The first option describes short polling.

A live order tracker only needs the server to push status changes to the customer. Which choice is best?

  • WebSockets, because SSE cannot send JSON.
  • Polling every 100 ms.
  • Server-Sent Events.
  • A webhook to the customer's browser.
SSE is one-way, simple and auto-reconnects. A browser cannot receive a webhook.

A Flask route returns Response(gen(), mimetype="text/event-stream") where gen yields data: hi\n\n five times with a one second sleep. A page opens new EventSource(url). What happens?

  • All five messages appear instantly.
  • The page receives five messages, one per second.
  • The server crashes.
  • Nothing, because EventSource cannot use GET.
Each yield is flushed as it is produced; EventSource uses GET.

Select all FALSE statements. (select all that apply)

  • SMTP is used to retrieve email from a server.
  • In pub/sub the publisher must know every subscriber.
  • Pub/sub messaging is asynchronous.
  • A webhook normally uses HTTP.
SMTP sends; IMAP retrieves. Pub/sub hides subscribers from the publisher.

In a Flask-SSE setup with several web processes, Redis is used as...

  • the login checker for connected users.
  • a shared channel through which published events reach connected clients.
  • permanent storage that replays every old event.
  • a CSS cache.
Flask-SSE publishes events through Redis pub/sub.

Viva: say it out loud

Viva: say it out loud
What is a webhook and how is it different from polling?

A webhook is an HTTP POST that one server sends to a URL registered by another server when an event happens. With polling the receiver keeps asking "anything new?" and most requests are wasted. A webhook is push and event driven, so it is faster and cheaper. It is meant for server to server calls, not for the browser.

How would you make sure a webhook request really came from the sender?

We share a secret key. The sender sends an HMAC of the raw body in a header. My endpoint recomputes it with hmac.compare_digest and returns 401 if it differs. I also use HTTPS and answer quickly, doing the real work in a Celery task.

What happens if a webhook is delivered twice?

Senders retry on failures, so duplicates are normal. Each event has an id. I store the processed ids and ignore repeats, so the handler is idempotent.

When would you use SSE and when WebSockets?

SSE when only the server pushes, such as progress, notifications or live status. It is plain HTTP GET with text/event-stream, and the browser reconnects automatically. WebSockets when both sides talk a lot, such as chat or games.

How does your project send the daily reminder and monthly report?

Celery beat holds the schedule and puts a task in Redis. A worker runs it, renders an HTML email from a Jinja template and sends it through Flask-Mail. In development MailHog catches the mails. The monthly report can attach a PDF made from the HTML.

Why not send the emails inside the request?

Sending many mails is slow and would freeze the page. A background task returns the page at once, can retry on failure, and survives a web restart because the task waits in the queue.

Difference between a queue and pub/sub?

In a queue one worker takes each message, used to share work. In pub/sub every subscriber gets a copy, used to broadcast events. Both are asynchronous and the sender does not know the receivers.

Recap

Remember this
  • Synchronous waits; asynchronous does not. Queues share work; pub/sub broadcasts.
  • Webhook = event-triggered HTTP POST, server to server, push not pull. Verify with an HMAC, answer fast, be idempotent, expect retries and duplicates.
  • SSE = one HTTP GET kept open, text/event-stream, one way, EventSource with onmessage, auto reconnect. WebSocket = two way. Long polling = request held open until data.
  • SMTP sends, IMAP reads. Flask-Mail plus MailHog (SMTP 1025, web 8025) for local tests; HTML part with html=.
  • Celery beat schedules the daily reminder and the monthly report; the worker does the work.
  • PDF = Jinja2 to HTML to WeasyPrint/pdfkit; CSV = csv module plus Content-Disposition.
Code and facts to keep
  • Webhook send: requests.post(URL, json={"text": "hi"}, timeout=5)
  • Signature: hmac.new(SECRET, body, hashlib.sha256).hexdigest() and hmac.compare_digest(a, b)
  • SSE response: Response(gen(), mimetype="text/event-stream"); frame = "data: text\n\n"
  • Browser: new EventSource(url), es.onmessage = e => ..., es.close()
  • Redis pub/sub: r.publish("ch", msg), p = r.pubsub(); p.subscribe("ch")
  • Flask-Mail: Message(subject, recipients=[...], body=..., html=...), mail.send(msg)
  • MailHog: SMTP localhost:1025, inbox http://localhost:8025
  • Beat: crontab(hour=18, minute=0); monthly crontab(day_of_month=1, hour=8, minute=0)
  • CSV download header: Content-Disposition: attachment; filename=export.csv
  • Next: week 11, performance; week 12, the project; viva: MAD2 viva

Practise with real exam questions

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