Patrick Donahue

Patrick Donahue

@levelbrook.bsky.social

Senior backend engineer. Ruby on Rails, Go, Postgres. I write about the part of a system that only misbehaves under load. 58 essays, plus the tools and demos behind them: levelbrook.com Open to contract work.

206 followers1.6K following222 posts

Go 1.23 changed timer garbage collection, so the classic time.After in a loop leak no longer applies. I had a check written for it. Deleted it. Analyzers rot when the runtime improves. #golang

Small static binaries in Go reduce deployment surface, but do not inherently prevent supply chain attacks. Vendor dependency vulnerabilities remain a distinct concern. #golang

PG 18 changed a default: a generated column without STORED is now VIRTUAL, and virtual columns can't be indexed. Rails 8.1 follows it, so a migration's virtual column without stored: true emits VIRTUAL on 18. Put a unique index on it and the migration fails. #postgresql

A partial index WHERE status = 'pending' only helps if the planner can prove the query matches it. With a prepared status = $1 and a generic plan it can't: on PG 18 that gave a seq scan + sort, the custom plan an index scan. Keep the literal in the SQL. #postgresql

Turbo morphing dramatically simplifies real-time UI updates, but it pushes the burden of consistent server-side HTML generation to every single endpoint. #RubyOnRails #Rails

PgBouncer 1.26.1 fixes three CVEs. The urgent one: under SCRAM auth, a client that knows one user's password could log in as any other SCRAM user, admins included. It also restores ignore_startup_parameters = search_path to 1.25 behaviour. Upgrade the image. #postgresql

After a migration changes a column type, Postgres fails stale prepared statements with "cached plan must not change result type". Rails retries outside a transaction. Inside one it raises ActiveRecord::PreparedStatementCacheExpired, so retry_on it in jobs. #rubyonrails

Goroutine leaks present as a gradual memory increase rather than a crash. This makes them a distinct class of defect, often missed by standard error monitoring tools. #golang

Rails behind PgBouncer in transaction mode no longer needs prepared_statements: false. PgBouncer 1.21 added protocol-level prepared statement tracking, and since 1.24 max_prepared_statements defaults to 200. On an older pooler, the old advice still holds. #rubyonrails

Go plus a static binary in an alpine container is a 10MB image and about 15MB of RAM in production. I keep putting Rails next to it and being reminded what the trade actually costs. #golang

New Rails 8 app: Solid Queue is wired for production only. Development runs Active Job's :async adapter, in-process, jobs lost on restart. If a job only fails in prod, that gap is the first suspect. #rubyonrails

Looking for my next role: senior backend or full-stack, full-time or contract, US remote. 7 years on one multi-tenant B2B SaaS ($4M to $13M ARR) and a 1TB Postgres. Now shipping a live Rails 8 product of my own. Rails/Postgres teams especially. levelbrook.com/hire/?ref=bluesky #rubyonrails

A float aide on a Saturday meets a resident knowing a room number and a care plan. That's it. I built Porchlight so the resident's own words travel with every shift: a short briefing on who they are and what they love to talk about. Free for communities while they try it. https://porchlight.ing

If you want an agent to put a site on the internet unattended, the whole problem is credentials with the right shape. The deploy itself is trivial. Everyone skips straight to the deploy. #CloudflareDev #DevOps

Taking on 1-2 contract projects for November. Senior engineer, 7 years on one multi-tenant B2B SaaS ($4M to $13M ARR), Rails and Postgres. Good fits: an app that got slow, an upgrade that stalled, a first product that needs a real backend. levelbrook.com/hire/?ref=bluesky

Solving too many queries often means restructuring the data access pattern entirely, not just adding includes. The naive approach frequently swaps query repetition for memory bloat on large data sets. #RubyOnRails #Rails

An ALTER TABLE that is only waiting for its lock already blocks your reads. On PG 18: one open read txn, an ALTER queued behind it, and a plain SELECT stuck behind the ALTER. SET lock_timeout = '1s' before DDL makes it give up, and reads flow again. #postgres

Using pg_advisory_lock behind PgBouncer in transaction mode? The lock belongs to the server connection, so the unlock can land on a different backend, fail, and leave the job locked. pg_advisory_xact_lock releases at COMMIT on the backend that took it. #postgres

PG 18's uuidv7() fixes the random-UUID index churn, but the id now embeds when the row was made: uuid_extract_timestamp(id) gives it back to the millisecond. Fine for most tables. For users, invoices or anything with a public id, that's your signup date in the URL. #postgres

Put a subdomain behind Cloudflare's orange cloud and Let's Encrypt HTTP-01 stops working, because the challenge never reaches your origin. Set the record DNS-only for the issuance, then decide. #CloudflareDev #DevOps

Cloudflare Pages plus a DNS API token is the cheapest way I know to let an agent stand up a real URL. Free tier, no server, TLS handled, and the site is live in about forty seconds. #CloudflareDev #DevOps

Taking on 1-2 freelance projects in October. Best fit: a web app that works but has gotten slow or fragile, or a manual process you want automated with a real backend and an audit trail behind it. levelbrook.com/hire/?ref=bluesky

Rails angle on the Postgres generic-plan trap: where(status: "pending") is sent as status = $1 with the value bound, so a partial index on status = 'pending' is exactly what execution six can lose. where("status = 'pending'") keeps the literal. Checked on Active Record 8.1.4. #rails

A Postgres prepared statement runs 5 custom plans, then may switch to a generic one. A partial index on status = 'pending' can't serve status = $1, so execution 6 can lose the index. EXPLAIN (GENERIC_PLAN) on PG 16+ shows that plan up front. Keep the literal in the SQL. #postgres

Turbo morphing fundamentally changes how client-side state is preserved. Instead of rebuilding, the browser intelligently updates, reducing flicker and preserving focus. #RubyOnRails #Rails

PgBouncer 1.26.0 (Sept 23) now tracks search_path on PG 18+ and default_transaction_read_only on PG 14+. Before it, a SET of either in transaction pooling mode leaked to the next client on that server connection. Also 3 CVEs, two reachable before auth. #postgres

Static site, free hosting, real domain, automatic TLS, deployed from the CLI in one command. In 2015 this was a week of work. People still budget a week for it. #CloudflareDev #DevOps