Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
1

Northern Sky & Southern Sky quilts

from Liz Denys [alt+shift+b] in programming

I find a lot of value in doing creative work that isn't in clay - I can feel more fully relaxed creating something that's purely a hobby, and working in other mediums inspires my ceramic studio practice in unexpected ways. I recently finished a very large, non-ceramic project: hand-quilted, hand-embroidered wall quilts of the Northern and Southern Skies. Perhaps it does not come as a surprise that such a large work of mine features the night sky, a motif that helps me feel hope in the dark. My project notes follow. Materials used for both quilts (roughly halve if you're only planning on doing one of the hemispheres) Haptic Lab's DIY constellation quilt patterns - I bought the pre-printed templates with the design printed on an embroidery stabilizer in the small (36" x 36") size Sulky Sticky Fabri-Solvy Stabilizer - optional, for printing any additional printing (essential if you go the print-it-yourself route) 5 meters Inky blue "Newton 260" linen from Merchant & Mills for quilt tops, quilt bottoms, and bias tape edges Note: One could get away with using less to make two quilts, especially since I intentionally cut the fabric with extra length on all sides (more on that in the notes). The fabric is much wider than needed for the tops and bottoms of the quilts at 145 cm (~57 inches), and the bias tape for finishing could have been made out of that excess width. I'm looking forward to using what's left in other projects! 3 spools (100 meters each) of the linen's matching Gütermann thread for the structural azimuthal grid lines of the quilts and finishing with bias tape 2 40" x 40" pieces of quilt batting - I used 100% bamboo batting Each quilt used one, of course. 2 skeins (57 meters each) Stef Francis's Space-dyed 603 Linen thread in colorway 27 for the words Each quilt required under one skein. This thread is closer in thickness to a sewing thread than a typical embroidery thread. Note that there is a possibility of significant variation between the dye lots (which...
3rd Jul 2025

Stay updated

Get a weekly newsletter with the top 5 articles worth reading every week.

More from Liz Denys

Lilypad mini quilt

While on a short road trip and then recovering from surgery, I hand quilted and hand embroidered a linen lilypad quilt. And yes, I did make this little quilt for my Frinos plush from Hades II to have a nice place to rest.

5th Jul 2026 • 1 votes
Things to consider when choosing a community ceramics studio, partial list

I've worked out of a handful of different community studios at this point, so I figured I'd share a (probably incomplete) list of you might want to consider when picking a community ceramics studio for your practice. Cost, location, value: I'm not going to comment too much on this as costs vary highly depend on what city/metro area/etc. you're in and you best understand what value you'll get and what you can afford. That said, if you live somewhere walkable and are planning on making at home, you might want somewhere you can walk to or get to with fewer transfers or shorter walking to make transporting fragile greenware simpler. Regardless of your budget, it's good to know all the costs up front: class tuition, membership fees, open studio time fees, firing fees, materials costs, etc. Also, if you live somewhere with a high demand for community ceramics studio access, you may not get to be that picky if you want to start sooner! Equipment and space: What equipment is provided? What condition is it in? How many wheels? How many wheels per member? How many wheels per student? Is there a slab roller? What size? Do they have dedicated canvases or mats for different color clay bodies? Which do you use if you use a non-studio claybody (if you're allowed). Is there an extruder? Which extruder dies does the studio have? Can you use your own if you buy more or make custom dies? What are the policies for who gets to use what equipment and training? Stricter policies usually means equipment will be in good condition, but only if it's enforced. Ask about how often equipment has needed repair and how to report issues, even if they have good policies. What do the workspaces look like? How much space is generally available per person? What kinds of chairs and stools are provided? Are stools at wheels adjustable? Are there bricks or similar available for resting feet on if that helps your wheel positioning? Are handbuilding and glaze area seats comfortable for you? What height are those work areas/chairs? This may not seem very important, but standing height tables can be much less comfortable if you're primarily going to sit. What does your storage space look like? Are shelves shared or do you get dedicated space? How much capacity and weight work on the shelves and how does that fit into your practice? Remember you may be storing tools and clay/reclaim as well as works in progress. There can be lots of politics around shelves - ideal ones are neither too high nor too low, all else equal, and can be very competitive to get. Highest shelves may also be highly coveted since you may be able to stack things very high and store more! How easy is it to move around the studio? How easy is it to get your pieces from the storage space to the working areas to the places where you set them to fire? Are there separate spaces for members and students? Even if there are dedicated student classrooms, do additional classes spill over into other areas or shared areas? How frequently? Is this information easy to find out before you get to the studio? Does the studio provide bats and wareboards? Remember, even if bats and wareboards are provided, you may want to bring your own that you can keep in better condition or have different properties. Are there shared studio tools? What condition are they in? Is there communal newspaper? Communal plastic for slowing down drying? Newspaper is easy to get yourself, but dry cleaning bags are hard to buy in individual-use quantities. Materials: What are the studio claybodies? Do they fit well in your practice? Are they smooth or groggy enough? Which colors of claybodies are available? How durable and food safe are they? Will they reach vitrification and achieve low absorption rates if you're making dinnerware or other vessels intended to hold water? Is clay communal or do you purchase your own? If so, does the studio reclaim for you or is it communal practice? What is done to ensure reclaim isn't short? If clay is not provided, can you buy it at the studio? 25 pound blocks of clay are heavy and annoying to transport if you walk, bike, and take transit everywhere like I do, so this can be a big deal! Can you do agateware/neriage/nerikomi? (Make sure you're doing this with compatible clays, of course.) This question is especially relevant if clay is communal as you cannot simply throw blended clay blocks into reclaim bins for a single clay as this would create a new claybody unexpectedly. Maybe you are required to reclaim yourself, maybe there's a miscellaneous reclaim, or maybe they accept a small amount of loss (remember you can minimize this by ensuring you use all your blocks up - the end can usually be wedged up into a new claybody and made into a little pinch cup or bowl worst case). Can you use outside claybodies? If yes, what's the approval process? Even if you are not personally interested in using outside claybodies, it's good to ask as free-for-alls can result in low fire clays melting all over shelves and ruin other people's work at hotter temperatures. Does the studio provide decorating slips and underglaze? If they don't provide, can you bring your own in? What's the approval process? Can you make your own decorating slips or colored claybodies with Mason stains or oxides? What's the approval process? What are the studio glazes? Are they clearly labeled as food safe vs. not? If multiple claybodies are common in the studio, how many glazes fit each and is this clearly labeled? Are there test tiles for every studio glaze on each studio claybody? Are there any test tiles for glaze combinations? Reminder that test tiles aren't a replacement for doing your own testing - different surface slopes, your dips may be longer or shorter - but they help give you a sense of what's compatible with which clay bodies and which are runny vs. stable. How and how often are studio glazes tested? Are new batches tested before they're put out for use? Are studio glazes typically kept in consistent conditions? Large buckets of glaze can change consistency over time. Is specific gravity monitored periodically so water can be added as needed? Are glazes flocculated well/kept well in suspension or are they often hard-panned? Can you use non-studio glazes? Typically, this means commercial glazes as homemade opens a gigantic can of worms. What's the approval process? Reminder that low fire materials used in higher temperature firings can mess up more work than just where they're used, so even if you're not planning on ever using non-studio glazes, it's good to know what checks are in place to avoid this issue. Does the studio provide wax? Wax brushes? What condition is it in? Does the studio provide oxide washes? Are you allowed to make your own if it doesn't? What's the approval process? Firings: What temperatures does the studio fire to? How does this match their claybodies? A lot of claybodies, especially commercial claybodies, claim to support a range of firing temperatures, but they're not fully vitrified at all of those temperatures, especially at the lower end of their ranges. If a studio fires to both cone 6 and cone 10 and you plan on working primarily with cone 6 glazes, either studio or commercial, and make food-safe pieces, is there an appropriate claybody that will reach vitrification or are they secretly really all meant for cone 10? If you're only interested in making non-functional work, this may not matter to you as much as someone making mugs or vases. Are there special firings, e.g. luster firings? If there are luster firings, make sure to inquire about their safety protocols around luster firings - even if you plan on never using luster, poor safety policies around luster can endanger the health of everyone in the studio. How quickly is work turned around? Faster isn't necessarily very important once you've settled in as you want to be in a flow that has work at all stages at all times so you always have something to do, but it can be helpful to plan your rhythm. What is being done to ensure quality materials and firings? Are cones used in every firing? Is work that is too wet or egregiously overglazed set aside or fired alongside everything else - causing frequent explosions and glaze mishaps that affect other people's work? Cleanliness and safety: How clean is the studio? If you can visit on a few different days, that might help. Do people generally clean up after themselves well? Don't forget to check glazing areas and the undersides of tables and such - lots of clay dust can hide there! Are people taught safe practices (wet cleanup, no dry sanding, etc.)? How often do they actually follow them? What supplemental cleaning does the studio staff do? How frequently? Are there air filters? What quality of cleaning do they provide and are they appropriately matched for the size of the studio? Are there air quality monitors? Note: this doesn't seem very common to find at community studios in my area, but it would be a good sign if the studio did! Access and security: Are there signups for specific space to use or is it a free for all? Signups can be frustrating, but you're guaranteed specific space at a specific time. I've noticed people are better at sticking to an appropriate amount of space for their work that's mindful of those around them when it's a space they have to sign up for, but the flexibility of being able to drop in whenever can be great, too! If studio time is signup-based, are new slots released at clear, consistent times that work for you or will you always be getting picks long after most others do? Do students and members have 24/7 access to the space or do they only access to the studio when it's staffed or it's their class or for times they've signed up for? When is the studio open? It can be especially helpful to check in on drying and potentially spray a piece with a little water if it's getting too dry before you're able to do work, even if it's not a time you can do work. It's also great for getting a sense of how quickly pieces dry at the beginning of new seasons/studio temperature and humidity conditions. Who's allowed in the studio? Are there video cameras? Staff on site? Studio flow: Getting a sense of a studio's flow is easiest by seeing and trying out the studio! Take a tour, attend a workshop, take a class! If you're interested in membership, it may still make sense to take a multiple-week class first, and at least in Brooklyn, a lot of studios require you to take a class at their studio before getting a membership to learn how their studio operates day to day. If you're interested in membership, note that membership flows and class flows might be different - different spaces, different open studio policies, different storage situations for work and tools, etc. for example, a studio I loved to work out of as a member was a terrible experience during a handbuilding class because it tried to cramp too many people in too small of a space, but members weren't in the member handbuilding areas as much all at once, so I could finally enjoy the studio. Also, students often share a communal shelf in my area, while members have more dedicated storage space, allowing for more projects at once and storing more tools and other items used in their practice. Community vibe: Is it more social? More get down to business? Loud? Quiet as a mouse? How do you vibe with the community? Does the studio have a code of conduct? Is it a good code of conduct? Does wearing headphones generally mean "don't bother me" or do people interrupt everyone frequently? How do the owners and staff interact with staff? How do owners and staff interact with students and members? What are their expectations of members? I've been at studios where the owners and staff really don't want to ever interact with you, but I also once worked out of a studio where the owner would roll in and interrupt whatever was going on to use whoever was at studio as their personal therapist... even though it was time those members and students were paying her to use the studio. While the latter is a pretty extreme example, I've heard from ceramist friends in many areas that it's sadly not that uncommon to encounter overbearing owners that sour the experience. Miscellaneous: Are you allowed to sell work you make there? Are there community shows/sales? Are there official or unofficial limits on the volume of work? Some studios only want potters who aren't making very small quantities of work or only small to medium sized pieces because they don't actually have the capacity for high throughput or larger work... but not all of them will have clear policies on this. If there's a limit but it's only an unofficial policy, it will likely be applied unevenly. Finally, while it's unlikely that every community studio, or perhaps even any community studio, will be a completely perfect fit for any given potter, I've personally found my practice to be enjoyable at every community studio I've been at with a good community vibe. The goal of this list isn't to find a reason to avoid any particular studio as much as knowing how a studio's choices or limitations might affect your practice so you know what to expect and how to adapt!

10th Mar 2026 • 1 votes
WWII watch cap crown shaping

Like so many other hat knitters, I've knitted the classic WWII watch cap, which is also known as Beanie no. 212, more than a handful of times. (You can find it on Ravelry here and here, respectively.) It's a charming, quick knit that's easy to resize, but I've never really liked the crown shaping on the original. I've knit a lot of these hats, and only one's had the original crown shaping for the decreases. I'm sharing two crown shaping variations I keep going back to below. I always knit this hat in the round, and my crown shaping patterns assume you will be knitting in the round, too. Alternate crown shaping 1 This alternate crown shaping follows the 6x2 ribbing of the previous section for the bulk of each round so that the ribbing continues throughout the entire hat. Written out: Divide stitches on 3 double pointed needles with stitches evenly divided among the three needles or mark out even sections with stitch markers if decreasing across 2 circular needles or with magic loop. (k1, ssk or skp, k if previous row was knit or p if previous row was purl until 3 stitches are left on the needle/before the next marker, k2tog, k) x3. Repeat step 1 until 4 stitches remain on each needle/in each marked section. Cut and draw yarn through the remaining 12 stitches 2-3 times. Tie off and weave in the ends. Alternate crown shaping 2 This alternate shaping both maintains the ribbing throughout the crown shaping and accentuates it by creating a sort of three-pointed star with 3 of the 2-stitch-wide purl parts from the previous 6x2 ribbed section. This shaping really only makes sense when making a size hat where the number of stitches is evenly divisible by 24, so I typically only do it on size "small" hats (cast on 96 stitches). You could adjust the needle size and gauge for a yarn that works well at that gauge to make this work for different sizes. Written out: Stop the last 6x2 ribbed row 1 stitch early and divide stitches on 3 double pointed needles with stitches evenly divided among the three needles or mark out even sections with stitch markers if decreasing across 2 circular needles or with magic loop. Ensure that each needle transition or marker has a single purl stitch on each side. (p1, ssk or skp, k if previous row was knit or p if previous row was purl until 3 stitches are left on the needle/before the next marker, k2tog, p) x3. Repeat step 1 until 4 stitches remain on each needle/in each marked section. Cut and draw yarn through the remaining 12 stitches 2-3 times. Tie off and weave in the ends. Additional project notes Needle: US 7 - 4.5 mm Gauge: 19 stitches and 24 rows = 4 inches in stockinette Hats made for me should generally be a small (cast on 96 stitches), and hats made for my partner Matt should generally be a medium (cast on 104 stitches). Footnotes Either of these will create a left-leaning decrease, use whichever one you prefer.↩ Ibid.↩

15th Dec 2025 • 1 votes
NYC Bike Rules for Drivers mini-zine

After five different drivers, including a school bus driver, aggressively close-passed me in Midwood last night and two of them threatened me just for being on the street, I realized there's a huge gap in educational outreach about bike rules in NYC: drivers. I decided to make a mini-zine that demystifies the behavior of law-abiding bicyclists like myself and helps drivers better understand what to expect when sharing the road: All text and illustrations in the mini-zine are my own. Many thanks to Matt Denys for help proofreading! This zine is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License, so you can copy and distribute this zine for noncommercial purposes in unadapted form as long as you give credit to me. Gas stations, car dealerships, auto repair shops, rental car offices, parking lots, book shops, restaurants, coffee shops, and other commercial establishments are welcome to give out these zines, too - as long as the zine itself is free! Check out the NYC Bike Rules for Drivers mini-zine on the web or download the pdf to print here!

21st Oct 2025 • 1 votes
How to fold a mini-zine

Here's how to fold a mini-zine, featuring my mini-zine So you want to make a public comment! Fold the printed mini-zine page in half, then half in again, and then in half again as shown. When you open the piece of paper, each of the pages should fit fully within a set of folds. With the mini-zine page completely open, fold it in half with short ends touching ("hamburger style") so the text is on the outside. Using scissors, cut a across the dotted line. When you reopen your paper, there will be a slit in the middle of the sheet. Fold the paper in half lengthwise with long ends touching ("hotdog style"), hold the paper at either end, and fold the sheet into itself to form an 8-page booklet. Make sure the cover is on the front!

15th Aug 2025 • 1 votes

More in programming

Clip of me singing Despard in Ruddigore in 2013

A clip of me singing a funny song from Gilbert and Sullivan’s Ruddigore back in 2013

6 hours ago • 1 votes
How and Why fork() Uses Copy-on-Write

In this video, we look at why fork() needs copy-on-write, how it works inside the kernel, and a memory usage problem that Instagram encountered with Python.

13 hours ago • 1 votes
What we lost when we lost comments

Comments require commitment, but they’re worth it.

19 hours ago • 1 votes
Lighthouse map

Lovely global map with animated lights sweeping the waters

21 hours ago • 1 votes
Warming up the Puma master before it forks

Basecamp 5 runs on Puma in cluster mode: one master process with preload_app! and 63 single-threaded workers per host, deployed as a Docker container with Kamal. We serve Basecamp from several sites. Each site has its own web hosts and a read replica of the database, and writes go to a single primary database in one of them. On our busiest hosts, each deploy left up to 2,000 requests waiting while the new workers warmed up. We reduced those queues by running signed-in requests through the app in the Puma master, before it forked the workers. Why 63 single-threaded workers? Basecamp has always served web requests from processes rather than threads. It ran on Unicorn, which only does processes, until we moved to Puma in January 2025, and we kept the same setup: workers (Concurrent.physical_processor_count * 1.3).ceil threads 1, 1 preload_app! On a 48-core host that’s 63 workers, each handling one request at a time. We chose 1.3 after benchmarking HEY in 2023, when we moved our apps out of the cloud and onto our own hardware. We tested several combinations of workers and threads with a mix of GET and POST requests on a 32-vCPU VM. Every multithreaded configuration we tested was slower and handled fewer requests than single-threaded workers. Adding workers beyond about 1.2 to 1.3 per vCPU brought little benefit. The threaded workers spent a lot of their time waiting for Ruby’s global VM lock. That made single-threaded workers a good fit for this workload, and we use the same setup for Basecamp. An app that spends more time waiting on its database or other services may benefit from more threads, so benchmark your own app. The other reason is the app itself. Basecamp has class-level state in places and has never needed to be thread-safe. With one request per process, it still doesn’t. Processes do use more memory than threads, and preload_app! reduces the difference. The master loads the app once and the workers share its memory through copy-on-write until they write to it. Shopify’s comparison of Ruby execution models explains the trade-off well. In the HEY benchmark the best setup came to about 260 MB of PSS per core, where PSS counts each shared page once, split between the processes using it, and the gap to a threaded setup was smaller than we’d expected. What Puma does on each host when a container starts: one master, then 63 forked workers that share its memory until they write to it. Two things about this setup matter for the rest of the post. A worker that’s compiling or loading something is fully blocked — there’s no other thread to pick up the next request. And whatever the master has in memory before it forks, all 63 workers share. Whatever they build after the fork, they build 63 times. What happens when we deploy Kamal starts the new container alongside the old one, and kamal-proxy moves the host’s traffic across as soon as the health check passes. At that moment, the new workers have handled health checks but no customer requests. preload_app! means the master loads the app once and the workers inherit it through fork. That covers the code. It doesn’t cover anything Ruby and Rails set up on first use: YJIT compiled code. YJIT compiles a method once it’s been called a certain number of times. The master calls very little during boot, so every worker compiles the same methods again on its own first requests. Compiled templates. Action View turns each ERB template into a Ruby method the first time it’s rendered. The schema cache. Active Record reads each model’s columns from the database the first time that model is used. Inline caches and memoized values throughout Ruby, Rails and the app. All 63 workers did all of this at once, while serving the traffic the old container had been handling a second earlier. In the test environment with YJIT on, the first request to a project page on a cold process took 652 ms, 151 ms of it YJIT compiling. The same request to a warm process took 28 ms. In production, CPU time per request peaked at around 200 ms while kamal-proxy moved traffic to the new container, against about 30 ms once the workers had warmed up. A host with spare CPU absorbs this. Every one of our web hosts has 48 cores and 63 workers, but each Amsterdam host serves around 250 requests per second, against 25 to 60 at our other sites. In Amsterdam the slow first requests turned into a queue. At a peak-hour deploy, the Puma backlog on an Amsterdam host reached anywhere from 250 to 2,238 requests, and kamal-proxy’s p99 response time hit about 10 seconds. Eron, our Director of Operations, had been tracking this since June. Another server in Amsterdam would help, but it would take weeks to arrive, so we also wanted to make deploys cheaper on the hardware we already had. What didn’t work We tried a few things first. In June, Donal tested the first two on a single Amsterdam host, comparing it with its neighbors, and they ruled out two likely causes. Warming each worker’s database connections. Puma’s before_fork hook clears the master’s connections, and each worker opened its own on its first request. Opening them in before_worker_boot instead made no difference. Queries on a freshly booted production host were already under a millisecond, so connections weren’t the problem. A synthetic request in each worker. Next, each worker made a few requests in before_worker_boot to an internal controller that touched every model. That ran the middleware, routing and Active Record paths, but it ran them in 63 workers at once — exactly the CPU spike we were trying to avoid. And a request with no real data renders no real views, so most of the app stayed cold. Spreading YJIT compilation out. Delaying YJIT in each worker by a random interval spread the compiling out over a few minutes, but every worker still ran interpreted until its delay ended. The queue didn’t change. Reforking from a warm worker. This is what Shopify’s Pitchfork does: let one worker serve traffic until it’s warm, then fork the others from it. Puma has an experimental version called fork_worker, and on beta it worked — the reforked workers were warm after three to five requests, where fresh ones took up to 30 seconds. But with fork_worker the template is worker 0, and it keeps serving requests. If it exits, the workers waiting to be forked never start (puma/puma#3596). If it gets no traffic, the refork never happens, which is what we saw on beta. Instacart have a mold_worker patch that promotes a warm worker to a template that stops serving, but it isn’t in a Puma release. We have a branch of it, and we may come back to it. That last experiment did show us where the fix was, though. Everything a warm worker has that a cold one lacks is in its memory, and fork copies memory. The master already has the app loaded. It just never runs it. Run the requests in the master So now, before the master binds its socket and forks, it makes the app’s own requests, in-process, the way a signed-in user would. Rack has a hook for exactly this. Rack::Builder#warmup takes a block that’s called once with the built app, before the server starts. rails server builds the app from config.ru, so the change to boot is one line: require_relative "config/environment" warmup { WarmUp.configured.run } if ENV["WARM_UP"] run Rails.application With preload_app! this runs in the master, and the workers inherit whatever it did. Puma binds its socket after the app is built, so until the warm-up finishes the health check’s connection is refused and kamal-proxy keeps retrying. No request reaches a worker that hasn’t been warmed. The warm-up has three steps. After precompiling the views, it gives the page requests and schema loading a shared 20-second budget, checked before each page or model. 1. Precompile the views actionview_precompiler reads every template for its render calls and compiles each one with the locals it’s passed. For us that’s 1,394 templates in about two seconds. A first request to a project page then compiles 2 templates instead of 44. 2. Request the pages, signed in A small browser class makes the requests through Rack::MockRequest, with the two cookies a real sign-in sets, then goes back for each page’s lazy Turbo frames: class WarmUp::Browser def initialize(signed_in_as:) @client = Rack::MockRequest.new(Rails.application) @headers = { "HTTP_USER_AGENT" => "Basecamp warm-up", "HTTP_COOKIE" => cookie_for(signed_in_as), "bc3.warm_up" => true } end def visit(path) page = get(path) frames_in(page).each { |id, src| get(src, "HTTP_TURBO_FRAME" => id) } end private def get(path, headers = {}) @client.get("https://#{host}#{path}", @headers.merge(headers)) end def frames_in(page) Nokogiri::HTML5(page.body).css("turbo-frame[src]").map { |frame| [ frame["id"], frame["src"] ] } end end The requests are signed in. The user is a monitoring account we already use for automated checks, and the pages are its own project, Campfire, to-dos, documents and messages. Public pages weren’t enough: after warming up with signed-out pages only, the first signed-in request to the projects page still took 131 ms, because authentication, the signed-in controllers and their views had never run. With signed-in pages it took 40 ms. cookie_for writes the same signed cookie the sign-in controller does, using the app’s own cookie jar, so there’s no API token and no secret to store. The frames are followed. The busiest HTML requests in production aren’t pages at all but Turbo frames — the sidebar badge, the inbox, the navigation menus. The browser parses each page and requests its <turbo-frame src> URLs with the Turbo-Frame header, so those controllers and views get warmed too. Our first four pages turned into 60 requests. The requests are excluded from rate limiting. They are internal, so they do not count against the rate limits that apply to real visitors. 3. Load the rest of the schema The page requests load the schema for the models they touch. The last step loads the rest, from the read replica: ApplicationRecord.reading do models.lazy.take_while { time_left? }.each { |model| model.load_schema if model.table_exists? } end The step checks 261 models and loads any schema information still missing. Those database round trips add up when the primary is far away: outside a request, Active Record uses the writing role, and from a host a long way from the primary each round trip is tens of milliseconds. Reading from the local replica brings the step down from about 20 seconds to 3.5. The pages go first because they load most of the schema anyway. If the time budget runs out, the step stops, logs how many models it got through, and the workers load the rest on first use like they always did. Rails can also load the schema from a dumped cache file at boot (bin/rails db:schema:cache:dump), which would make this step unnecessary. We don’t ship one in our image yet, because the dump needs a database to read from at build time, and we have several databases to cover. It’s on the list. What to close before the fork Running requests in the master opens things the master never opened before, and every worker inherits them. Two processes writing to the same socket will corrupt each other’s traffic, so you need to know what’s open before you fork. The way to find out is to list the master’s open file descriptors — ls -l /proc/<pid>/fd — before and after a warm-up, in an environment set up like production. Development wasn’t enough for us: it stores files on disk, so our S3 connections only showed up in production. Then, for each thing that’s open, check how its library handles a fork. We found three kinds: Already handled. Plenty of libraries detect a fork on their own, either by recording the PID they connected from and reconnecting in the child, by opening per-process files, or by resetting their thread pools. Redis clients, metrics libraries and concurrency libraries tend to be in this group. Check, but you probably don’t need to do anything. Already closed. Database connections are the classic one, and most Puma configs already clear them in before_fork. Anything else that’s opened per process — we have a SQLite cache the workers open on boot — needs closing when the warm-up finishes. Needs a new step. HTTP clients with keep-alive connections are the ones to look for: cloud SDKs with connection pools, tracing exporters, error reporters. They usually have no fork handling at all. We empty the aws-sdk connection pools in before_fork, and we run the warm-up untraced so the OpenTelemetry exporter never opens its connection to Tempo in the first place. Once that’s done, before_fork finishes with Process.warmup, which Ruby 3.3 added for this purpose: a major GC, a heap compaction, and every surviving object promoted to the old generation, so the memory pages the workers share change as little as possible afterwards. Choosing the pages The first list was the four pages that ran the busiest requests on beta. Once the warm-up was live, production showed us which endpoints were still cold. For one deploy, we compared each endpoint’s mean duration in the six minutes after kamal-proxy moved traffic to the new container with the same endpoint an hour later, then multiplied the difference by the number of requests in those six minutes. That gives the extra time each endpoint cost us because it was cold: Endpoint Cold Warm Requests in 6 min Extra seconds Campfire 246 ms 70 ms 6,490 1,140 Projects (JSON API) 84 ms 50 ms 22,077 771 Docs & Files 262 ms 177 ms 4,996 421 To-dos tool 205 ms 113 ms 4,018 371 To-dos (JSON API) 33 ms 16 ms 18,738 320 The pages already in the warm-up showed what to expect: the project page kept a 36 ms gap after a deploy, and the to-do page 10 ms. We’ve proposed adding these five requests, and expect them to add about five to seven seconds to the page step. The two JSON endpoints were a surprise. The warm-up’s page list had no API requests in it, so nothing on the API path had run before the first real request: not the API controllers, and not the Jbuilder templates rendering real records. Precompiling the views covers JSON templates too, but it isn’t a substitute for running the request. Results The warm-up is on for all 68 web hosts. With the first four pages it took 12 to 16 seconds per host: about 2 seconds to precompile the views, 7 to 9 for the 60 requests, and 3.5 for the schema. Deploys take that much longer per host, and we raised the deploy timeout from 30 to 60 seconds to cover it. In Amsterdam, at a peak-hour deploy: During deploy Before After Peak Puma backlog per host 250–2,238 requests 19–223 requests Peak kamal-proxy p99 about 10 s 2.4–4.8 s Peak CPU time per request 201–214 ms 88–132 ms Peak database time per request 56–69 ms 39–47 ms The same eight hosts at three deploys on 1 October, an hour apart, as the warm-up went from one host to four to all eight. The deploy in the middle, with four hosts warmed and four not, shows why every host needed the warm-up. Each warmed host recovered faster on its own: mean request duration peaked at 130 to 173 ms, against 203 to 311 ms on the hosts that weren’t warmed. But the backlog on all eight was about the same, because they were all waiting on the same database. Mean request duration on each host at the 07:21 UTC deploy. Blue hosts warmed up in the master before forking, orange hosts did not. Memory came down too. The workers now share compiled templates, YJIT code and the schema with the master instead of each building their own copy. On beta, the view precompiler alone took a busy worker’s private memory from 174–202 MB to 119–135 MB. Thirty minutes after the deploy, the web containers used about 39 GB less memory than the previous day’s containers at the same age and traffic. Amsterdam served most of our traffic at the times we tested. In Amsterdam, each new container used about 2 GB less just after traffic moved to it, which lowers the peak while the old and new containers overlap. Working with Claude Claude Code helped throughout. It combed through the per-worker backlogs and per-endpoint timings in Prometheus and Loki after each deploy, worked out the cold-versus-warm cost of each endpoint, and prepared the changes and the pull request descriptions with the benchmarks in them. We decided what to try, deployed it and read the results. If you do this Warm the master before it forks. Compile common code and templates and load their schema in the master, so workers inherit that work. With preload_app!, Rack::Builder#warmup runs before the workers start accepting traffic. Use the app’s real requests. Public pages, internal endpoints and synthetic queries warm the paths they run and nothing else. Signed-in requests to real records, frames included, run what production runs. Measure the cold penalty per endpoint. The difference between an endpoint’s cold and warm duration, times its request count after a deploy, ranks the pages worth adding. Ours weren’t the ones we’d have guessed, and two of them were JSON. Check what the warm-up leaves open. List the master’s file descriptors after a warm-up and account for every one before the fork. Two of ours needed changes. Set a time budget. A warm-up that runs long on one slow host fails the deploy on that host. Ours gives the page requests and schema loading a shared 20-second budget, checked before each page or model, puts the most valuable pages first, and logs what it skipped. Reforking from a warm worker, as Pitchfork does, solves the same problem continuously rather than once at boot, and it would warm paths no fixed list of pages covers. We may still get there: our branch brings Instacart’s mold_worker up to date with Puma’s main branch and fixes the bugs we found in it. But warming the master works with the Puma we already run, took a few days to implement, and substantially reduced the queues after deployment.

yesterday • 1 votes
📚 BoredReading

You seem to be enjoying this.

Join free to unlock everything.

Create free account

Already have an account? Sign in