2026-06-22 08:02:49
Django's ORM is one of its greatest strengths. It abstracts away raw SQL, lets you express database operations in clean Python, and gets you productive fast. But that convenience comes with a hidden cost: if you're not deliberate about how you fetch related objects, you'll silently generate far more queries than you intend — and you won't notice until your app slows to a crawl in production.
The most common culprit is the N+1 query problem: a pattern where fetching a list of N objects triggers an additional query for each one, resulting in N+1 total round-trips to the database. At ten rows it's invisible. At ten thousand rows, it's a disaster.
Django provides two tools to fix this: select_related and prefetch_related. This article explains how each one works internally, when to use which, and how to combine them effectively — with before/after examples and real query counts throughout.
Consider a simple blog with posts and authors. You want to render a list of posts, showing each post's title and its author's name.
Models:
# models.py
from django.db import models
class Author(models.Model):
name: str = models.CharField(max_length=100)
class Post(models.Model):
title: "str = models.CharField(max_length=200)"
author: Author = models.ForeignKey(
Author,
on_delete=models.CASCADE,
related_name="posts",
)
The naive approach:
# views.py
from django.db import connection
from .models import Post
def list_posts() -> None:
posts = Post.objects.all() # Query 1: fetch all posts
for post in posts:
print(f"{post.title} by {post.author.name}")
# ^^^ Query 2, 3, 4, ... N+1: one per post
For 100 posts, this produces 101 queries. Django lazily fetches post.author the first time you access it on each object. Each access hits the database separately.
You can verify this with django.db.connection.queries (requires DEBUG = True):
from django.db import connection, reset_queries
reset_queries()
posts = Post.objects.all()
for post in posts:
_ = post.author.name
print(len(connection.queries)) # 101
In development, django-debug-toolbar gives you a visual breakdown of every query and its duration — highly recommended for catching this early.
select_related — For ForeignKey and OneToOne
select_related performs a SQL JOIN and retrieves the related object's data in a single query. When you access post.author, Django reads from the already-fetched data in memory — no additional database hit.
-- What Django generates under the hood:
SELECT post.id, post.title, post.author_id,
author.id, author.name
FROM blog_post post
INNER JOIN blog_author author ON (post.author_id = author.id);
Use select_related for:
ForeignKey relationshipsOneToOneField relationshipsThese are the cases where the related object lives in a separate table and is reachable via a direct JOIN — exactly what select_related is built for.
Before (101 queries for 100 posts):
def list_posts_naive() -> None:
posts = Post.objects.all()
for post in posts:
print(f"{post.title} by {post.author.name}")
After (1 query):
def list_posts_optimized() -> None:
posts = Post.objects.select_related("author")
for post in posts:
print(f"{post.title} by {post.author.name}")
You can also traverse multiple levels of ForeignKey in one call:
# Fetches post → author → country in a single JOIN
posts = Post.objects.select_related("author__country")
select_related does not work for ManyToManyField or reverse ForeignKey (one-to-many) relationships. A JOIN on those would multiply rows rather than resolve them cleanly. For those cases, use prefetch_related.
prefetch_related — For ManyToMany and Reverse FK
prefetch_related takes a different approach: it runs separate queries for each relationship and then merges the results in Python. For a queryset of posts with tags (ManyToMany), Django will:
WHERE id IN (...) query — 1 query.Total: 2 queries, regardless of how many posts or tags you have.
Use prefetch_related for:
ManyToManyField relationshipsForeignKey lookups (e.g., fetching all comments for each post)Models:
class Tag(models.Model):
name: str = models.CharField(max_length=50)
class Post(models.Model):
title: str = models.CharField(max_length=200)
author: Author = models.ForeignKey(Author, on_delete=models.CASCADE)
tags: models.ManyToManyField = models.ManyToManyField(Tag, related_name="posts")
Before (1 + N queries):
def list_posts_with_tags_naive() -> None:
posts = Post.objects.all()
for post in posts:
tag_names = [tag.name for tag in post.tags.all()]
print(f"{post.title}: {', '.join(tag_names)}")
After (2 queries):
def list_posts_with_tags_optimized() -> None:
posts = Post.objects.prefetch_related("tags")
for post in posts:
tag_names = [tag.name for tag in post.tags.all()]
print(f"{post.title}: {', '.join(tag_names)}")
Prefetch object
For finer control — filtering, ordering, or annotating the prefetched queryset — use the Prefetch class:
from django.db.models import Prefetch
from .models import Post, Tag
def list_posts_active_tags() -> None:
active_tags = Tag.objects.filter(active=True).order_by("name")
posts = Post.objects.prefetch_related(
Prefetch("tags", queryset=active_tags, to_attr="active_tags")
)
for post in posts:
# post.active_tags is a plain list, not a queryset
tag_names = [tag.name for tag in post.active_tags]
print(f"{post.title}: {', '.join(tag_names)}")
to_attr stores the prefetched result as a Python list on the object, which is slightly faster to access than a queryset and makes the code intent explicit.
Real-world models rarely have just one relationship. Here's a more realistic example — a Post that has an Author (ForeignKey) and Tags (ManyToMany):
from django.db.models import Prefetch
from .models import Post, Tag
def list_posts_full() -> None:
posts = (
Post.objects
.select_related("author") # JOIN for ForeignKey
.prefetch_related( # Separate query for ManyToMany
Prefetch(
"tags",
queryset=Tag.objects.only("name"),
to_attr="tag_list",
)
)
)
for post in posts:
tag_names = [tag.name for tag in post.tag_list]
print(f"{post.title} by {post.author.name} — tags: {', '.join(tag_names)}")
Total queries: 2 — one for posts JOIN author, one for all tags. This stays flat no matter how many posts are in the queryset.
You can chain as many select_related and prefetch_related calls as needed. Django deduplicates and optimizes them before sending anything to the database.
select_related on ManyToMany
select_related silently ignores relationships it can't JOIN cleanly. You won't get an error — you'll just get the N+1 behavior back, with no warning.
# ❌ Does nothing for ManyToMany — falls back to per-object queries
posts = Post.objects.select_related("tags")
# ✅ Correct
posts = Post.objects.prefetch_related("tags")
Prefetching caches the queryset result. If you apply a filter after the fact on a prefetched relation, Django bypasses the cache and hits the database again:
posts = Post.objects.prefetch_related("tags")
for post in posts:
# ❌ New query per post — cache is bypassed
active = post.tags.filter(active=True)
# ✅ Filter inside the Prefetch object instead
The fix is to push the filter into a Prefetch object as shown in section 4.
select_related
A wide JOIN can pull in more data than you need. If you only use post.author.name, fetching the entire Author row (with bio, avatar URL, created_at, etc.) wastes bandwidth. Combine with only() to limit columns:
posts = Post.objects.select_related("author").only(
"title", "author__name"
)
The choice between select_related and prefetch_related comes down to the relationship type:
| Relationship | Tool |
|---|---|
ForeignKey / OneToOneField
|
select_related (SQL JOIN) |
ManyToManyField / Reverse FK |
prefetch_related (separate queries + Python merge) |
| Both in the same queryset | Chain them together |
Start by identifying your hot querysets — the ones called on list pages or inside loops. Add django-debug-toolbar to your dev environment and look for duplicate queries. Then apply select_related or prefetch_related as appropriate and watch your query count drop.
Once you've mastered these two tools, the natural next steps are only() and defer() to control which columns are fetched, and annotate() with aggregates to push computation into the database instead of Python. Those techniques, combined with what you've learned here, cover the vast majority of Django ORM performance problems you'll encounter in production.
2026-06-22 08:01:34
The scary part of an agent-driven container escape is not the container escape.
That sounds wrong, so let me be precise.
The primitives in Sysdig's latest threat research are not new magic. A mounted Docker socket has been a bad idea for years. Over-permissioned Kubernetes service accounts have been a bad idea for years. Privileged containers are dangerous. Host namespace tricks are dangerous. Secrets reachable from application pods are dangerous.
None of this should surprise anyone who has had to review production Kubernetes setups with a straight face.
The new part is the operator.
Sysdig observed what it describes as an LLM-harness-driven attacker exploiting a vulnerable marimo notebook, enumerating the container and host environment, using the Docker socket as an escape path, creating privileged containers, reading host credentials, and replaying a Kubernetes service-account token to dump Secrets.
That is the part worth sitting with.
Not because the agent invented a new class of exploit.
Because it made the old mistakes compose faster.
Most security incidents are not movie plots. They are boring edges left open long enough for someone to connect them.
In this case, the edges are familiar.
An internet-reachable application had a vulnerability. The workload had access to a Docker socket. The container environment exposed enough information to enumerate possible escape paths. A Kubernetes service-account token was available. The token had enough RBAC to read Secrets. Secrets contained useful downstream credentials.
That is not one bug.
That is a chain of assumptions.
The application team may have thought about the notebook vulnerability. The platform team may have thought about the Docker socket as a convenience for one workflow. The Kubernetes team may have thought the service account was scoped "only" to a namespace. The security team may have had runtime alerts somewhere in the backlog.
Each decision can look locally tolerable.
Together, they become a runway.
This is why I dislike treating container security as a checklist of isolated hardening tips. "Do not mount the Docker socket" is correct, but it is not the whole lesson. The real lesson is that orchestration-plane permissions are relationships. A small application compromise becomes much worse when it can talk to the host runtime, read cluster credentials, or discover secrets without friction.
Agents are very good at exploring relationships.
Human attackers can do all of this too.
That is important. We should not pretend an LLM suddenly made Docker socket exposure dangerous. It was already dangerous.
But speed and persistence change the operational shape of the risk.
A human attacker has to decide what to try next, type commands, inspect output, adjust, and keep enough state in their head to avoid wasting time. A scripted attacker can automate known paths, but tends to be brittle when the environment differs from the expected shape.
An agent sits in the uncomfortable middle.
It can run broad enumeration. It can parse output. It can test a delivery mechanism before using it. It can use section markers so the next step can slice command output cleanly. It can try one escape path, observe the result, and choose another. It can move from "am I in a container?" to "is the Docker socket mounted?" to "can I create a privileged container?" to "is there a Kubernetes token?" without needing a human to babysit every branch.
That does not make it brilliant.
It makes it tireless.
And for a lot of cloud-native security failures, tireless is enough.
The old defensive comfort was that messy environments slow attackers down. The host is weird. The image is minimal. The service account is named badly. The runtime differs from the blog post. The network path is awkward. There are three partial clues and one misleading error.
Agents reduce the value of that accidental friction.
They are not guaranteed to succeed, but they can afford to ask more questions.
The Docker socket is one of those infrastructure shortcuts that keeps surviving because it is useful.
You want a container to build images. You want a CI job to start sibling containers. You want a local development tool to manage services. You mount /var/run/docker.sock and everything works.
It works because the container can now ask the host daemon to do things.
That is also why it is dangerous.
If a workload can talk to the host Docker daemon, it may be able to create a privileged container, mount the host filesystem, share host namespaces, and read things it was never supposed to see. The application did not need root on the host. It needed access to something that could ask for root on the host.
That distinction matters for agent security.
We spend a lot of time asking what the compromised process can do. We need to spend at least as much time asking what control planes it can reach.
Can it reach the container runtime?
Can it reach the Kubernetes API?
Can it reach cloud metadata?
Can it reach CI credentials?
Can it reach a deployment tool?
Can it read a token that can reach any of those things?
For a human attacker, every reachable control plane is an opportunity. For an agentic attacker, it is also a menu.
The Kubernetes part is just as important as the host escape.
It is easy to treat service-account tokens as boring cluster plumbing. They are mounted automatically in many workloads. They sit in a predictable path. They are not as emotionally visible as an AWS access key pasted into an environment variable.
But if a compromised pod can read a service-account token, and that token can list or get Secrets, then the application compromise is no longer just an application compromise.
It is a credential disclosure event.
Maybe namespace-wide. Maybe cluster-wide. Maybe enough to get database passwords, API keys, webhooks, SSH keys, or cloud credentials. The exact blast radius depends on RBAC and on what teams put into Secrets.
This is where the boring Kubernetes defaults become security architecture.
Does the workload need a service account at all?
Does it need the token mounted?
Can it read Secrets, or only the one thing it actually needs?
Are Secrets being used as a junk drawer for every credential a team did not know where else to put?
Are tokens short-lived and bound, or are they effectively durable keys lying around inside every pod?
These questions are not glamorous. They are the difference between "attacker got code execution in one workload" and "attacker collected the keys to half the environment."
Static posture still matters.
You should know which workloads mount the Docker socket. You should know which pods run privileged. You should know which service accounts can read Secrets. You should know which containers have broad capabilities, weak seccomp profiles, or writable host paths.
But posture is only the start.
The Sysdig report is interesting because the behavior is visible if you are looking in the right place. Runtime enumeration. Docker API calls over a Unix socket. Privileged container creation. Host filesystem bind mounts. Namespace entry. Reads of service-account tokens. Kubernetes API calls from workloads that normally should not make them. Sudden Secret listing.
That is not a generic "AI attack" signal.
It is cloud-native runtime behavior.
The defensive answer is not to buy a product with "agentic" in the headline and call it a strategy. The answer is to make sure the boring signals are actually collected, retained, and connected to ownership.
When a workload creates a privileged sibling container, someone should know.
When an application pod reads a service-account token and immediately lists Secrets, someone should know.
When a namespace suddenly emits API calls that look like discovery rather than normal application behavior, someone should know.
The first alert does not need to say "LLM harness detected."
It can say "this workload is behaving like an operator is using it as a control-plane pivot."
That is already useful.
If I were responsible for a Kubernetes platform this week, I would not start with a new AI threat model document.
I would start with inventory.
Find every workload that mounts /var/run/docker.sock. Then justify each one as if it were host root, because in practice that is often what it means.
Find every privileged container and every hostPath mount. Separate the few that are legitimate infrastructure components from the ones that exist because a workaround became permanent.
List service accounts that can read Secrets. Then ask whether the application using that identity actually needs that permission at runtime.
Disable automatic service-account token mounting where it is not needed. Make that the default for application namespaces, not an exception that requires every team to remember.
Look at Secrets as blast-radius objects, not just configuration blobs. If one workload's token can read a Secret, assume a compromise of that workload can reveal it.
Add runtime detections for Docker socket use, privileged container creation, namespace entry, host filesystem mounts, and unusual Kubernetes API calls from application pods.
None of this is new.
That is the point.
The agent-driven part does not remove the old work. It makes the old neglected work more urgent.
Container escape is becoming an agent workload.
Not because agents discovered containers.
Because agents are good at chaining the little pieces of access we leave lying around: runtime sockets, mounted tokens, permissive RBAC, host paths, weak profiles, reachable metadata, and secrets with too much value packed into them.
The lesson is not "AI attackers are magic."
The lesson is worse and more practical: an autonomous harness can turn yesterday's platform shortcuts into today's fast escalation path.
So the defensive bar should move accordingly.
Treat Docker socket access like host root. Treat service-account tokens like production credentials. Treat Kubernetes Secret permissions like a blast-radius boundary. Treat runtime behavior as evidence, not noise. And stop assuming that a weird, messy environment will slow an attacker down enough for comfort.
The boring controls were already right.
Agents just made them harder to postpone.
To test my projects, I use Railway. If you want $20 USD to get started, use this link.
2026-06-22 08:00:58
When we first built RAG at Twio, pgvector was the obvious pick. Our business data was already in PostgreSQL, and dropping embeddings into the same database was the fastest path to a working product.
For the first version, that was right. As we scaled, the problem stopped being "how do we store vectors?" and became "how do we reliably understand thousands of broker documents, emails, and attachments in production?" That changed the answer. Today, Vertex AI Search is our main retrieval layer.
Twio is an AI SaaS for loan brokers. A single client case is a mess of fragmented information:
The AI needs to answer questions like:
If retrieval is weak, the answer is weak. If indexing lags, context is missing. If parsing is wrong, the model sees the wrong evidence. RAG isn't a feature on the side — it's the memory layer of the product.
Twio is a multi-tenant SaaS, so retrieval can't just return "similar content" — it has to return similar content scoped to the right user, client, application, or file. pgvector made that trivial: embeddings sat next to the business records, joined cleanly, and filtered with plain SQL.
The early wins were real:
It let us build the first version quickly and learn from actual usage. That matters more than people give it credit for.
pgvector didn't fail. It did exactly what it's designed for. The issue was that vector storage is only one slice of the RAG pipeline, and pgvector left every other slice to us:
A clean PDF is easy. A scanned bank statement isn't. An email body is easy. An email with five attachments, lender forms, tables, and partial OCR isn't. A demo dataset is easy. A real broker workspace with years of historical emails isn't.
With pgvector, every weakness in that pipeline was ours to fix. When retrieval quality dropped, the suspect list ran all the way from OCR through chunking and embedding to vector distance, SQL filtering, ranking, and DB performance. The extension is simple. The production RAG system around it isn't.
The cost shifted from cloud bill to engineering time — and engineering time was the constrained resource.
| Scenario | pgvector | Vertex AI Search |
|---|---|---|
| Clean text PDF | We own extraction, chunking, embedding, storage, search | Vertex handles most of the indexing and retrieval workflow |
| Scanned document | We build or integrate OCR ourselves | Vertex absorbs much of the document-processing logic |
| Broker asks a document question | We own query design, ranking, filtering | Managed search with stronger out-of-the-box quality |
| Attachment bursts | Postgres carries more search and indexing load | Search workload lives outside the main database |
| Debugging | Excellent SQL visibility, but many custom layers to inspect | Less low-level control, but far less custom infra to debug |
| Cost | Lower direct service cost | Higher service cost, lower engineering and maintenance cost |
| Production readiness | Significant custom work required | Easier to operate as a managed layer |
pgvector was cheaper as a database extension. Vertex is cheaper as a product decision. The cloud bill is one input; engineering time, reliability, and iteration speed are the bigger ones at our stage.
Twio's RAG problem is document-heavy. We aren't searching short snippets — we're dealing with messy broker PDFs, scans, forms, tables, and forwarded attachments. Vertex helps in four concrete ways:
Vertex isn't free, but the alternative isn't either. Building OCR, indexing, ranking, monitoring, and tuning ourselves has its own bill — paid in engineer-weeks.
pgvector is still a strong choice when:
For us, it was the right first implementation — and it taught us what retrieval the product actually needed. It may stay in the stack for internal or fallback use cases.
The lesson from Twio's RAG evolution is simple:
Start with the tool that helps you learn fastest. Move to the tool that helps you operate best.
pgvector got us to a working RAG system quickly. As the product matured, the real challenge shifted to document processing, indexing quality, and operational reliability — and at that point, Vertex AI Search became the better fit. It costs more as a service and less as a system to maintain. For a SaaS at Twio's stage, that's the trade that matters.
2026-06-22 07:51:51
Dockerfiles that pass a casual build check can still fail silently in production when they lack proper layer caching, run as root, or omit health checks. For teams without a dedicated container specialist, arriving at an optimized, secure configuration often means multiple rounds of scanning, tweaking, and rebuilding — time that could go into shipping features.
Dockerfile Builder is an interactive configuration tool that generates production-grade Dockerfiles following current best practices. Rather than editing text directly, developers select a runtime environment — Node.js, Python, Go, Rust, Nginx, Ruby, Java, Deno, or Bun — and the tool constructs a multi-stage build, layer caching strategy, health check, and non‑root user setup tailored to that stack.
The output is a complete Dockerfile with inline comments that explain the rationale behind each instruction, from choosing slim base images to ordering COPY commands for cache efficiency. The builder automatically splits dependency installation and application code across stages, reducing final image size without manual tweaking. It appends a HEALTHCHECK instruction that common orchestrators can query, and it generates the USER directive and associated permission changes so the container never runs as root. Every generated file follows patterns aligned with the OWASP container security guidelines and Docker official image best practices.
The tool runs entirely in the browser — no uploads, no signup, no tracking — part of the DevTools collection of 200+ free browser tools for engineers. All configuration stays local, so proprietary project details never leave the machine.
Start by selecting your application’s runtime. The interface immediately picks an appropriate slim base image (for example, node:18-alpine for a Node.js service) and suggests common patterns for that ecosystem.
Next, define the build stage. Specify the builder base image version, working directory, and the files to copy first for layer caching. For a Node.js project, you might select package.json and package-lock.json and set the build command to npm ci --only=production. The tool uses this to place dependency installation before source code, maximizing cache reuse:
# Builder stage — generated example
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
Move to the final stage. Choose the production base image (often the same slim variant), a working directory, and which artifacts to copy from the builder stage. Add environment variables, set the exposed port, and specify the start command. The generator structures the copy step to avoid dragging development-only dependencies into the final image.
Toggle the health check option and provide the endpoint your application exposes (e.g., /health on port 3000). The builder inserts the appropriate HEALTHCHECK instruction with retry and interval defaults that you can tune. The security toggle creates a dedicated system user and sets file ownership, so the container’s runtime user is never root. The final generated Dockerfile can be copied, downloaded, or edited directly in the browser.
Reach for Dockerfile Builder when containerizing a greenfield service and you want a secure, optimized baseline without spending an afternoon on Dockerfile research. It’s equally useful for teams that deploy containerized workloads only occasionally — the generated comments help maintainers understand the configuration months later.
The tool addresses a specific gap in existing Docker tooling: interactive, visual configuration with immediate feedback. Unlike IDE snippets or static templates, it enforces best practices (multi-stage builds, non‑root user, health checks) by default rather than leaving them as optional checkboxes. That makes it a quick standardization layer when an organization wants every new service to ship with uniform security and observability patterns, regardless of the original author’s Docker experience.
Use it as a learning aid when ramping up on containerization. Because every line includes an explanatory comment — why COPY comes before RUN npm install, why Alpine images are selected, how the --from=builder syntax works — the output doubles as reference material. When a project’s needs outgrow the generator, the resulting Dockerfile remains a readable foundation that can be extended by hand.
The tool is available at dockerfile-builder. It runs entirely in the browser, so you can generate a best‑practice Dockerfile in about 30 seconds without creating an account or installing anything. Select a runtime, walk through the stages, and get a deployable configuration you can inspect or copy directly.
For managing secrets and configuration values in container environments, the Environment Variable Encoder/Decoder helps encode and decode base64 strings without leaving the browser. The Gitignore Generator builds tailored .gitignore files that include Docker‑specific entries like .dockerignore and generated build artifacts, keeping repositories clean. Teams building test data for containerized services often combine the Dockerfile Builder with the Mock Data Generator, which produces realistic datasets for development and staging environments.
A well-structured Dockerfile doesn’t just build — it communicates intent to every engineer who touches it down the line.
Try it: Dockerfile Builder on DevTools
2026-06-22 07:51:39
Honestly, I was stuck in a loop that felt like rewinding the same scene over and over. I’d just finished a simple chat widget for a side‑project, and every time a user typed a message I’d fire off an AJAX poll every second to see if anything new had arrived. The UI felt clunky, the server was getting hammered, and users complained about lag. I kept thinking, “There’s gotta be a better way.”
One night, after yet another 3 a‑hour debugging session where I watched my browser’s network tab spike like a heart monitor during a cardio session, I stumbled upon a tutorial about WebSockets. The idea of a persistent, two‑way connection sounded like discovering a secret cheat code. I was instantly hooked — no more polling, no more wasted bandwidth, just pure, real‑time magic.
So what’s the treasure? WebSockets give you a full‑duplex channel over a single TCP socket. Unlike HTTP’s request/response dance, once the handshake succeeds the client and server can push data to each other whenever they want. Think of it as opening a walkie‑talkie channel instead of shouting across a crowded room every few seconds.
The handshake starts with an HTTP upgrade request — something like:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
If the server agrees, it replies with a 101 Switching Protocols status, and from that point on you’re speaking a binary (or UTF‑8) frame protocol directly. No headers, no cookies, just raw payloads flying back and forth at lightning speed.
The beauty is that you can build anything that needs instant updates: chat rooms, live notifications, collaborative editors, multiplayer game state, you name it. The latency drops from hundreds of milliseconds (polling) to tens of milliseconds, and the server load becomes predictable because each client holds exactly one open socket.
Here’s what the naive polling version looked like (client‑side only):
let lastId = 0;
function fetchMessages() {
fetch(`/api/messages?since=${lastId}`)
.then(r => r.json())
.then(data => {
data.forEach(msg => {
renderMessage(msg);
lastId = msg.id;
});
})
.catch(console.error);
}
// Poll every second – yikes!
setInterval(fetchMessages, 1000);
Every second we slammed the API with a request, even when nothing changed. The server had to parse the query, hit the DB, and send back an empty array most of the time. It worked, but it felt like trying to fill a bathtub with a teaspoon.
Now let’s switch to a real‑time socket. I’ll use Node.js with the ws library because it’s lightweight and shows the raw mechanics — no magic abstractions, just the core. (If you prefer Socket.IO for rooms and fallback, the concepts are the same.)
Server (server.js)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// Keep track of all connected clients so we can broadcast
const clients = new Set();
wss.on('connection', ws => {
console.log('🟢 New client connected');
clients.add(ws);
// Handle incoming messages from this client
ws.on('message', message => {
console.log(`📨 Received: ${message}`);
// Broadcast to everyone *except* the sender
for (const client of clients) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(message);
}
}
});
// Clean up when a client leaves
ws.on('close', () => {
console.log('🔴 Client disconnected');
clients.delete(ws);
});
// Optional: heartbeat to detect dead connections
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
});
const interval = setInterval(() => {
for (const ws of clients) {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
}
}, 30000);
Client (index.html)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Realtime Chat</title>
<style>
#log { height: 300px; overflow-y: scroll; border: 1px solid #ccc; padding: 10px; }
</style>
</head>
<body>
<div id="log"></div>
<input id="msg" placeholder="Type a message" autocomplete="off"/>
<button id="send">Send</button>
<script>
const log = document.getElementById('log');
const input = document.getElementById('msg');
const btn = document.getElementById('send');
// Open the socket
const socket = new WebSocket('ws://localhost:8080');
socket.addEventListener('open', () => {
log.innerHTML += '<div>🟢 Connected to server</div>';
});
socket.addEventListener('message', event => {
const data = event.data;
log.innerHTML += `<div>💬 ${data}</div>`;
log.scrollTop = log.scrollHeight;
});
socket.addEventListener('close', () => {
log.innerHTML += '<div>🔴 Disconnected</div>';
});
btn.addEventListener('click', () => {
const text = input.value.trim();
if (text) {
socket.send(text);
input.value = '';
}
});
// Allow pressing Enter
input.addEventListener('keypress', e => {
if (e.key === 'Enter') btn.click();
});
</script>
</body>
</html>
What changed?
Forgetting to handle reconnections – Networks drop. If you don’t implement a retry strategy (exponential backoff is a solid choice), users will stare at a dead UI. A simple setTimeout that tries to reconnect after a failure works wonders.
Leaking sockets – Every new WebSocket() must eventually be closed (socket.close()) or you’ll accumulate zombie connections that eat up server memory. Always clean up on page unload or component unmount.
Broadcasting to the sender – In the example we deliberately skip the original sender (if (client !== ws)). Sending the message back to the same client creates an echo loop that can confuse UI state (you’ll see your own message appear twice).
Skipping heartbeats – Some proxies or load balancers idle‑close silent TCP connections after a few minutes. Sending a periodic ping/pong (as shown) keeps the socket alive and lets you detect dead peers early.
Now that you’ve got the spellbook, the world opens up. Want to build a live‑scoreboard for a sports app? Push updates the second a goal is scored. Need a collaborative whiteboard? Broadcast each stroke as it happens. Dreaming of a notification banner that appears the moment a friend posts? WebSockets make it feel instantaneous, like a spell cast in real time.
The best part? The mental shift. Instead of thinking “I have to ask the server every second if anything changed,” you start thinking “I have a permanent line open — what do I want to say?” That shift reduces boilerplate, cuts latency, and makes your apps feel alive.
And hey, if you ever feel overwhelmed, remember: even Neo had to learn to dodge bullets before he could bend the Matrix. You’ve already taken the red pill — now go build something awesome.
Here’s a quick quest for you: take the chat example above, add a simple “typing…” indicator that shows when another user is actively typing, and deploy it to a free Node host (like Render or Fly.io). When you see the indicator appear in real time without any polling, you’ll know you’ve truly leveled up.
What real‑time feature are you itching to build next? Drop a comment or tweet me — I’d love to see what you conjure!
May your sockets stay open and your data flow fast. 🚀
2026-06-22 07:49:41
Part 1: Self-hosting on Jetson Orin Nano
Cool! Now that the mini web server is up and running, how can I see web traffic easily? I discovered GoAccess recently, which is a free and open source tool for checking out server logs in real time. There are two way to view it. At first I was happy to just see nicely-parsed server logs in the terminal. Ahhhh, organization! It gives you a bunch of interesting stuff to look at.
Here is what the terminal view looks like:
Dats coolYou know, inviting traffic to a webserver invokes anxiety. Having half an idea of what is happening helps ease tension for sure. I was really excited to find this tool. You can open go access in the terminal to display different information with different views. I will leave the explaining to the official documentation found here: GoAccess Docs
But the web developer in me was super excited to find a very human-readable html version readily available. Using a reverse proxy through nginx, you can view all the stats on a web page locally. It also allows you to pick a theme and customize how the information is displayed. Be sure to check out the settings and chart options!
Here is what the html view produces:
Stats for silly hoomins.Idk about you, but this is my super exciting find for the day.
I think my next step is to connect an agent that reads the logs and alerts me on set parameters.
I'm interested to hear what tools all of you use to enhance web server monitoring?
What's the best agent for web analytics, in your opinion?