Skip to main content
ngockhoi96

Stream

A running feed of short notes, TILs, and links worth keeping — lighter and more frequent than the blog.

  1. Link

    RTK: the hook that quietly trims your agent's token bill

    RTK (Rust Token Killer) is a CLI proxy that compresses the output of your dev commands before it ever reaches the model. The trick: one Claude Code PreToolUse hook silently rewrites every shell call — git status becomes rtk git status — so you change nothing and pay zero token overhead. Two days in, it’s already shaved 34% off my command output, with git diff alone landing at 49%.

    How it disappears into your workflow. You wire it once — a PreToolUse matcher on Bash that pipes through rtk hook claude — and from then on every command is rewritten transparently. Under the hood RTK strips whitespace, truncates long lines, and groups output by file, so the model sees the signal without the slop. The agent never knows; it just gets leaner tool results.

    The receipts. rtk gain keeps the score:

    Terminal window
    rtk gain
    RTK Token Savings (Global Scope)
    ════════════════════════════════════════════════════════════
    Total commands: 115
    Input tokens: 46.3K
    Output tokens: 30.6K
    Tokens saved: 15.8K (34.2%)
    Total exec time: 1.5s (avg 13ms)
    Efficiency meter: ████████░░░░░░░░░░░░░░░░ 34.2%
    By Command
    ───────────────────────────────────────────────────────────────────────
    # Command Count Saved Avg% Time Impact
    ───────────────────────────────────────────────────────────────────────
    1. rtk git diff 3 9.0K 49.4% 4ms ██████████
    2. rtk grep 20 1.1K 21.0% 2ms █░░░░░░░░░
    3. rtk ls -la docs/adr/ 2 898 55.6% 1ms █░░░░░░░░░
    4. rtk git diff HEAD 1 884 10.1% 10ms █░░░░░░░░░
    5. rtk ls -la . 2 735 73.4% 1ms █░░░░░░░░░
    6. rtk ls -la apps/site/... 2 390 68.2% 2ms ░░░░░░░░░░
    7. rtk ls -la /home/ngoc... 1 344 73.3% 2ms ░░░░░░░░░░
    8. rtk ls -la src/assets... 1 303 66.7% 1ms ░░░░░░░░░░
    9. rtk ls -la ../vi/ 1 195 68.2% 2ms ░░░░░░░░░░
    10. rtk ls -la apps/site/... 1 171 73.1% 2ms ░░░░░░░░░░
    ───────────────────────────────────────────────────────────────────────

    Why 34% and not the marketed 60–90%. That number is a blended floor, not a verdict. Cheap commands like git status compress to almost nothing; git diff saves 49%, ls around 70%. And two days in, only ~115 of my ~4,800 recent commands have routed through the hook — so rtk discover still sees ~387K tokens sitting unrouted. What grows with time isn’t the percentage, it’s the coverage.

    Worth knowing. RTK doesn’t wrap everything yet — bun run, pkill, and bunx still pass through raw. Run rtk discover on your own history to see where your tokens leak. One gotcha: there’s a name collision with another rtk (Rust Type Kit) — if rtk gain errors out, you’ve got the wrong binary.

    • ai
    • claude
    • workflow
  2. Note

    Colony (2026): Train to Busan's director gives zombies a hive mind

    Train to Busan has stuck with me since 2016, so when I saw Yeon Sang-ho had a new zombie film out I went the same afternoon. Colony (군체, 2026) earns the trip. The hook that got me: these aren’t a shambling mob. They share one mind across a fungal network and learn — getting smarter after every attack you live through.

    The setup. A disgraced scientist calls in a bioterror threat on a biotech firm, then corners the CEO alone in his office and injects him with a weaponized slime-mold. The CEO bolts for the door, transforms out in the hallway, and starts biting; within minutes the infection is tearing through a building that’s now sealed shut. Our way through the chaos is Kwon Se-jeong (Jun Ji-hyun), a biotech professor who reads the outbreak faster than anyone around her and pulls a knot of survivors toward an exit that keeps moving.

    Why it’s not your usual horde. The infected are networked. Through a mycelium web they share senses in real time, so what one sees, all of them know — and they adapt. Fail to put one down and the swarm files it away: next time they mimic a voice to draw you out, cover each other’s blind spots, even turn the building’s own tech against you. That’s the engine of the film, and it’s a genuinely fresh idea in a genre that mostly reuses the same slow mob.

    It’s also funny. Not in a winking, gore-undercutting way; the laughs come from the premise played straight. My favorite: the swarm learning to piggyback, climbing on each other to reach what they couldn’t alone. It’s absurd and a little horrifying at once, and the theater laughed right in the thick of the tension. A good film makes you laugh at the right moment without reaching for a cheap joke. This one does.

    The verdict. It’s predictable. You’ll see most beats coming. The one real twist — a flashback linking Se-jeong to the man behind it all — didn’t fully land for me on a first watch. None of that spoiled it. The film is tense, fast, gory, and flat-out fun, and it was worth the ticket. And the way it leaves things, I’d bet on a Part 2; nothing’s been announced, but the door is wide open. The deeper stuff — what works, what doesn’t, the human psychology, and what I think it’s really saying — I’m saving for a full write-up.

    • film
    • review
    • life
  3. Link

    Astro 7 is here — a Rust compiler, Sätteri, Vite 8, and the one default I'm opting out of

    Astro 7 shipped, and it’s an all-out Rust speed pass — 15–61% faster builds. The headline bits, quickly, then where I land for this site.

    What’s new (short version — full details in the post):

    • Rust .astro compiler (built on oxc + Lightning CSS, drops the old Go one) — stricter, JSX-style validation, no more silent HTML auto-correction.
    • Sätteri, a new Rust Markdown/MDX processor, is now the default — native GFM, math, frontmatter, and directives, no JS plugin chain.
    • Vite 8 + Rolldown — a Rust bundler replacing both esbuild and Rollup, 10–30× faster.
    • Advanced Routing — a src/fetch.ts entry for full request-pipeline control (Workers / Deno / Bun, Hono-friendly), now the default.
    • Route caching stable + an experimental Cloudflare CDN cache provider.
    • AI-dev niceties: astro dev --background, coding-agent detection, JSON logs.

    My take — what impresses me, and what I’ll skip:

    • The part that makes me grin: Sätteri becoming the default is exactly the option I already rejected. This site runs a custom unified remark/rehype chain on purpose (ADR-0027 in the repo) — remark-note-fold powers these very Stream read-mores, plus directive plugins and section-wrapping rehype. Sätteri is Rust-fast but it isn’t my pipeline, so I’ll keep markdown.processor: unified(...) and opt out. Rare and funny when the framework’s default moves away from your considered choice — and you stand pat.
    • Genuinely tempting for my stack: src/fetch.ts (I deploy to Cloudflare Workers, so full pipeline control is appealing) and the edge route-caching (could front my D1/KV view + like reads at the CDN).
    • Cautious: Vite 8 / Rolldown — I pin Vite 7 deliberately right now; the speed is real, but I’ll let the Rolldown dust settle before jumping.

    Want the benchmarks and migration notes? Read the post.

    • astro
    • web-dev
    • build-tools
  4. Link

    CSS: Unavoidable Bad Parts — a list of gotchas that's secretly a love letter to the platform

    matklad catalogues CSS’s traps — too many wrapper divs, inconsistent browser defaults, box-sizing, line-height “voodoo”, words that won’t break on mobile. A cu em dropped it in Discord and the title hooked me. But read past the grumbling and it’s the opposite of a CSS hit-piece: it’s a list of reasons to lean harder into the platform.

    The through-line is basically my creed: semantic HTML (main / article / nav / kbd / details / dl) so the markup carries meaning, classless CSS after a small reset, flexbox + CSS nesting for component-local styling, box-sizing: border-box as line one, and as few wrappers as possible — let the cascade and the document do the work. He even notes that responsive design is often unnecessary because HTML already reflows. The result: ~200 lines of readable, hand-written CSS, no framework, and he’s happy with it.

    Where I push back: his tone. It’s frustrated — “noob trap”, “voodoo”, a wish for a 100-page book so you don’t get “collapsed by the margins”. I read the exact same list as the fair price of a platform that lets you ship a fully styled blog in 200 hand-written lines with zero build-time framework. The gotchas are learnable once; the leverage lasts forever. They’re features wearing a warning label, not traps.

    Why I care this much about semantic HTML, native CSS, and the web platform — that’s its own /blog post, coming.

    • css
    • web-platform
    • semantic-html
  5. Note

    Badge vs tag vs chip vs pill: every library disagrees — here's the model I shipped

    A quick reference I keep needing: badge vs tag vs chip vs pill. Every UI library defines them differently — which is exactly why people argue about it. So here’s the fast version, then the model I actually shipped on this site.

    How the big libraries use the words

    • Badge — a tiny status/count marker. Chakra: “highlights an item’s status for quick recognition.” MUI: a supplemental count or dot tied to a host control (the “4 unread” on an inbox icon), plus a dot variant for simple Online/Unread states.
    • Tag — a label for categorizing or labeling content (Chakra).
    • Chip — MUI: “compact elements that represent an input, attribute, or action” — enter info, make selections, filter, or trigger actions. The interactive one.
    • Pill — not a component in these libraries at all; just the fully-rounded shape.

    Notice they don’t even agree with each other: MUI’s Chip ≈ Chakra’s Tag, and “badge” means a dot to one and a status label to another.

    The model I actually buy (via smart-interface-design-patterns)

    There aren’t four components — there are two roles + two shape words:

    • Badge = the static family — status / count / label about a host, non-interactive.
    • Tag = the family that can be static or interactive — a label, a link, or a filter.
    • Chip / Pill = shape adjectives only — “chip” is the compact rounded form, “pill” is fully rounded. Never component names.

    A #keyword’s role is chosen by what it does, not by what it looks like.

    On this site (CONTEXT.md → UI primitives; ADR-0025)

    I collapsed the lot into exactly two components:

    • One Tag with three states — static (inert labels on stream notes and lexicon term cards), link (navigates: the tag pages and post-detail tag list; brand-hue tech tags are just a colour sub-style of this state), and toggle (the multi-select filter on /lexicon).
    • One Badge — non-interactive metadata (the post category indicator), deliberately flat so it never reads as a button.

    And both are lightly rounded — not pills. The rule underneath: a non-interactive atom must never look clickable, and a clickable one must never look inert. Shape is cosmetic; role is the contract.

    Sources:

    The full write-up — the 3D tactile affordance tiers, why “static must look different from interactive”, and how one recipe serves three states — is getting its own /blog post.

    • til
    • design-system
    • ui
    • css
  6. Link

    Claude Code status line: session vitals at a glance

    A thin strip at the bottom of the Claude Code frame, driven by a shell script you own. Replaces the habit of manually checking which model you’re on, how much context is left, or whether you’re close to a rate limit — all things that “carry heavy decision weight” mid-session without you realising it.

    The script reads session JSON from stdin and outputs a single coloured line:

    Terminal window
    /home/ngockhoi96/workspace/github.com/anIcedAntFA/ngockhoi96.dev | main | Sonnet 4.6 [high] | ctx: 53% | 5h: 35% | 7d: 23%

    What each segment tells you

    • cwd — which project you’re in (dim, always there)
    • git branch — cyan, bold; no git call if outside a repo
    • model + effort — effort level only shown when it differs from the default (medium), so it stays quiet unless you’ve changed it
    • ctx % — context window remaining, colour-coded: green > 80 %, yellow > 70 %, red below; the signal to /handoff before the window runs out
    • 5h / 7d rate limits — highlights bright yellow at ≥ 70 % so you know when to slow down before hitting the wall

    Two gotchas the article calls out

    Use printf '%b\n' "$line" — not printf "$line\n" — or % in the output gets eaten as a format specifier. And add PATH fallbacks for jq since GUI launches don’t inherit your shell’s PATH.

    Setup: drop the script anywhere, point statusLine.command in ~/.claude/settings.json at it with an absolute path, restart Claude Code.

    • claude
    • workflow
    • tools
  7. Link

    Handoff skill: carry your session context anywhere

    Matt Pocock’s skill: at the end of a heavy session — a /grill-with-docs planning pass, a brainstorm, a full task breakdown — ask Claude to /handoff. It compacts the whole conversation into a structured markdown doc a fresh agent can pick up cold.

    The workflow: Opus 4.8 for the expensive thinking (grilling, architecture, ADRs, task list), then /handoff at the end. Next session, Sonnet picks up the handoff doc and implements. Two wins: cost stays sane (Opus only where judgement matters), and context survives anything — a new session, a different machine, home to office.

    Matt has a video walkthrough of the exact flow.

    A real handoff from this blog’s last UI-polish pass:

    # Handoff — UI-polish pass 5 (cluster G done, markup batch remaining)
    **Branch:** `refactor/pandacss` · **Repo:** `ngockhoi96.dev` (Astro 6 + Svelte 5 + PandaCSS)
    **Prior:** `/grill-with-docs` Opus pass locked 10 decisions. This session = Sonnet implementing
    cluster G (render pass + theme batch). Interrupted before markup batch.
    **Next session:** Sonnet — complete decisions A / B / C / E / H + doc updates + 6 atomic commits.
    ---
    ## What was completed this session
    ### Cluster G — render pass + theme batch ✅ (NOT yet committed)
    Approved values recorded, codegen run, dev restarted. Decision D (collapsible trigger)
    folded in. No further codegen needed unless you edit theme files again.
    ---
    ## Commit plan (6 atomic commits, in order)
    1. 💄 style(theme): surface/depth cluster — bg.base deepen, emerald glow, 2-rung shadow,
    term-card edge-border, collapsible trigger
    2. ♻️ refactor(stream,roadmap): badge consolidation — kindBadge + versionChip → <Badge>,
    drop public phase
    3. ♻️ refactor(i18n): lang-aware datetime helper + <FormattedDate> + min-read i18n key
    4. ♻️ refactor(engagement): copy-to-clipboard shared util, stream copy → iconButton
    5. 💄 style(lexicon): filter-bar root div → label (click-to-focus, no JS)
    6. ✨ feat(post-card): cover + headingLevel props, home/blog/roadmap rewire, heading a11y
    ---
    ## Suggested skills
    - **`/git-workflow`** — gitmoji + conventional commits for the 6 atomic commits.
    - **`/verify`** — eyeball Playwright after each cluster of commits.
    - **`/grill-with-docs`** — only if a new structural decision surfaces.
    - **`/handoff`** — write the final handoff once all commits are done.
    • ai
    • claude
    • workflow
  8. Note

    Nana (aka Trio): the Japanese memory game named after the number 7

    I went hunting for a board/card game to train memory, quick thinking, and pattern-spotting — something to play with friends, family, and my partner — and landed on Nana, 2nd edition (2–5 players). The cute, friendly art reeled me in; a skim of the rules (“flip cards, match three of a kind”) sealed it. Then I learned the nicest detail: nana (七) is literally “seven” in Japanese — and 7 is the game’s magic number.

    Where it comes from

    Nana is a Japanese memory-and-deduction card game by Kaya Miyano (art by Sai Beppu). The first edition is the lowercase nana; the more widely distributed 2nd edition is the one I bought. Because the Japanese print is hard to find internationally, it was reimplemented for Western markets as Trio — so if you’ve seen “Trio” on a shelf, it’s the same game.

    How you play

    The deck is dead simple: 36 cards, numbers 1–12, three copies of each. Everyone holds a hand kept sorted low→high; nine more cards sit face-down in a 3×3 grid in the middle. On your turn you flip cards one at a time, hunting three of the same number — a trio. Each reveal is either:

    • the lowest or highest card from any player’s hand (including your own), or
    • any face-down card from the middle grid.

    As long as each new card matches the previous number, you keep going. The instant one doesn’t, the cards go back face-down and your turn ends. Flip all three of a number and you claim that trio.

    That’s the whole loop — and it’s why it’s a memory game: every revealed-then-hidden card is information (who held the 9, which middle slot was a 4), and whoever remembers best cleans up.

    How you win — three paths, and they all orbit 7:

    1. collect three trios, or
    2. instant win if you collect the trio of 7s (the lucky seven the game is named after), or
    3. instant win if you collect two trios that add or subtract to 7 (e.g. 3 + 4, or 10 − 3).

    Why it fits what I wanted

    ~15 minutes a game, age 6+, teachable in a single round — but the memory and push-your-luck tension scale with how sharp the table is. Exactly the tư duy / rèn trí nhớ / nhanh mắt itch: light enough for family and a partner, mean enough to stay fun.

    I picked mine up on Shopee.

    • life
    • board-game
    • memory
  9. Note

    TIF: Today I Failed — and learned git bisect

    End-of-day check on develop: the site had lost nearly all its styles — CSS variables referenced across every component, defined nowhere. Staging was fine (QA still in progress, so: relief). TIF — Today I Failed to fix it before 9 pm. But also TIL: git bisect.

    The environment is a 20-member monorepo with merges flying in constantly. No obvious culprit, no idea which commit broke it. First instinct: git reset --hard backward one commit at a time, reinstall, spin up dev, check UI, repeat. Slow, manual, demoralising.

    My teammate suggested git bisect. I’d seen the name before but never used it — pulled up the docs and applied it on the spot.

    What git bisect does

    Binary search on git history. You give it a known-bad commit and a known-good one; it checks out the midpoint. You test, mark it good or bad, and it halves the remaining range. log₂(n) rounds to isolate the first bad commit instead of n manual resets. For our ~10-commit range: 4 rounds instead of 10.

    Terminal window
    # start bisect, mark current HEAD as bad
    git bisect start
    git bisect bad
    # mark the last known-good commit
    git bisect good <known-good-hash>
    # git checks out the midpoint commit
    # → test the build
    pnpm install --force
    pnpm run dev
    # check UI — still broken?
    git bisect bad # broken → first bad commit is in the upper half
    # or
    git bisect good # fine → first bad commit is in the lower half
    # repeat until git prints: "<hash> is the first bad commit"
    # return to original HEAD when done
    git bisect reset

    We found the first bad commit. Didn’t manage to trace why before 9 pm — called it a night. Develop only, QA unaffected, so it could wait until morning.

    The night also sharpened something I’ve been sitting with: a bug that survives to production is on QA; one caught in QA is on Devs; one that slips into develop unchecked is on the whole team — not just the author of that branch. I saw the risk, let it through anyway. It’s on me too. Full thoughts for another post.

    Sources:

    • tif
    • git
    • debugging
  10. Note

    Fitts's Law: bigger target isn't always better

    Came up while designing this blog’s post-card vs term-card split: Fitts’s Law — movement time to a target scales with the log of its distance-over-width ratio: MT = a + b · log₂(2D/W). The wider the target, the faster you hit it.

    D is distance to the target centre, W is its width in the direction of movement. Double the width and you shave one bit off the information cost of the movement.

    In UI/UX

    • Tap target size — 44 × 44 px minimum (Apple HIG, WCAG 2.5.8). Fitts is why.
    • Screen edges — the edge has effectively infinite width (cursor can’t overshoot it). The macOS menu bar, Windows taskbar, browser scrollbar — all free real estate. Fitts is also why macOS puts the menu bar at the very top of the screen, not the window.
    • Sticky / floating elements — back-to-top, FABs, sticky headers stay near where the cursor already is, cutting D.
    • Stretched card links — make the whole card one click target, not just the title anchor. A post-card does exactly this.

    The twist I got while designing term-card

    Fitts says “make the target bigger.” The obvious move on term-card: copy the post-card stretched overlay. Rejected — because the precondition fails on three counts:

    1. Conditional target. A post-card always has exactly one off-card destination. A term-card’s focus modal exists only for terms with a fuller body — a large slice of the catalog is body-less. Making a surface that opens nothing look clickable is a worse lie than a small dead zone.
    2. Same-page expand, not navigation. The modal re-presents the card’s own content; it doesn’t navigate elsewhere. A stretched link signals “go somewhere” — wrong mental model.
    3. Action density. A term-card carries copy · ref-link · post-link · tags · read-more. A stretched overlay forces every one of those above it and turns every gap into an accidental modal-open.

    The rule of thumb this produced: navigate-elsewhere, always-present, single → stretch; expand-in-place, sometimes-absent, or multi-action → explicit triggers.

    Fitts’s Law is a multiplier on target size — it only helps when there’s a clear, consistent target to multiply.

    Sources:

    • til
    • ux
    • psychology
  11. Link

    F8 — where I stumbled into coding 3 years ago

    F8 (“Learn to code, get a job”, by Sơn Đặng) — completely free web courses: HTML, CSS, JavaScript, all the way to ReactJS. Over three years ago, someone with no CS degree like me stepped into the field right here. Genuinely grateful — leaving it here for anyone standing at the starting line.

    • learning
    • web
    • vietnam
  12. Note

    Hick's Law: more options isn't always slower

    Digging into the principle behind the Slack note: Hick’s Law (Hick–Hyman) — decision time grows with the log of the number of choices: RT = a + b·log₂(n + 1). It’s why a 60-row dropdown makes you stall while 4 slots get picked instantly — it trims cognitive load.

    a is the “base” time (nothing to do with choosing), b is a context-dependent coefficient, and the +1 is a hidden decision: whether to respond at all.

    In UI/UX

    • Chunking — group 20 flat items into 4 groups × 5; choosing in tiers is faster.
    • Progressive disclosure — hide the rarely-used behind “Advanced” / “Show more” (exactly this stream’s read-more).
    • Happy path — if many options are unavoidable, highlight the one to pick (“Most popular”).
    • Stepper — split a 15-field form into 3–4 steps; more clicks, but less overwhelm.

    The twist I just got

    Hick’s Law is not universal. It holds for simple choices among roughly equal options scanned one-by-one. When items are grouped, familiar, or searchable — or the user already decided before looking — the law barely applies. A well-grouped 20-item menu can beat a messy 5-item one. The problem isn’t the count; it’s how much work you make the brain do.

    Takeaway: don’t cut choices mechanically — organize them.

    Sources:

    • til
    • ux
    • psychology
  13. Link

    grill-with-docs: interrogate the plan, then grow ADRs & CONTEXT

    Matt Pocock’s skill: it interviews you one question at a time to pressure-test a plan against the existing domain model, sharpens terminology, and updates CONTEXT.md (the glossary) + ADRs the moment a decision lands.

    The stream, the lexicon, the Collapsible primitive… all of this blog took shape through it (together with Karpathy skills). What I love most is that it’s stateful — every decision and trade-off gets written down as an ADR + glossary entry, so later I can look back and know why I chose this back then.

    • claude
    • architecture
    • workflow
  14. Link

    Karpathy skills: guardrails against the classic LLM coding mistakes

    Guidelines for Claude Code, distilled from Andrej Karpathy’s observations: LLMs tend to guess assumptions and over-engineer. Four principles: think before coding, simplicity first, surgical changes, goal-driven.

    Simplicity First keeps me from over-engineering — build exactly what’s asked, no speculative abstractions or “just-in-case” features. Surgical Changes says touch only what’s needed: no sprawling refactors, no drive-by polish. The clearest proof while building this blog: deferring pagination, sort, and the Vietnamese edition until actually needed; and when a bug hit, fixing it surgically (escape it in place) plus a note to clean up the system later — instead of overhauling the whole markdown pipeline on the spot. Day to day, when I ask AI to help find a solution, it forces me to state my assumptions before charging into code.

    • ai
    • claude
    • workflow
  15. Note

    Why won't Slack let you schedule a message for 9:07?

    Scheduling a Slack message the other day, I noticed the time picker only offers 15-minute slots — 9:00, 9:15, 9:30, no 9:07. Curious, I dug into the API and found the fun part: chat.scheduleMessage takes post_at as a Unix timestamp — precise to the second. The backend can happily fire at 9:07:32. So the 15-minute grid is a product decision, not a technical limit.

    Why “fewer choices” is the right call

    An hour has 60 minutes; a 60-row dropdown is an invitation to choice paralysis. Cutting it to 4 slots an hour is Hick’s Law in action — decision time grows with the log of the number of options. Put another way, it’s a deliberate cut to cognitive load: the picker asks your brain to do less so you decide faster. And in a workplace context nobody actually needs “exactly 9:07” — “top of the hour / half past” is plenty. There’s a subtler bit too: a message landing at 9:00 feels like a person who just opened their laptop and typed; one landing at 9:03:14 instantly reads as scheduled or bot. Round slots keep it feeling organic.

    What other apps do (verified 2026-06)

    • Telegram — down to the minute, stored server-side (sends even with your device off); up to 100 messages per chat, a year out.
    • WhatsApp — scheduled messages only just showed up in beta (Android + iOS, early 2026), not yet enabled even for testers. (Ignore the “precision / limit” numbers floating around — nothing’s final.)
    • Discordno native send-later for regular messages; only Scheduled Events, or a bot (server-only, and it posts as the bot, not as you).
    • Slack — 15 minutes in the UI, seconds in the API; up to 120 days out (time_too_far), up to 30 messages per 5-minute window per channel (restricted_too_many).

    Same distributed-scheduling problem, same million-user scale — the difference is product context: Slack is the workplace (“about right” beats “to the second”), Telegram is personal (“birthday wish at exactly midnight”).

    Two things I had backwards

    1. “15 minutes so the scheduler scans less often?” — Nope. Scanning every minute is standard: Walmart’s BigBen buckets events by time window (10:21:55 → the 10:21:00 bucket), a master scans every minute, with a minimum granularity of one minute. Minute-level UI is entirely viable at scale — Telegram is living proof.
    2. “15 minutes to fight the thundering herd?” — Backwards. Funneling everyone into 4 slots an hour concentrates the spike more than spreading it across 60 minutes. The thundering herd is handled on the backend with jitter (nudging actual send times apart) + rate limits, not with a UI constraint.

    Takeaway: sometimes removing an option is the right design — and the reason is product context, not an engineering limit.

    Sources:

    The deeper system-design dive — time-bucket partitioning, exactly-once delivery, jitter/backoff, how messaging platforms swallow millions of scheduled messages at once — I’m saving for its own /blog post.

    • til
    • ux
    • system-design
    • slack
  16. Link

    VoidZero joins Cloudflare — and a $1M fund for the Vite ecosystem

    VoidZero (the company behind Vite) is joining Cloudflare, with a $1M fund for the Vite ecosystem. Vite stays MIT, vendor-agnostic, team-governed. Next up: Full Bundle Mode + stabilizing the Environment API in Vite 9. This touches the blog on both layers: Astro is built on Vite (the dev server, HMR, and bundling are all Vite underneath), and the site deploys to Cloudflare Workers — so both the “engine” and the “where it runs” are in this news.

    • vite
    • cloudflare
    • open-source