notes by mara quill

Recent posts

I'm Mara, a backend engineer who keeps a small notebook here. Mostly Postgres, HTTP plumbing, and the boring parts of shipping software — written up once I've been burnt by them twice.

  • postgres
  • http
  • observability
  • process
  • tooling

Posts

  1. 7 min read

    Caching is a naming problem

    Every cache bug I have shipped was really a cache key bug. Here is the checklist I now run before adding one.

    • http
    • caching
    • backend

    A cache is a dictionary with a deadline. The deadline gets all the attention — Cache-Control, max-age, stale-while-revalidate — but the dictionary half is where things go wrong. If two requests that should see different responses hash to the same key, no TTL will save you.

    Start from the response, not the URL

    The key has to name everything the response depends on. The path is the obvious part; the Vary headers, the tenant, the feature flags in play and the API version are the parts that get forgotten. Write them down first, then build the key from that list:

    const key = [
      req.method,
      url.pathname + url.search,
      tenant.id,
      vary.map((h) => req.headers.get(h) ?? '').join('|'),
    ].join(' ')
    
    const hit = await cache.match(key)
    if (hit && ageOf(hit) < maxAge) return hit
    return fetchOrigin(req)

    If you cannot explain in one sentence why two requests share a key, they should not share a key.

    The checklist

    • Does the key include everything in Vary?
    • Does it include the identity of whoever is asking, if the answer differs?
    • Is the key stable across deploys, or does a build hash sneak into it?
    • Can you list the keys for one user and purge them all?
    Request flows into a cache keyed by method, path, tenant and vary headers; a miss continues to the origin. request cache method · path tenant · vary origin
    Figure 1 — the key is the contract between the request and the response.

    One more thing I do on every project now: a debug palette that shows the computed key for the current page. Press K in the staging build and it lists the key, the age and the TTL. It has caught more bugs than any test I have written for caching.

    Continue reading →

  2. 9 min read

    A tiny job queue in 120 lines of SQL

    You probably do not need a broker. SELECT … FOR UPDATE SKIP LOCKED and a cron-shaped worker got me further than I expected.

    • postgres
    • sql
  3. 5 min read

    What I got wrong about feature flags

    Flags are cheap to add and expensive to forget. A retrospective on a year of them, and the two rules that finally kept the count under twenty.

    • process
    • backend
  4. 6 min read

    Reading logs like a detective

    Structured logs are only half the story. How I lay out a request's timeline from three services and one very confused load balancer.

    • observability
    • ops
  5. 4 min read

    Notes from rewriting my dotfiles

    Ten years of accumulated aliases, deleted in an afternoon. What survived, what I actually use, and why the shell prompt is now three characters.

    • tooling
    • shell
  6. 8 min read

    The case for boring migrations

    Expand, backfill, contract. Three deploys instead of one, and I have not locked a table in production since.

    • postgres
    • process
Subscribe
New posts by email, roughly once a month. No tracking, unsubscribe in one click.
or grab the feed: rss