Pixel

Pixel Updates & Events

Server Updates & Events

Announcements

DEV Community

Cross‑Request KV Cache Exposure in GLM‑5.3 Workers AI: A Deep Dive and SGLang Remediation

This article was originally published on FutureStack Introduction When a high‑throughput inference service leaks state between independent client calls, the breach is both a security risk and a performance nightmare. In late 2024 Cloudflare disclosed a cross‑request KV cache exposure in the GLM‑5.3 model running on Workers AI. The root cause was a subtle configuration mismatch that triggered a bug in SGLang, the lightweight runtime used to orchestrate token generation. Below we dissect the architecture, reproduce the failure mode, and walk through the remediation strategy we implemented at FutureStack. 1. Architectural Deep‑Dive 1.1 GLM‑5.3 on Workers AI – The Stack Layer Technology Role Model GLM‑5.3 (Meta) Large language model (≈ 5.3 B parameters) Runtime SGLang (C++/Rust hybrid) Token‑level scheduler, KV‑cache manager Edge Compute Cloudflare Workers (V8 isolates) Request isolation, network I/O State Store Workers KV (distributed LSM) Persistent key‑value cache for attention KV pages Transport HTTP/2 + binary protobuf Low‑latency inference payload The model is sharded across three V8 isolates. Each isolate holds a local KV cache for the attention mechanism (the so‑called KV pages). The cache lives in Workers KV, which offers eventual consistency and high read‑through performance but no built‑in request‑level isolation. 1.2 KV Cache Memory Model During generation, each transformer layer emits a key and a value matrix for the current token. These matrices are stored as pages (e.g., 64 KB each) in Workers KV under a composite key: kv:{model_id}:{request_id}:{layer_idx}:{token_idx}
model_id – static identifier (e.g., glm5.3). request_id – UUID generated per HTTP request. layer_idx – layer number (0‑N). token_idx – position of the token in the generated sequence. When the runtime needs to attend to past tokens, it fetches the appropriate pages via a batched GET. The latency budget per token is ~2 ms on the edge, so the KV fetch must be cache‑friendly and avoid cross‑request contention. 1.3 SGLang’s KV‑Cache Manager SGLang abstracts the KV store behind a CacheHandle object. Internally it: Pre‑allocates a buffer pool for each request. Maps KV pages to buffer slots via a hash table keyed by the composite string. Releases buffers back to the pool when the request completes. The critical invariant is that no buffer may be reused until the request’s lifecycle ends. The pool is global to the isolate to amortize allocation cost. 1.4 The Configuration Mismatch Workers AI ships with two mutually exclusive modes: Mode sglang.kv_isolation Description Strict true Each request gets a private pool; buffers are zero‑filled on reuse. Relaxed false Buffers are recycled across requests for throughput. A deployment script mistakenly set sglang.kv_isolation = false while the model’s KV‑page TTL was set to 0 (no expiration). The combination meant that after a request finished, its buffers were immediately recycled and could be served to a subsequent request before the KV store had flushed the stale pages. An attacker could therefore infer previous token embeddings by timing the fetch latency or, in the worst case, read the raw KV payload. 2. The SGLang Bug The bug manifested in the CacheHandle::acquire routine: CacheSlot CacheHandle::acquire(const std::string& key) {
auto it = pool.find(key);
if (it != pool.end()) {
// BUG: missing guard that ensures the slot belongs to the current request
return it->second;
}
// allocate new slot …
}
Because the pool key omitted the request_id component when kv_isolation was disabled, two distinct requests could resolve to the same slot. The isolation contract was silently broken, leading to the cross‑request exposure. 3. Remediation Blueprint FutureStack’s remediation strategy had three pillars: Enforce request‑scoped keys – always embed request_id in the KV lookup, regardless of the isolation flag. Introduce a
generation fence – a lightweight epoch counter that invalidates a...

Powered by Readybot.io*

**DEV Community** ## Cross‑Request KV Cache Exposure in GLM‑5.3 Workers …

DEV Community

I Turned Spain's BOE Public Auctions Portal into a Searchable Site

Spain's BOE runs a public auction portal. Every day it lists court-ordered repossessions, tax-agency seizures and notarial sales: flats, garages, warehouses, plots, vehicles. Each listing states an auction value, a minimum bid, a deposit, whether the property is occupied and what charges survive the sale. All of it is public, and none of it is designed to be read by someone deciding whether to bid. I built Subastas BOE around it: a Spanish-language site that turns those listings into a searchable catalogue. The hard part was not the frontend, it was making a government HTML portal behave like a dataset. See it in Spanish: subastas.devetty.es has a filterable search, one page per province, per municipality and per asset type, and a guide to bidding. Two pages per auction The portal exposes a search page and a detail page. The search page returns result blocks; each block has a title, an authority, a file number and a link to the auction. The record itself is split across tabs, and each tab is a separate request with a different ver parameter. So the scrape is one list request with pagination, then two detail requests per auction — one for the auction terms, one for the asset. The list config is short: "list": {
"selector": "li.resultado-busqueda",
"dedupeBy": "id",
"required": ["id", "url_ficha"],
"fields": {
"id": {
"selector": "h3",
"transform": ["collapse", { "regex": "(SUB-[A-Z0-9-]+)" }]
}
}
}
The identifier is the only thing I trust from the list. It looks like SUB-JA-2026-..., and it is what the detail URL needs: https://subastas.boe.es/detalleSubasta.php?idSub={id}&ver=1
https://subastas.boe.es/detalleSubasta.php?idSub={id}&ver=3
Pagination follows a link labelled siguiente, capped at 30 pages and paced 1.5–2.5 seconds apart. Detail requests run one at a time with a 2–4 second delay, and the whole thing is a single daily run, not a tight loop. This is a public service. The cost of being polite is a slower crawl; the cost of not being polite is being blocked, deservedly. Parsing with configuration instead of a parser I did not write selectors in code. The scraper takes a JSON config where each field declares a CSS selector and a chain of transforms. Extracting the minimum bid becomes a line in a file, not a function to debug: "puja_minima": {
"selector": "th:contains('Puja mínima') + td",
"transform": ["collapse", "number"]
}
The portal renders label/value tables, so th:contains(...) + td does most of the work. Dates are the exception: the conclusion date is human text with an ISO timestamp inside parentheses, so I regex the ISO: part for fecha_fin and keep the display text separately. The province problem is a slug problem Asset details carry a province in the source spelling, and Spain's provinces are officially bilingual in several regions. The portal is inconsistent: Araba/Álava, Bizkaia, Gipuzkoa, Illes Balears, Valencia/València, Palmas (Las). Slug those directly and you get three URLs for one place, plus a hub page that splits its own inventory. I resolve it at ingestion, as a chain: "provincia_es": { "op": "clean_regex", "from": "provincia", "pattern": "/^Araba\s\/\s/u" },
"provincia_slug_base": { "op": "clean_regex", "from": "provincia_es", "pattern": "/\s\/.$/u" },
"provincia_alias": { "op": "map", "from": "provincia_slug_base",
"map": { "Bizkaia": "Vizcaya", "Gipuzkoa": "Guipúzcoa", "Illes Balears": "Baleares" } },
"provincia_canon": { "op": "coalesce", "fields": ["provincia_alias", "provincia_slug_base"] },
"provincia_slug": { "op": "slug", "from": "provincia_canon" }
Two regexes and one alias map reduce every variant to one canonical name and one slug. Araba/Álava becomes alava, Valencia/València becomes valencia. The display name stays in provincia; only the URL is canonicalised. That single decision is why the site can cross-link. The same slug works on my sister sites, so an auction in madrid can point at publi...

Powered by Readybot.io

**DEV Community** ## I Turned Spain's BOE Public Auctions Portal …

** Latest from PC Gamer **

League of Legends will get an overhaul called New Dawn with new visuals, build options, and an updated client

"We want League to stand the test of time."

Powered by Readybot.io

** Latest from PC Gamer ** ## League of Legends will get an

DEV Community

Why Is COUNT(*) So Slow on Large Tables?

COUNT() feels like it should be instant. The table already exists, an index already exists, so why does Postgres sometimes scan a large table just to answer how many rows it holds? The short answer is that in Postgres, a count is not a stored number. It depends on what the current transaction can see. Why Postgres checks rows instead of reading a stored count Postgres uses MVCC, which means different transactions can see different versions of the same table at the same time. A row inserted by an uncommitted transaction, or deleted but not yet vacuumed, may be visible to one transaction and invisible to another. That means there is no single correct count for a table in isolation. The count depends on the snapshot of the transaction running the query. Postgres therefore has to check visibility, row by row or index entry by index entry, rather than returning a cached total. SELECT COUNT() FROM orders;
On a small table this is fast. On a table with millions of rows, that visibility work adds up, even when the query itself looks trivial. Why an index does not always save the query An index can help, but only in the right conditions. Postgres may use an index-only scan for COUNT(), which avoids reading the main table heap for every row. Even then, it still needs to confirm visibility. Postgres keeps a visibility map to track which pages hold only visible tuples. If a page is marked all-visible, an index-only scan can skip heap checks for that page. If the table has not been vacuumed recently, or has a lot of recent updates and deletes, many pages will not be all-visible. The scan then falls back to heap fetches to check visibility, and much of the benefit disappears. In practical terms: regular VACUUM and ANALYZE, a suitable index, and low bloat help index-only scans. Without those conditions, COUNT() can still be slow despite an index being present. How to see what is happening Start with EXPLAIN, and add ANALYZE and BUFFERS when checking a slow query in a safe environment: EXPLAIN (ANALYZE, BUFFERS) SELECT COUNT() FROM orders;
Look for the scan type, whether heap fetches appear on an index-only scan, and how many buffers were read. A sequential scan on a large table, or a high heap fetch count, usually points to visibility work rather than a missing index alone. Avoid treating one plan as proof for every case. Table size, write patterns, vacuum history, and Postgres settings all change the result. Faster options when an exact count is not required Many uses of COUNT(
) do not need an exact number. Dashboards, admin totals, and rough progress indicators often work well with an estimate. 1. Use the planner estimate from pg_class Postgres stores an estimated row count in pg_class.reltuples, updated by VACUUM and ANALYZE: SELECT reltuples::bigint AS estimated_rows
FROM pg_class
WHERE relname = 'orders';
This is fast because it reads catalog metadata, not the table. The trade-off is accuracy. The estimate can be stale after bulk loads, large deletes, or before the next analyze run. Use it when “about right” is good enough, and say so in the interface if users will see the number. 2. Read the estimate from EXPLAIN A plain EXPLAIN without ANALYZE shows a row estimate for the query without executing it. It comes from the same planner statistics, so it shares the same staleness trade-off. It is useful for a quick check, but it is not a substitute for an exact count when exactness matters. 3. Maintain a counter for hot totals For a total that is read often and written often, a separate counter can help. The usual pattern is a small table holding one row per counted thing, updated when rows are inserted or deleted, or refreshed on a schedule from a background job. This moves the cost from read time to write time or to a periodic refresh. It also introduces new trade-offs: a single counter row can become a contention point under heavy writes, and a scheduled refresh means the displayed total lags behind reality. It suits dashboar...

Powered by Readybot.io

**DEV Community** ## Why Is COUNT(*) So Slow on Large …

DEV Community

Kubecost vs. OpenCost in 2026: Production Architecture & Setup for Kubernetes FinOps

As containerized workloads scale across enterprise production clusters, understanding where container spend goes is one of the hardest challenges in cloud financial management. Traditional cloud billing tools like AWS Cost Explorer or Azure Cost Management stop at the node level. They can tell you how much an EC2 instance or an EKS node pool costs, but they are entirely blind to what is happening inside the cluster: which microservices, Kubernetes namespaces, deployments, or individual engineering teams consumed that compute. In our foundational guide on Kubernetes Cost Allocation & Showback in 2026, we explored why accurate container showback is non-negotiable for modern engineering squads. To achieve this granularity, platform engineers generally choose between two primary open-standards-based tools: OpenCost (the CNCF open-source standard) and Kubecost (the commercial enterprise tier built on top of OpenCost). In this technical deep dive, we will compare their production architectures, resource footprints, and setup patterns to help you choose the right stack for your cluster. Architectural Overview: The Shared Core Before comparing features, it is vital to understand their relationship. OpenCost was originally created by Kubecost and later donated to the Cloud Native Computing Foundation (CNCF). At their core, both tools rely on the same Prometheus metrics pipeline to scrape cAdvisor and Kubernetes API server data, mapping CPU, memory, persistent storage, and network egress down to individual pods. OpenCost: A lightweight, 100% open-source Prometheus-exporter daemon designed specifically for real-time cost allocation, multi-cloud pricing integration, and basic namespace-level reporting without licensing overhead. Kubecost: Built on top of the Open Cost engine, Kubecost adds an enterprise control plane, advanced access control (RBAC), multi-cluster aggregation, automated governance alerts, and direct integrations with enterprise ticketing and billing systems. Step 1: Deploying Open Cost via Helm in Production If your primary goal is clean, zero-cost namespace show back backed by your existing Prometheus instance, Open Cost can be deployed in minutes via its official Helm chart. Below is a production-hardened Helm values configuration that secures the deployment and points it to your cluster's Prometheus endpoint: opencost-production-values.yaml exporter: costModel: prometheusServerEndpoint: "http://prometheus-k8s.monitoring.svc.cluster.local:9090" cloudProvider: "aws" clusterId: "production-eks-cluster-01" resources: requests: cpu: 200m memory: 256Mi limits: cpu: 1000m memory: 1024Mi service: type: ClusterIP port: 9003 persistence: enabled: true storageClass: "gp3" size: 10Gi Deploy this chart into your monitoring namespace using Helm: helm repo add opencost https://opencost.github.io/opencost-helm-chart helm repo update helm upgrade --install opencost opencost/opencost \ --namespace monitoring \ -f opencost-production-values.yaml Step 2: Evaluating the Kubecost Enterprise Upgrade Path While OpenCost handles single-cluster allocation exceptionally well, platform teams managing multiple EKS, GKE, or AKS clusters often hit architectural limitations. You should consider upgrading from OpenCost to Kubecost if your enterprise environment requires: Cross-Cluster Federation: Aggregating cost data across 10+ disparate Kubernetes clusters into a single unified global dashboard. Enterprise RBAC & SSO: Restricting engineering teams so they can only view cost allocations for their own specific namespaces via Okta, Keycloak, or SAML integration. Advanced Recommendations: Automated, machine-learning-driven container right-sizing suggestions that output exact CPU and memory limit adjustments. Allocation Accuracy for Shared Services: Granular algorithms for splitting shared infrastructure costs (like Ingress controllers or monitoring agents) across tenant namespaces proportionally. Choosing the Right Tool for Your FinOps Stage If you a...

Powered by Readybot.io

**DEV Community** ## Kubecost vs. OpenCost in 2026: Production Architecture …

DEV Community

My nightly backup job had been green for months. I never restored it once.

My nightly backup job had been green for months. I never restored it once. Last week I looked at a cron job of mine that had printed "backup ok" every night for months, and I asked myself a plain question: when did I last load one of those files into a database? I could not remember a date. A green job tells you a file was written. It does not tell you the file works. I built a small test and ran it end to end. I created a Postgres database, backed it up on a schedule, changed the live data afterwards, restored the backup into a separate database, and compared row counts. I used Levelrail, the self-hosted deploy platform I work on, built from the v1.0.1 release branch (commit d510e8488). Everything below ran on my Mac with Docker, and I mark the parts I only read in the docs. TL;DR Step Command What it proves Schedule backups schedule set A backup runs without you Restore as new backups restore-as-new The backup loads into a fresh database Compare databases query Counts match the moment of the backup Drill backups drill run Checksum, restore and content check pass Copy backups destinations copy set A second bucket holds the same object (docs only) The test setup I made a Postgres 16 database with two tables: 500 customers and 2000 orders. Then I recorded three numbers I could check later. levelrail-cli databases create --name main --engine postgres --version 16
levelrail-cli databases query main --sql "select (select count() from customers) as customers, (select count() from orders) as orders, (select sum(total_cents) from orders) as total_cents"
customers orders total_cents
500 2000 8805000
I pick a sum on top of the counts because two tables with the right row count can still hold the wrong values. Schedule it and take one now A backup target is the place the files go. Levelrail has a local type that needs no bucket, which is fine for a first run but lives on the same disk as the database, so it is no protection against losing the machine. I used it first, then switched to an S3-compatible target, which I get to below. levelrail-cli backup-targets create --name nightly --provider local --path nightly
levelrail-cli backups schedule set main --target TARGET_ID --cron "0 3 * * *" --retain 7 --retain-days 30
levelrail-cli backups trigger main --target TARGET_ID
levelrail-cli backups list main
The list printed a succeeded row of about 11.8 KB, finished in the same second it started. Retention combines two limits: keep at most the newest 7 backups, and nothing older than 30 days. A backup goes when either rule would remove it (docs/guides/self-host-postgres-with-backups.md). Break the live data, then restore the old state This is the step that makes the test mean something. After the backup, I deleted 500 orders from the live database, the way a bad migration or a wrong DELETE would. levelrail-cli databases query main --write --confirm main --sql "delete from orders where id > 1500"
levelrail-cli backups restore-as-new main --backup BACKUP_ID --new-name main-restore
Restoring as new builds a separate database next to the original and never touches it. The new container was up in under a minute. Then I ran the same count query against both. live
customers orders total_cents
500 1500 6597750

restored
customers orders total_cents
500 2000 8805000
The restored database has the numbers from before the delete, to the cent, and the live one has the damage. That is the output I wanted from a backup, and I had never produced it from my cron job. An in-place restore also exists. It overwrites the live database and asks you to type the database name, so I would use restore-as-new first and only then decide. What a restore drill adds A manual rehearsal is good once. A drill is the same idea on a timer. For Postgres, MySQL and MariaDB, Levelrail downloads the stored backup, compares its SHA-256 with the checksum recorded at backup time, replays it into a throwaway container with no network, check...

Powered by Readybot.io

**DEV Community** ## My nightly backup job had been green …

DEV Community

My YouTube automation kept "running" for almost 6 months. The bot made 1,376 commits. Zero uploads, and I had no idea.

I run a small Python pipeline on GitHub Actions that generates and uploads Shorts for a few YouTube channels. Five scheduled workflows, including one I proudly named "Self-Healing Health Check". Last weekend I finally opened the Actions tab. Every recent run was red. Upload runs, growth jobs, and the self-healing job itself. Meanwhile the bot had pushed 1,376 automated commits since May, so from a distance the repo looked alive. Last successful upload before the fix: May 25. That is 175 days of nothing. I sat down with an AI coding agent and had it run every stage and classify each failure instead of fixing anything. Result: 14 separate problems. 4 were expired or revoked credentials 5 were upstream changes (a TTS endpoint returning 403 until I upgraded the library, a deprecated image model, a free tier that added rate limits, a removed vision model, a trends endpoint returning 404) 5 were my own code or config So 9 of 14 had nothing to do with my code. The world moved and my pipeline stayed still. The actual root cause was not in the repo at all. My Google OAuth app was still in "Testing" status, which means refresh tokens die after 7 days. Every re-auth I had ever done was a 7-day fix. The error in the logs was just invalid_grant. Fixing that had its own trap: the "Publish app" button was greyed out with a vague message about completing the Branding page, even though every required field was filled. It only unlocked after I added a home page URL, a privacy policy URL and an authorized domain. For a personal uploader script. I used a tiny public GitHub repo with a README and PRIVACY.md. Other things I learned the hard way that night: My self-healing workflow shared the same dead token as the jobs it was supposed to heal. It failed every time, quietly. A watchdog in the same failure domain is not a watchdog. My local repo had six months of uncommitted changes. Production was running code from May. I had been "fixing" a version that was never deployed. The agent printed my git remote URL, which had a personal access token embedded in it. Rotated it to a fine-grained token, forgot the Secrets permission, got a 403 on gh secret set, fixed that too. I asked the agent to delete temp files matching probe. The match was case-insensitive and it deleted 41 files of a finished video about the Parker Solar Probe. That one is on me for the sloppy instruction. What fixed production in the end was almost no code. It was: find the real cause, change one Google setting, re-auth once, update one secret. First green scheduled run in a long time followed a few minutes later. What I am changing: Check outcomes, not exit codes. "Did a video actually appear on the channel today" is the only health check that matters. Keep an expiry ledger: every token, key, free tier and model name, with the date it dies. Alerts go somewhere I actually look, and the alerting path must not depend on the thing it monitors. Question for people running unattended automations: how do you catch the "everything is technically running but nothing is being produced" case? Heartbeats? Output checks? Something smarter? Note: I wrote this post with AI assistance. The incident, numbers and fixes are from my own pipeline.

Powered by Readybot.io

**DEV Community** ## My YouTube automation kept "running" for almost …

DEV Community

How to Send a Password Without Leaving It in Your Chat History

You need to give a coworker the Wi-Fi password, or a family member the login for a shared account. The fastest way is to paste it into a chat. The trouble is that chat messages are built to last. They sync to every device you're signed in on, they get backed up, they show up in search, and they're still there years later when someone borrows your laptop or a backup leaks. A password pasted into a chat lives as long as the chat does. This post walks through a simple fix, the one-time secret link: how it works, the browser crypto that makes it possible, and the limits worth knowing before you rely on it. The idea: a link that works once A one-time secret link (also called burn-after-reading) turns a piece of text into a link that the recipient can open exactly once. After that it's destroyed, and if nobody opens it before it expires, it's destroyed too. What's left in the chat history is a dead link. Anyone scrolling back later finds a link that no longer opens anything. Plenty of tools offer this. The interesting part is how you build one so that the service in the middle can't read the secret either. How it works under the hood A good design has four pieces. 1. Encrypt in the browser, before anything is uploaded The sender's browser generates a random key and encrypts the text locally, using AES-GCM via the Web Crypto API. Only the encrypted bytes (the ciphertext) go to the server. 2. Put the key in the URL fragment The key goes into the link, after the #: https://example.com/s/abc123#k=Q3VyaW91cyBhcmVuJ3QgeW91
The part after # is the fragment. Browsers don't send the fragment to the server when they request the page, and they leave it out of the Referer header too. So the server stores ciphertext it has no key for, and the key travels only inside the link you send. The fragment does stay in the browser's address bar and history, and in the chat as part of the link. That's exactly the copy that goes dead once the secret is burned. 3. Burn on read, and expire When the recipient opens the link, the server hands over the ciphertext once and deletes it. If the link is never opened, a timer deletes it anyway. 4. Don't burn on page load Chat apps often fetch a link in the background to build a preview card. If merely loading the page reveals and burns the secret, a preview bot can use up the one view before your recipient ever sees it. The fix is to load the page with zero requests to the secret, and fetch the ciphertext only when a person presses an "open" button. A minimal sketch in JavaScript Here's the core of both sides using the Web Crypto API. upload and fetchAndBurn stand in for your own server calls. // helpers: bytes <-> URL-safe base64
const toB64Url = (bytes) =>
btoa(String.fromCharCode(...bytes)).replace(/+/g, "-").replace(/\//g, "").replace(/=+$/, "");
const fromB64Url = (s) =>
Uint8Array.from(atob(s.replace(/-/g, "+").replace(/
/g, "/")), (c) => c.charCodeAt(0));

// Sender: encrypt locally, upload only ciphertext, keep the key for the link
async function createLink(secret) {
const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode(secret));
const rawKey = new Uint8Array(await crypto.subtle.exportKey("raw", key));
const id = await upload(iv, new Uint8Array(ciphertext)); // server sees iv + ciphertext only
return https://example.com/s/${id}#k=${toB64Url(rawKey)};
}

// Recipient: runs only after the person clicks "open"
async function openLink(id) {
// base64url has no & or =, so URLSearchParams can parse the fragment safely
const k = new URLSearchParams(location.hash.slice(1)).get("k");
const key = await crypto.subtle.importKey("raw", fromB64Url(k), "AES-GCM", false, ["decrypt"]);
const { iv, ciphertext } = await fetchAndBurn(id); // server returns once, then deletes
const plain...

Powered by Readybot.io

**DEV Community** ## How to Send a Password Without Leaving …

DEV Community

Boot the Release Against a Copy of Production Before You Switch

The deploy script already had a rollback. Switch the current symlink, restart the systemd unit, poll /healthz, and if that fails, switch back and restart again. On paper that's a safe deploy. In practice, every broken release still produced an outage. The reverse proxy returned 502 from the moment the old process stopped until the rollback had brought it back — up to about 35 seconds in the worst case, and a few seconds even after the health check learned to fail fast. A post-switch health check can't prevent that. It runs after the damage, and its only job is to bound it. The fix was to move the question "does this release start?" to before the switch, and to ask it against the real data. TL;DR: Before touching current, the deploy now copies the live SQLite database with VACUUM INTO, starts the new release on a free localhost port against that copy with a throwaway config and session secret and every outbound job disabled, and waits for /healthz. If it doesn't come up, the deploy aborts with the test server's last log lines and production never notices. The first check in the preflight is whether the release is readable by the runtime group — because the preflight runs as the deploy user and would otherwise miss permission bugs entirely. A deliberately broken release was then pushed through the real pipeline: the deploy failed, the site served HTTP 200 throughout. What Does a Post-Switch Health Check Actually Protect? Less than it looks like. The sequence is: switch current to the new release restart the service — the old process is gone now poll /healthz on failure: switch back, restart, poll again Everything between step 2 and the end of step 4 is downtime if the release is broken. The health check decides whether you stay down, which is valuable, but the outage itself is already happening by the time it reports anything. What breaks a release in a small Node.js app is rarely subtle. A missing package after npm ci, a syntax error in a file the tests didn't load, a schema migration that throws on real data, a file the runtime user can't read. All of those fail within the first second of node server.js. None of them needs the release to be live to show up. They just need the release to start. How Do You Start a Second Copy Without Touching the First? Three things have to be separate from production: the port, the data, and anything the app does to the outside world. The port. Ask the kernel for one instead of picking a number: port="$(node -e 'const s=require("net").createServer().listen(0,"127.0.0.1",()=>{console.log(s.address().port);s.close()})')"
There's a tiny race between closing that socket and the app binding it, and on a single-tenant VM it doesn't matter. Binding to 127.0.0.1 matters more — the preflight instance must never be reachable from outside, not even for the few seconds it lives. The data. The app runs on SQLite in WAL mode, which means copying the .db file with cp while the app is running can produce a file that's missing whatever is still sitting in the -wal file, or one that's torn mid-write. VACUUM INTO asks SQLite itself for a consistent snapshot, WAL contents included, as a single self-contained file: vacuum_into() {
TARGET="$1" node -e '
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync(process.env.DB_FILE);
db.exec("VACUUM INTO \x27" + process.env.TARGET.replace(/\x27/g, "\x27\x27") + "\x27");
db.close();
'
}
It reads while the live app keeps writing, and it's the same call the deploy already used for its pre-switch backup. Using the real data rather than a fixture is the point: migrations run on startup, and a migration that works on the test schema but fails on a production row is precisely the bug a fixture can't find. The side effects. This is the part that's easy to get wrong. A second instance of the app is a second instance of everything the app does on startup — including its scheduler. This one syncs orders from an external marketplace ev...

Powered by Readybot.io

**DEV Community** ## Boot the Release Against a Copy of …

DEV Community

Networking Essentials for Software Engineers – Part 5: How Email Actually Works

We use emails almost every day, whether it's for work or communicating with someone. Suppose you're using Gmail and want to send an email to your friend. You enter their email address, write your message, and click Send. That's all we do from our side. But have you ever wondered how Gmail knows where to send that email, especially if your friend uses Yahoo or another email provider? Just like HTTP, email also uses networking protocols to communicate between servers. Let's see how an email travels from our application to the receiver's inbox and understand the different protocols involved. How Does an Email Actually Travel? When we send an email, it doesn't go directly from our computer to the receiver's computer. It goes through email servers, which handle sending, receiving, and storing emails. But who manages these email servers? Some companies maintain their own mail servers, while others use email providers like Google, Microsoft, or Zoho. For example, suppose a company owns the domain example.com. It can set up its own mail servers to handle emails for addresses like [email protected]. Or it can use Google Workspace. Employees can still use [email protected], but Google takes care of sending and receiving their emails. So, having our own email domain doesn't mean we need to run our own mail servers. Now, let's see how an email travels when we send it from Gmail to Yahoo Mail. We'll also look at what changes when both people use Gmail or when we use a third-party email application like Thunderbird. Sending an Email from Gmail to Yahoo Mail Suppose you're using Gmail and want to send an email to your friend who uses Yahoo Mail. You open Gmail in your browser, write your message, enter your friend's email address, and click Send. Let's see what happens next. 1. Your email reaches Gmail's servers Since we're using Gmail in a browser, our email first reaches Google's servers through HTTPS, the same protocol we learned about in our previous article. Google receives the email and checks where it needs to be sent. 2. Gmail finds Yahoo's mail server Suppose your friend's email address is [email protected]. Gmail looks at the domain yahoo.com and needs to find which server receives emails for that domain. To find it, Gmail uses DNS and looks for a special record called an MX (Mail Exchange) record. An MX record tells us which mail servers handle incoming emails for a particular domain. Once Gmail finds the mail server's name, it can find its IP address and connect to it. 3. Gmail sends the email to Yahoo Now Gmail needs to transfer the email to Yahoo's mail server. For this, it uses SMTP (Simple Mail Transfer Protocol). SMTP is the protocol mail servers commonly use to send emails to each other. Gmail connects to Yahoo's mail server, sends the message, and receives a response telling it whether the email was accepted. 4. Yahoo receives and stores the email Once Yahoo accepts the email, it processes the message and delivers it to your friend's mailbox. Now, how does your friend read it? If they open Yahoo Mail in their browser, the browser uses HTTPS to communicate with Yahoo's servers and display the email. But if they're using a third-party email application like Thunderbird, it can use IMAP or POP3 to access emails from Yahoo's servers. We'll understand how IMAP and POP3 work shortly. What If Both of You Use Gmail? Now suppose you and your friend both use Gmail. When you click Send, your browser sends the email to Google's servers using HTTPS, just like in our previous example. But this time, Google doesn't need to find another email provider's mail server. Since your friend also uses Gmail, Google can handle the delivery within its own mail system and store the email in your friend's mailbox. Google may use SMTP or other methods to communicate between its internal servers, but it doesn't need to establish an SMTP connection with another email provider. What If We Use a Third-Party Email Application? So far, we've seen h...

Powered by Readybot.io

**DEV Community** ## Networking Essentials for Software Engineers – Part …

DEV Community

rsync -a Stole the setgid Bit, and systemd Blocked the Rollback

The setup was deliberately boring. A small Node.js app with SQLite on a single VM, a release-directory layout with a current symlink, a CI workflow that uploads each release over SSH, runs npm ci, flips the symlink, restarts the systemd unit and waits for /healthz. If the health check fails, flip the symlink back and restart again. Every piece of that looked right in review. The first time CI ran it end to end, the deploy went red, the app went down, and the rollback — the part that exists precisely for this moment — failed as well. Neither failure was in the deploy logic. Both were in what the tools around it did by default. TL;DR: rsync -a includes -p and -g, so it copies the sender's mode and group onto the target — including onto the release directory itself, which silently clears the setgid bit the server relied on. npm ci then wrote node_modules with the deploy user's private group, and the app, running as a different user that only shares a group, couldn't read its own dependencies. The app crash-looped into systemd's StartLimitBurst, after which systemd refused every start — including the rollback's — until systemctl reset-failed. Fix: normalise group and setgid after the upload, call reset-failed before every restart, and stop waiting on a health check once the unit has visibly crashed. Why Two Users in the First Place? Because the process that writes code to the server shouldn't be the process that runs it. The app runs as an unprivileged runtime user with no sudo and no SSH key. Deploys run as a separate deploy user whose key is restricted and whose sudo rights cover exactly two commands: restarting the unit and reset-failed (why the second one is on the list is most of this post). The two share one group, and the shared directories are set up so both can write: # once, at setup
chgrp -R app-shared /srv/app
chmod -R g+rwX /srv/app
find /srv/app -type d -exec chmod g+s {} + # new files inherit the group
The setgid bit on a directory is what makes this work without either user having to think about it: every file created inside inherits the directory's group instead of the creator's primary group. Combined with umask 0007 on both sides, everything the deploy user creates is group-readable and group-writable, and everything outside the group sees nothing. That's the property the first deploy lost. What Did rsync Actually Change? The workflow creates the release directory first, then uploads into it: ssh deploy-target "mkdir -p '/srv/app/releases/${REL}'"
rsync -az ./ "deploy-target:/srv/app/releases/${REL}/"
The mkdir happens inside releases/, which has setgid, so the new directory is created correctly: shared group, setgid set. Then rsync runs, and -a is shorthand for -rlptgoD. Two of those letters do the damage: -p copies permissions from the sender. On the CI runner, the checkout directory has no setgid bit — why would it. A trailing slash on the source (./) means rsync syncs the attributes of the directory itself as well as its contents, so it chmods the freshly created release directory to the runner's mode. setgid gone. -g copies the group. A non-root receiver can only assign groups it is a member of, so rsync falls back to the deploy user's own primary group for everything it creates. Nothing about this looks wrong in a listing unless you're looking for it. The files are there, the sizes match, the owner is the deploy user as expected. The difference is one letter in the mode column and one name in the group column. The next step made it fatal. npm ci runs on the server, as the deploy user, inside a release directory that no longer has setgid. Every file in node_modules got the deploy user's private group, with umask 0007 stripping all "other" bits. The runtime user — different UID, member only of the shared group — couldn't read a single dependency. require('express') failed on the first line of server.js. Why Didn't the Rollback Save It? Because by the time the script tried to roll back, systemd had st...

Powered by Readybot.io

**DEV Community** ## rsync -a Stole the setgid Bit, and …

DEV Community

Eliminating Redis & Valkey Hot-Key Bottlenecks in Multi-Region Distributed Rate Limiters

When a single enterprise tenant fires 250,000+ requests per second across US, UK, and European API gateways, the failure mode rarely looks like an obvious cluster crash. More often, aggregate Redis or Valkey CPU sits comfortably below 25%, yet P99 gateway latency jumps from 1.2ms to nearly 190ms. In a standard distributed token-bucket setup, an atomic Lua script runs against one central key—such as ratelimit:tenant_9841. Because Redis and Valkey execute Lua scripts single-threaded on the primary node owning that CRC16 hash slot, dozens of edge gateway pods end up queuing behind a single CPU core. (You can explore the complete architectural walkthrough and production implementation in our full guide on AI Dev Toolkit.) Why Single-Key Rate Limiters Stall at Scale Three compounding bottlenecks trigger this sudden latency cliff during heavy traffic surges: Single-Slot Serialization: Wrapping {tenant_id} inside hash tags pins every request for a high-volume customer to one primary shard, leaving the rest of the cluster mostly idle. Cross-Region NTP Clock Skew: Passing edge proxy timestamps into Lua causes inconsistent token refills across us-east-1, eu-central-1, and London edge nodes. Per-Request Network Round-Trips: Paying an inter-AZ network hop on every single HTTP request caps throughput and inflates tail latency. The 3-Layer Architecture Fix (1.45M+ RPS) To eliminate hot-key stalls while keeping strict quota accuracy, high-throughput API gateways combine three architectural shifts: Virtual Key Sharding: Place the shard index inside the hash tag (rl:tenant_882:{shard_0..15}) instead of the tenant ID so token buckets distribute evenly across 16 independent primary slots. Server-Authoritative Millisecond Timing: Anchor elapsed refill math directly to Redis or Valkey server time (redis.call('TIME')) inside Lua so edge clock drift never causes burst leaks or negative deltas. Two-Tier Hybrid Lease Batching (Edge L1 + Cluster L2): Have Go or Rust edge proxies lease micro-batches of tokens (for example, 50 tokens per lease with a short local TTL) in memory. Most checks resolve locally in nanoseconds, cutting cluster round-trips by up to 98%, backed by a fail-open local sliding window if inter-region RTT spikes. What Multi-AZ Benchmarks Show In multi-AZ edge tests simulating a single hot tenant at 300,000 RPS: A single key with per-request Lua execution saturates one primary thread and pushes P99 overhead to 188.4 ms (~285k RPS). Distributing across 16 virtual shards balances CPU load across primaries and drops P99 overhead to 4.8 ms (~920k RPS). Combining 16-shard keys with edge L1 lease batching cuts rate-limit overhead to 0.42 ms while sustaining 1.45M+ RPS with linear horizontal scale. Read the Complete Production Guide For the full Lua token-bucket implementation, EVALSHA deployment checklist, deterministic PEXPIRE memory bounds, and complete multi-region failover playbook, read the full engineering guide on AI Dev Toolkit: 👉 Read the full guide: https://aidevtoolkit.blogspot.com/2026/11/distributed-systems-api-gateway-redis.html

Powered by Readybot.io

**DEV Community** ## Eliminating Redis & Valkey Hot-Key Bottlenecks in …

DEV Community

Regression tests for GROWI-style revision authorization

Regression tests for GROWI-style revision authorization Overview CVE-2026-105272 came from two IDs that looked related to the application but were not proven related to each other. GROWI authorized a page ID, then accepted a revision ID from a different page. The fix in v8.0.7 turns that relationship into an explicit condition. A useful regression suite should test mixed ownership, not only valid and invalid sessions. The security property is that every request binds a child object to the same parent used for authorization. JVN lists the affected range as GROWI v3.2.10 through v8.0.6 and assigns CVSS 4.0 2.3 and CVSS 3.0 4.2 scores. Mechanism and exploitation conditions The vulnerable read paths accepted pageId and revisionId. Authorization checked the page. The revision was fetched without confirming that it belonged to that page. A logged-in user could combine a viewable page with a foreign revision and receive the foreign body. The comment endpoint could return foreign comments. Comment create and update paths accepted foreign revisions for linking. The attacker needed a valid login and a target revision ID. MongoDB ObjectId values contain time, process randomness, and a counter. They are not secret by design. A user can create revisions and observe their shape. The page route did not require edit permission. Share-link access was not affected. Impact A missing test for object ownership can expose restricted page bodies, comments, and creator information. The maintainer says the page read creates no revision and leaves no trace in that path. No reviewed source reports exploitation in the wild. The issue is a confidentiality bypass with a limited comment-linking integrity problem. Affected products and scope JVN lists GROWI v3.2.10 through v8.0.6. The page-body path has been present since v3.3.0, and the comment endpoint accepted revision IDs from at least v3.2.10. The fixed release is v8.0.7. Exposure context ZoomEye returned 401 product-title matches for GROWI on 2026-11-15. The exact vul.cve="CVE-2026-105272" Filter returned zero. These results are inventory evidence only. Version and route testing are still required. Remediation and mitigations GROWI v8.0.7 checks that a supplied revision belongs to the page. Page-body and comment reads fall back to the page's own latest revision or comments when ownership fails. Comment writes reject the foreign revision. The advisory lists no workaround. A regression test based on the advisory should cover a page and revision owned by the caller, a revision that exists on another page, and a revision ID that does not exist. The first should render the expected page data. The second and third should not disclose the foreign body or comments and should follow the same fallback path. Repeat for comment reads and for comment create and update operations. Check a read-only account and share-link behavior as separate cases. Do not assert that an empty application log proves no read occurred. References JVN#51223238 GROWI advisory GHSA-m72c-xgjf-56ww GROWI v8.0.7 release OSV CVE-2026-105272

Powered by Readybot.io

**DEV Community** ## Regression tests for GROWI-style revision authorization Regression …

DEV Community

My ISP would not give me a public IP, so I stopped asking for one

My ISP would not give me a public IP, so I stopped asking for one I once spent an evening forwarding port 443 on a home router, only to find the router's "WAN address" was a private range. My provider shared one public IPv4 address between many customers. No port forward on my side could ever work, because the traffic never reached my router. That setup is called carrier-grade NAT, or CGNAT, and it is common on home and mobile connections. The usual answer is a tunnel: a small program on your server dials out to a provider, and visitors come in through the provider. Levelrail, the self-hosted deploy platform I work on, can run a Cloudflare Tunnel connector for you. This post covers what I could verify and what I could not, because the honest answer on that second part matters for your decision. TL;DR Question Answer Verified how Need an open inbound port? No, the connector dials out Cloudflare docs Does Levelrail run the connector? Yes, one cloudflared container I ran it locally Did a real tunnel carry traffic? Not tested by me No Cloudflare account used Is the feature finished? The docs call it experimental Levelrail docs Do I still need a certificate? Yes, use DNS-01 Levelrail docs What I did not test, up front I did not create a Cloudflare tunnel, did not route a hostname through one and did not load a page over one. A real tunnel needs a token from a Cloudflare account, and I did not use one. The Levelrail docs say the same about themselves: the feature is built and covered by reconciler and API unit tests, but has not been proven on a real host with a real Cloudflare account. Everything about live traffic below comes from the docs, not from me. How a tunnel gets around CGNAT Cloudflare describes its tunnel as an outbound-only connection model, and says it connects resources without a publicly routable IP address. Its guidance for firewalls is to allow only those outbound connections and block inbound traffic. That is exactly what CGNAT permits: your server can reach out, and nobody can reach in. Public hostnames are routed on Cloudflare's side. In the Zero Trust dashboard you add a published application, pick a subdomain and domain, and set a service URL that the connector can reach. The domain must already be on Cloudflare, and the application is publicly reachable until you add an Access policy. What Levelrail adds Levelrail starts one container named -cloudflared from cloudflare/cloudflared, running tunnel --no-autoupdate run, and passes your tunnel token as the TUNNEL_TOKEN environment variable. The token is stored encrypted and is never returned by the API or CLI (docs/cloudflare-tunnel.md). Levelrail does not configure Cloudflare for you. The hostname-to-service mapping is yours to set in their dashboard. The commands, once you have a real token: levelrail-cli cloudflare-tunnel set --cf-tunnel-token TOKEN
levelrail-cli cloudflare-tunnel get
The feature is off by default and has to be switched on first. I did it with the APP_EXPERIMENTAL=cloudflare-tunnel seed on a local dev instance, and the docs also describe a dashboard switch in Settings. What I ran, with a fake token To see the reconciler work without an account, I gave it a deliberately invalid token on a throwaway local control plane (a build of the v1.0.1 release branch, on a Mac with Docker). The container appeared, pinned to cloudflare/cloudflared:2026.10.7, with the command tunnel --no-autoupdate run, no host port bindings, and restart policy no. $ levelrail-cli cloudflare-tunnel get
enabled: true
has_token: true
status: connected

$ docker logs Levelrail-cloudflared
Provided Tunnel token is not valid.
Two things stand out. First, no inbound port was published, which matches the pitch. Second, the st...

Powered by Readybot.io

**DEV Community** ## My ISP would not give me a …

DEV Community

HTTP 200, status 0, nothing wrong: teaching an API to tell empty from broken

I'm Eric Gitangu. I work on the backend of a mobility/fintech platform. This is one problem from the last two weeks: an API that answered "nothing nearby" and "the store is down" with the same shape, and how I taught it the difference without breaking a single installed app. The problem The mobile API wraps every answer in an envelope. The HTTP status is 200 and a status field inside says whether it worked: 1 is success, 0 is failure. That contract is older than most of the code around it, and the apps in people's pockets depend on it. A monitor watches for silent failures: 200 on the wire, status 0 in the body. When I read a few days of its log, a lot of what it had caught was not failure at all. A driver with no rides nearby. An empty queue. An empty document list. A setting nobody had configured yet. Each one answered status 0, because at some point "no results" was written down the error path. Three things follow from that: The app can't tell "nothing here" from "something broke". Both arrive as failure. The monitor counts ordinary days as failures. An alert that pages on a normal state teaches people to ignore the alert. A real fault hides in that noise. Same shape, same count. The decision The textbook fix is real HTTP status codes. Not available: installed builds read the envelope, and I can't recall an app from someone's phone. So the rules were: No existing key, header or status changes for a client that did not ask for it. Status 0 splits into three classes: normal state (nothing to show), refusal (you may not, and here is why) and fault (we broke). Every status-0 answer carries a stable, machine-readable code. Classification happens in one place, not at every call site. One choke point A hook sits at the last point before the response leaves. If the answer is JSON with status 0 and no code, it looks the message up in a catalog and adds a code and a reason. It only adds. It also publishes the code in a response header, so the access log and the monitor see it without parsing bodies. CATALOG = {
"No rides available": ("no_results", "normal"),
"Account not verified": ("unverified", "refusal"),
"Could not read store": ("store_fault", "fault"),
}

def classify(request, body):
if not is_app_request(request) or body.get("status") != 0:
return body, None
field = "error_code" if request.route in ECHOES_ONE_TIME_CODE else "code"
if field not in body:
code, _ = CATALOG.get(body.get("message"), ("unclassified", "unknown"))
body = {body, field: code} # add only: no key removed or changed
code = body[field]
return body, code if code in STABLE_CODES else None # header gets stable codes only
A complete, runnable version with the baseline test is in this gist. The monitor then counts only what is unclassified or uncoded. Everything else is a known answer with a name. To stop the pile growing back, a test walks the source for status-0 sites and compares them with a committed baseline. A new site with no code and no catalog entry fails the build. The debt can shrink. It cannot grow. Three things that bit The hook was too greedy. Payment providers call routes on the same API, and a provider's callback body is the provider's contract, not mine. The classifier now applies only to requests that carry the app's session token header, and provider routes are excluded by name. That fix went in the same day as the hook. code was already taken. Several endpoints return a one-time code in a field called code, and installed builds read it as exactly that. Put a refusal code there and an installed build reads it as the one-time code. On those routes the refusal goes in error_code, and the header copies only values from the stable list, so nothing that isn't a stable code reaches the access log through it. The version gate could never open. The plan was to answer normal states with status 1 only for app builds at or above a minimum version, so older builds kept today's behaviour...

Powered by Readybot.io

**DEV Community** ## HTTP 200, status 0, nothing wrong: teaching …

TechCrunch

The tech products we enthusiastically recommend this holiday season

We've been reviewing a bunch of new gadgets at TechCrunch this year, and we have some clear favorites to share for your Black Friday shopping and beyond.

Powered by Readybot.io

**TechCrunch** ## The tech products we enthusiastically reco

TechCrunch

TechCrunch Mobility: The Nvidia backup plan for China’s self-driving industry

Welcome back to TechCrunch Mobility, your hub for the future of transportation and now, more than ever, the role AI is playing in it.

Powered by Readybot.io

**TechCrunch** ## TechCrunch Mobility: The Nvidia backup pla

DEV Community

AI Receptionist Pricing in 2026: 10 Providers Compared at 100, 300 and 1,000 Calls

I build AI phone agents for small businesses, and the first question is always the same: what will this actually cost me per month? Vendor pricing pages make that hard, because everyone meters differently: minutes, calls, "unique customers", credits. So I put 10 providers on one page and worked out the bill at three call volumes. Every price below comes from the vendor's own pricing page, checked on 13 November 2026. The full version with sources is on the OpsThrive guide. The short answer Packaged AI receptionist: $29 to $599 a month (Dialzara, Goodcall, My AI Front Desk, Rosie, NextPhone) Hybrid AI + human: $300 to $2,100 a month (Smith.ai) Voice platform you build on: about $0.08 to $0.16 per minute (Bland, Vapi, Retell) Human receptionist: median $18.27/hour, $38,010/year (US BLS, May 2025), about $3,170 a month in wages alone Cheapest plan per provider Provider Cheapest plan Metered by Setup fee Dialzara $29/mo (60 min) Minutes, overage $0.35-0.48/min None Rosie $49/mo (250 min) Minutes Not listed Goodcall $79/mo (100 customers) Unique customers Free demo agent My AI Front Desk $99/mo, $79 annual (200 min) Minutes + credits, $0.25/min over Not listed NextPhone $199/mo, $166 annual Unlimited calls None Smith.ai $300/mo (30 calls) Calls, AI + humans None Bland $0 platform fee $0.14/min + telephony Your build Vapi $0 to start $0.05/min hosting + components Your build Retell $0 to start $0.055/min + components Your build Synthflow From $30,000/yr Enterprise Quote What you'd pay at 100, 300 and 1,000 calls a month Assuming 4 minutes per call. Where a vendor doesn't publish overage, I used the next plan up. Route 100 calls (400 min) 300 calls (1,200 min) 1,000 calls (4,000 min) Dialzara $180 $419 $1,399 Goodcall $79 $129 $299 My AI Front Desk $149 $349 $1,049 Rosie $149 $299 overage not published NextPhone $199 $199 $199 Smith.ai $915 $2,100 $8,050 Bland (custom agent) $56 + telephony $168 + telephony $560 + telephony Vapi / Retell (custom agent) $32-64 $96-192 $320-640 What I'd take away from this Packaged plans are cheapest to start, platforms are cheapest to run. Under ~150 calls a month, Goodcall or Rosie is hard to beat. Past a few hundred calls, per-minute platforms win, but someone has to build and maintain the agent. Overage is the real bill. Dialzara's $0.35-0.48/min above plan is 3-6x the platform rates. Size the plan for your busiest month. "Unlimited" removes the guesswork. NextPhone at a flat $199 is the simplest budget at high volume. Humans cost per call, AI costs per minute. Smith.ai's $7-10 per call is what a person in the loop costs; it doesn't scale like AI minutes. Telephony is extra almost everywhere on the platforms: a number plus carrier minutes (Twilio, Telnyx, Vonage). If you're comparing these for your own business, the full guide has the per-plan tables, platform component rates and the FAQ: AI Receptionist Pricing in 2026.

Powered by Readybot.io

**DEV Community** ## AI Receptionist Pricing in 2026: 10 Providers …

DEV Community

Agents Don't Replace Headcount — They Replace It With Liability

This week the whole industry converged on a single story: agents are the new workforce. Microsoft's Satya Nadella vision of "20 million AI agents for 200,000 employees" is, per this week's coverage, "becoming a reality." OpenAI shipped ChatGPT Wallet so agents can pay for things on your behalf. Google rolled out Antigravity multi-agent swarms. Anthropic put Claude Meetings live inside your calls. The pitch is consistent and seductive: grant the machines autonomy, and the humans you no longer need get "redeployed to higher-value work." Here is the line the keynote skipped. This same week, the autonomy we already shipped produced a very specific, very expensive class of failure — and almost none of it was caught by the agents. It was caught by humans doing incident response, usually after the data was already public. Three receipts from one week 1. The apps that shipped without auth German security firm Crissy Field, working with Handelsblatt, examined 159,041 Lovable projects. They found 24,664 sites with Supabase databases readable without authentication — including 200,000+ private chat messages, 80,000+ user profiles, and over 1,000 passport records. The agent generated the app. Nobody generated the access control. This is the defining failure mode of "just ship it": the scaffolding is free, and so is the liability. 2. The agent toolchain is now an attack surface Cursor disclosed that attackers used an access key stolen from Fireworks AI to publish two trojanized copies of Anthropic's Claude Code extension on Cursor's own marketplace. They stayed live for about 11 hours and may have reached up to 5,000 developers. The moment your "agentic coding" workflow becomes the load-bearing part of your pipeline, it becomes a target worth compromising. 3. Autonomy at internet scale is unguarded by default A researcher found roughly 35,000 Grok Build apps wide open to anyone. Cloudflare reported that traffic from AI penetration-testing and exploitation tools is up more than 30x since October 1, with 12,000+ repos descended from the open-source ARTEX agentic pen-testing system. The offensive tooling is agentic too — and it moves faster than the defensive tooling does. The part nobody wants to budget for Agents don't eliminate labor. They relocate it. Every unit of autonomy you grant is a unit of liability you accept, and somebody has to hold the pager for it. The "20 million agents for 200,000 employees" org chart has a hidden line item: the humans who babysit, audit, and clean up after the agents. You didn't reduce headcount. You changed the job description from "does the work" to "owns the blast radius." And blast radius doesn't show up on a productivity slide. It shows up as a 2am page, a regulator's letter, or a headline about 200,000 leaked chats. The questions the demos never answer If your agent writes the code, who signs off that the database isn't public?- If your agent can pay, who owns the fraud?- If your agent is a live participant in the meeting, who is accountable for what it says and what it drafts?- If your agent acts while you sleep, what is the worst thing it can do before a human notices?If the answer is "the agent" — that's not an answer. An agent cannot be accountable. Only people can. ## Autonomy is a dial, not a switch The honest counterargument: it's early, the tooling will improve, and a lot of this is vibe-coding-specific. Fair. But the trajectory is the point — we shipped autonomy faster than we shipped accountability. So treat autonomy as a dial you turn deliberately, action by action. For every capability you hand an agent, map three things: Reversibility — can you undo it cheaply, or is it a one-way door?- Blast radius — if it's wrong, how many people, records, or dollars does it touch?- Accountability — a named human who owns the outcome, not a team, not a model.Grant autonomy in proportion to how cheaply you can reverse the damage. Everything else is a demo, not a deployment. ## The uncomfortable concl...

Powered by Readybot.io

**DEV Community** ## Agents Don't Replace Headcount — They Replace …

DEV Community

How to connect Yahoo Mail to Claude and ChatGPT

To connect Yahoo Mail to Claude or ChatGPT, create a Yahoo app password, add your Yahoo mailbox to Fezola, then add Fezola to your assistant. Yahoo does not accept your usual password for other apps, so the app password is the one step people miss. Yahoo's servers come pre-filled in Fezola: you only enter your address and the app password. Yahoo's settings Read from Yahoo's IMAP help page, checked on 15 November 2026. Yahoo can change them, so check the page before relying on them. Server Port Encryption Incoming mail (IMAP) imap.mail.yahoo.com 993 SSL Outgoing mail (SMTP) smtp.mail.yahoo.com 465 or 587 SSL Table as of 15 November 2026. Username: your complete Yahoo address, including the domain. Password: an app password, not your account password. SMTP authentication: required. Fezola uses 993 for reading and 465 for sending. In the app, choose Yahoo and nothing else needs typing. Which assistant, which plan Assistant Custom connector Source Claude Free (one custom connector), Pro, Max, Ultra, Team, Enterprise Anthropic help page, checked 15 November 2026 ChatGPT Business and Enterprise/Edu; Pro with read and fetch permissions OpenAI help page, as cited in our ChatGPT guide Table as of 15 November 2026. Neither assistant ships a Yahoo Mail connector of its own, so a custom connector is the way in. A single connector that covers several services, like Fezola, fits the one custom connector of Claude's Free plan. Setup in six steps Create a Yahoo app password. Sign in to your Yahoo account, open Account security, then generate an app password (the Yahoo account security page is where it lives). Name it Fezola and copy the generated code. Create a Fezola account. It is free with 2 sources. Add the mailbox. In Sources, add a mailbox, choose Yahoo, enter your address and paste the app password. Test the connection. Fezola tests receiving and sending separately. Add Fezola to your assistant with https://api.fezola.com/mcp: see how to add a custom connector to Claude or how to add an MCP server to ChatGPT. Ask a question: "What came into my Yahoo inbox this week that needs an answer?" The short version is in the Yahoo Mail guide. What you can ask "Which unread emails did I get this week in my Yahoo mailbox?" "Find the invoice from my internet provider, in all my mailboxes." "Draft a reply to the last email from my bank." Attachments in PDF, Word, text and image formats can be read by the assistant: see read email attachments with AI. Guardrails Drafts by default. Replies are saved in your Yahoo Drafts folder and you send them yourself. Direct sending is off until you switch it on, mailbox by mailbox. Revocable access. The app password only opens your mail. Delete it in Yahoo's security settings and Fezola loses access, with your main password untouched. Which mailbox is which. Search results carry the mailbox name; the address shows when a message is opened. For the security side, read is it safe to give AI access to your email. Common errors Authentication failed: you used your normal password. Generate an app password and paste it instead. Connection refused: check that the username is the complete address, with the domain. For other providers, see AOL, Comcast, OVH and the IMAP connector page. Several mailboxes? See search all your inboxes with Claude. FAQ How do I connect Yahoo Mail to Claude or ChatGPT? Create an app password in your Yahoo account security settings, add your Yahoo address and that password to Fezola (the Yahoo servers are filled in), then add Fezola to your assistant with https://api.fezola.com/mcp. Claude accepts it on every plan, ChatGPT on Business, Enterprise and Edu plans. What are Yahoo Mail's IMAP settings? According to Yahoo's help page, checked on 15 November 2026: incoming imap.mail.yahoo.com, port 993, SSL; outgoing smtp.mail.yahoo.com, port 465 or 587, SSL, with authentication; username: your complete email address; password: an app password. Can I use my normal Yahoo password? No. Yahoo's page ...

Powered by Readybot.io

**DEV Community** ## How to connect Yahoo Mail to Claude …

DEV Community

Google security disclosure: Project Zero's fixed clock explained

Google security disclosure is getting a different clock. Project Zero's new ordinary policy starts the publication timer when its researchers report a vulnerability to the responsible vendor. An early patch leaves that timer running toward the same endpoint. For some early fixes, technical details can remain private longer than under the previous ordinary rule, even as Google prepares shorter disclosure windows for the future. TL;DR Current Full 90 ordinarily publishes technical details ninety days after reporting. Defined exceptions remain. The historical 2021 ordinary rule published details thirty days after a fix shipped within its deadline. Google's current policy example patches on day 47 and publishes on day 90. The historical ordinary rule calculates to day 77 for that example. A deadline-day fix loses the old extra month before technical disclosure. Fix timing therefore changes the direction of the comparison. Google plans shorter clocks in stages, while existing reports keep the policy that applied when they were submitted. How Google security disclosure now uses the report date Project Zero is Google's security research team. It sends a detailed technical report to the vendor or open-source project responsible for fixing a vulnerability. Its disclosure policy determines when those technical details become public in the team's tracker. The official FAQ describes the reporting and publication process. The November 13 announcement by Natalie Silvanovich introduces Full X. The current value of X is ninety. The dedicated policy page says the vendor has that interval to make a patch available to users, and the ordinary technical disclosure endpoint stays fixed regardless of when the vulnerability is fixed. That produces a simpler scheduling question for vendors: when did the report arrive? The answer establishes the ordinary publication date. Shipping a patch earlier still protects people who install it, but it no longer starts a separate ordinary disclosure timer. This governs Project Zero's own reports. Other research teams and vendors can have their own workflows. Reading this policy as a universal obligation for every vulnerability report would lose the scope the team describes, including its expectations about report quality and demonstrated security impact. What the historical post-patch rule did Tim Willis's official 2021 policy table supplies the historical comparison. For an ordinary issue fixed within ninety days, technical details were published thirty days after the fix. The team called that period patch adoption time. The practical intention was to leave time for an available update to reach users before researchers released a public technical explanation. That explanation can help defenders assess risk, understand a fix and investigate related weaknesses. The historical article also acknowledged the defensive costs of waiting for details, including prioritizing deployment and testing patch efficacy. The shorthand "90 plus 30" can obscure the early-fix case. A vendor fixing a bug well before its deadline started the post-patch interval at that early fix. Earlier patches could bring disclosure forward within that combined window. Historical grace periods and actively exploited cases had additional rules, so this article keeps the comparison to the ordinary fixed-within-deadline case. This also explains why removing a post-patch interval does not necessarily move every publication earlier. The event that starts the clock changes. That is the important part of the diff, and it is easy to miss when a headline only lists the length of the window. A patch release and a protected user are different milestones Silvanovich defines the patch gap as the time between a security fix being released and the user installing the relevant update. An available patch can sit between a vendor's release process and the systems that need it. Publication timing cannot tell you that every installation has reached the fixed version...

Powered by Readybot.io

**DEV Community** ## Google security disclosure: Project Zero's fixed clock …

DEV Community

Jensen Huang is gone, and Nvidia has to explain itself on Tuesday

I've been following Nvidia for years and I still don't have a clean way to describe the last two weeks. So here's the plain version. Jensen Huang resigned as CEO on November 12. Not a handover, not a planned transition, just gone, effective immediately, after 33 years running the company he co-founded in 1993. Colette Kress, the CFO since 2013, is interim CEO and is somehow still doing the CFO job at the same time. What ended it wasn't a product miss or a bad quarter. It started on October 29, when the New York Times reported on Huang's relationship with a businesswoman, Alice Huang, whom US intelligence suspects of being a Chinese spy. More reporting followed: an affair with a senior Nvidia employee, and another with someone who reported directly to him. The board opened an independent investigation, and by November 12 it was done. I want to be careful here. A suspected spy is not a proven spy, and an affair is not automatically a national security breach. Nobody has shown that anything was leaked. But the board decided it couldn't wait for that answer, and once the second relationship with a direct report surfaced, sentiment turned fast. Someone familiar with the decision told CNBC the board worked late into the night and made the call that morning. What the market did The stock got hammered. Nvidia shed more than half a trillion dollars in market value at the worst point and closed November 13 at $162.85. That's about 24% below where it sat before the first story, and roughly 33% off its 52-week high of $243.37 from October 6. November is now on track to be Nvidia's worst month since ChatGPT kicked off the boom in late 2022. Nine investment houses downgraded it. Bernstein halved its price target to $200 and moved to market-perform; Stacy Rasgon called the stock cheap but warned more allegations could still surface. Oppenheimer went to perform and floated a "brain drain," naming AMD, Marvell and Broadcom as the beneficiaries. Stifel kept its buy rating but cut the target hard, from $315 to $205. Meanwhile Elon Musk and a pile of other tech execs signed a petition asking Nvidia to bring Huang back. It passed 115,000 signatures. Two investors holding about $2.13 billion of stock are pushing the board to at least consider his return. And Trump, predictably, praised him and floated Tim Cook as the replacement. Tuesday's earnings call is the real test Nvidia reports its fiscal third quarter on November 17, after the close. The numbers will probably look great: revenue around $109 billion, up roughly 50% from a year ago, and earnings per share near $2.47. But this is the first earnings call in the company's 27 years as a public company that Jensen Huang won't run. Kress has to make the case five days into the job, while still being the CFO. I don't envy her. The quarter itself is almost beside the point. Everyone wants to know who runs Nvidia next, what happens to the roadmap over the next year or two, and whether the "Jensen premium" was a real thing that just got deleted. UBS also flagged that Nvidia might pause part of its funding of AI companies, which would pull something like $50 billion out of the ecosystem, plus the risk of tighter chip export rules on top. What actually matters underneath Huang has said he's willing to testify before Congress before Thanksgiving. Senator Elizabeth Warren's reply was blunt: resigning doesn't absolve him or the company of responsibility for putting national security at risk. There are already two congressional investigations into Nvidia's China sales. Separately, a former OpenAI researcher claims GPU fingerprinting shows models from DeepSeek, Moonshot, MiniMax and Ant were trained on Blackwell chips, despite the export controls meant to stop exactly that. Take those together and the headline isn't really one CEO's personal life. It's whether the most important company in the AI supply chain can answer hard questions about who it sells to and what it knows. The leadership mess just dragg...

Powered by Readybot.io

**DEV Community** ## Jensen Huang is gone, and Nvidia has …

DEV Community

How to get more App Store reviews without asking unhappy users

This guide was originally published on the Hailbell blog. Short answer: Ask right after a success moment: the user finished something, hit a milestone, or, best of all, you just solved their problem in support. Use the system prompt (requestReview on iOS, the In-App Review API on Android), because Apple allows no custom review prompts and both platforms limit how often theirs appears. Give unhappy users an easy way to reach you instead, such as a support chat or a visible contact link, so a complaint reaches you before it becomes a one-star review. Don't ask "Do you like the app?" first, and never offer anything for a review. Most apps have very few reviews. This month, a developer who runs a directory of indie iOS apps counted 757 apps updated in the last 90 days; 562 had under 5 US ratings (r/iosdev). Their question is the one this page answers: what got you your first ratings? Hailbell is our product, built by an indie app developer who uses it in his own apps; one section near the end says where it helps. Everything before that needs no tools. What Apple and Google allow Checked November 9, 2026, on developer.apple.com (Review Guidelines last updated October 16, 2026), developer.android.com and Google Play's policy center. Apple Use the system API to ask for a review: guideline 5.6.1 says Apple will "disallow custom review prompts". For someone who hasn't rated your app on that device, the prompt shows at most three times in 365 days. The system decides whether it appears, you can't change its look or text, and people can turn it off entirely. SKStoreReviewController is deprecated since iOS 18; use AppStore.requestReview(in:) or SwiftUI's requestReview environment action. Google The In-App Review API shows a card over your app. Google doesn't publish the quota and says it can change without notice; calling it more than once in a short period (for example, under a month) might show nothing. You can't restyle the card or cover it. Both Paid, incentivized or fake reviews are banned. Apple's guidelines also name "filtered" feedback as manipulation, and Google says not to ask any question before the card. Breaking these can get the app removed. 1. Give unhappy users a way to reach you first This keeps your average up. Someone whose purchase didn't unlock will write a review if that's the only place to complain. Apple says this on its own ratings page: make your support contact easy to find in the app and on your product page, so you can fix bad experiences "that may otherwise lead to poor reviews". That means a "Contact support" row in Settings, a help link on error screens and in restore purchases, and a support URL on your listing that a person answers. This is for everyone, not a filter. Happy and unhappy users see the same support link and the same review prompt. You're just giving everyone a quicker way to get help than a one-star review. 2. Pick the moments to ask Apple's guidance is to ask "when users are most likely to feel satisfaction with your app, such as when they've completed an action, level, or task", and not on first launch, during onboarding or in the middle of a task. Google says to wait until someone has used the app enough to give useful feedback. Good moments look like this: A recipe app: after someone marks their third recipe as cooked, back on the recipe list. A budgeting app: after the user closes out their first full month and sees the summary. A language app: after a 7-day streak, on the lesson-complete screen. A photo editor: after a successful export, once the share sheet is dismissed. Any app with support: right after you fixed someone's problem and they said thanks. More on this in step 5. Bad moments: after a crash or error, after someone closed your paywall without buying, mid-typing, or on launch. Apple's sample code also waits until the task is done several times, asks once per app version at most, and pauses a few seconds on the done screen. 3. Call the system prompt at those moments On i...

Powered by Readybot.io

**DEV Community** ## How to get more App Store reviews …

DEV Community

How to talk to users who leave at the paywall without paying

This guide was originally published on the Hailbell blog. Short answer: Put a small "Questions?" link next to the paywall that opens a chat inside the app, answer within minutes, and look at what the person already did before you reply. Make an offer only when price is the reason, and follow up once with a push at a decent local time. If you're a solo developer with a few hundred new users a week, a support email link on the paywall that you answer yourself is enough to start. If you use Superwall or RevenueCat and want in-app chat, purchase history, offers that open your paywall and push in one SDK, Hailbell does that from $29 a month. If you're a funded team that also needs a full help center and many channels in one inbox, Intercom is the better fit. Hailbell is our product, built by an indie app developer who uses it in his own apps. This guide works with or without it. The steps come first, and the section on where Hailbell fits, including what it doesn't do, is near the end. Why most people leave the onboarding paywall Superwall analyzed more than 40 million onboarding paywall opens from February to May 2026. Single-page paywalls converted 9.07% of viewers into a trial or purchase, and multi-page paywalls 12.41% (Superwall; paywalls with no sales were left out). Put the other way, about 9 in 10 people close it. Most paywall advice is about the other 10%: layout, trial length, A/B tests. That work matters. But the people who leave usually aren't saying "no forever". They leave with something unresolved: a question the paywall didn't answer ("does it sync to my iPad?", "can I cancel the trial in settings?", "is there a student price?") a price that's a bit too high for them, or a plan length they don't want a bug or a confusing step in onboarding that made them doubt the app no reason yet to pay today The first three can be fixed in a conversation. The fourth can't, and you shouldn't try. The trouble is that most apps give these people no way to talk to anyone. They close the paywall and you never hear from them. The same problem shows up later from the cancel side. A founder on r/SaaS was weighing a stricter refund policy: refunds within a week of each charge for now, maybe no refunds at all after the first month. A rule like that decides who gets money back. It doesn't tell you why they wanted out, and a conversation does. Seven steps to talk to the people who didn't buy Record that they left. When someone closes the paywall without buying, log an event such as paywall_decline with the placement name. Superwall's presentation handler and RevenueCat's paywall views both call your code when the paywall is dismissed, so you can tell whether it ended in a purchase. Without this event you can't tell the difference between "never saw the paywall" and "saw it and said no". Give them a way to ask, next to the paywall. A small "Questions? Chat with us" link under the plans, or a quiet help button after they close it. Don't make it a popup that blocks the screen. An email link works too, but people rarely switch to their mail app in the middle of onboarding, so a chat that opens inside the app gets more messages. Answer while they're still in the app. A reply four hours later reaches someone who already moved on. Write a short note with your prices, plans, trial terms, refund policy and known issues, and let an AI answer first from that note, labelled as AI. It should hand the chat to you when it doesn't know. You finish the hard ones from wherever you already are during the day. Read what they did before you reply. Did they start a trial last month and cancel? Get a refund? Close this paywall three times today? Is a billing retry failing? A person who closed the paywall three times probably wants it, and the price or a missing answer is in the way. Your purchase tool already has this history; put it next to the conversation so you don't reply blind. Make an offer only when it fits the reason. If they say "too expensive", send...

Powered by Readybot.io

**DEV Community** ## How to talk to users who leave …

DEV Community

AgentOps Center: running multi-agent systems with OpenTelemetry and SigNoz

A multi-agent workflow takes 45 seconds to answer, no exception was thrown, and every dashboard is green. What do you do next? That operations question is what AgentOps Center sets out to answer. It is a small GitHub project built for a SigNoz hackathon. I read it file by file without deploying it, so this article keeps three things apart: what the code does, what the README claims, and what I could not check. The repository is more useful as a pattern than as a product. It shows how to name spans in a system of cooperating agents, where to send them, and how to make an assistant’s answer depend on real telemetry. We will walk through those three decisions, then the limits. A note on method: the last commit I read is dated 26 July 2026. I quote no star count because I did not read one from the source. The README says MIT, yet I found no LICENSE file in the tree, so check before reusing anything. The problem AgentOps Center targets: slow is not broken Classic APM counts requests and HTTP errors. An agent workflow behaves differently. It loops, replays a tool, or waits on a model provider that is rate limiting it. Nothing breaks, everything slows down, and the token bill climbs. The README lists the questions operators need to settle: which node looped, which tool returned a rate limit, what each sub-agent cost. Answering them takes spans per agent, per model call and per tool, all linked in one trace. That is what the code builds. Decision 1: how to name spans in a multi-agent system The file backend/instrumentation/agent_spans.py declares attribute constants. It reuses the gen_ai. keys for model calls, then adds house keys for agents and tools. The split is sensible. Standard keys let you compare models and providers, while house keys carry business logic: which agent, which workflow. A workflow identifier on every span then groups everything together, even when the spans come from different libraries. To see what a span carries, read the anatomy of an OpenTelemetry span. Two cautions apply: The OpenTelemetry GenAI conventions are moving quickly. The specification page now points to a dedicated repository that I could not read (the server returned an error when I checked). gen_ai.system may have been replaced by a newer provider attribute: unverified. gen_ai.usage.cost_usd is the project’s own invention, not a key I know to be standard. Dashboards built on it will stay specific to your code. One more design detail: gen_ai.prompt and gen_ai.completion are cut to 500 characters. Full prompts in spans are expensive and expose data, so truncation is a fair compromise. Still, set your own policy before production. See also OpenTelemetry attributes in Dynatrace. Decision 2: where the telemetry goes The setup.py module creates a tracer provider and a meter provider, exporting over OTLP gRPC to localhost:4317 by default, which OTEL_EXPORTER_OTLP_ENDPOINT overrides. It turns on FastAPI and httpx instrumentation, plus LangChain through OpenInference, which also covers LangGraph according to a code comment. Sampling is fixed at 100 %, flagged in the code as a demo setting. The repository’s Collector receives OTLP and writes straight to ClickHouse using SigNoz exporters, one pipeline per signal. This layout is for self-hosted SigNoz. With another backend, Dynatrace for instance, only the exporter changes: the OTLP protocol and the attributes stay. Our OpenTelemetry Collector to Dynatrace guide shows that variant. I have not tested this repository against Dynatrace. For operations, note the memory_limiter and the batch of 10,000 items every 10 seconds. Good instinct: an agent system can emit thousands of spans per run. The README’s smoke tests report between 2,340 and 3,559 spans per run. Decision 3: let an assistant diagnose, but only from evidence This is the most interesting idea. The Root Cause Copilot does not ask a model cold. An evidence engine first calls the SigNoz MCP server to fetch traces, error logs, metrics and alerts. I...

Powered by Readybot.io*

**DEV Community** ## AgentOps Center: running multi-agent systems with OpenTelemetry …