Staying "As Rails as Possible": How Buzzsprout Runs a Large Podcast Host with a Tiny Team

Open on YouTube ↗
Overview

Tom Rossi, co-founder of Higher Pixels, the company behind Buzzsprout, joined Robby Russell on On Rails to explain why the company stays as close to vanilla Rails as it can. Rossi's reason is pragmatic rather than ideological. Rossi calls themself a pragmatist who never went shopping for the best technical stack. They wanted to build a SaaS business, and Rails supplied the guardrails for that. Since it kept working, they saw no reason to look elsewhere, and they say they never chased the "shiny objects" when others talked about switching technologies.

27 min read

The conversation covered Buzzsprout's history and scale, the painful move from Paperclip to Active Storage and the upstream contribution that came out of it, RSS polling and ETags, a shift in analytics from hand-built summary tables to ClickHouse, and why Rossi wants the company to stay small.

Inheriting opinions from people "smarter than you"

Asked whether the Rails community fits a business-first mindset, Rossi said that, especially when starting out, it is valuable to inherit opinions from people smarter than you, and they had no trouble admitting that others had made better decisions. They also admitted that on their first couple of Rails projects they wrote configuration to override the defaults because they didn't want to follow them, and those overrides "come back and bite me later."

Rossi started with Rails before 1.0, around 2005, not long after the well-known "build a blog" video. The video came from Kevin, a designer they had worked with who later became their business partner. The two had been a .NET shop, and Kevin told them the video described exactly the problems they kept hitting when building apps in .NET. Kevin wasn't a programmer, but after watching it Rossi was "blown away" and never looked back.

From client work to a portfolio of products

Higher Pixels began doing client services. Around 2001 it pivoted toward its own products, starting with one built in .NET. Rossi liked running a SaaS product and proposed that Kevin join them in building another. That became Tick, time tracking for people who track time against budgets, and it was built on Rails. More products followed: the original .NET product was rebuilt in Rails, and the company launched Donor Tools and StreamCare, a product in the medical space. For a long time the two founders had more products than people, which Rossi describes as "spinning plates."

Tick and StreamCare were the main revenue sources. Buzzsprout, built in 2008 and launched (Rossi thinks) in 2009, was a small product they built because they needed it to complement another product. It didn't make much money at first. Early podcasters were mostly technical people and a lot of DJs. As podcasting took off, Buzzsprout "just exploded." Rossi credits Rails with letting the team keep adapting and adding features as podcast hosting evolved as a service.

Rossi has no desire to return to client work. They describe it as brutal and prefer being "the client" who makes the decisions. They do miss the variety and seeing clients' "eyes light up" when a problem gets solved, so they occasionally help friends for free. At the time of recording they were doing this for a friend whose technical team hates Rails without knowing much about it. Rossi told the friend they could build what was wanted in probably less than six weeks.

Rossi also described a lesson about marketing. The team used to think building a great product was enough. When growth stalled, their instinct was to build more features, but "the problem isn't the features," it is marketing. Kevin handles the creative side and connecting with audiences. When the medical product launched, a third partner, Marshall, joined on the finance side and balances Rossi and Kevin, who in Rossi's words "put all of our chips on the table all the time."

Scale, team size, and the January spike

Today Higher Pixels works only on Buzzsprout and StreamCare, each with its own team. StreamCare has one developer, Ron, who is part of a Ruby group in Colorado, and Rossi occasionally helps out. Buzzsprout has two SREs, three programmers, and two designers, which Rossi calls a pretty small team compared with competitors.

Buzzsprout hosts about 472,000 podcasts. Rossi estimates about 125,000 are active, meaning people are still uploading episodes. Asked about the common claim that most podcasts never get past a certain number of episodes, Rossi said they checked their own statistics for something like a "seven-episode hump" and found nothing, other than that many people never upload a first episode. The free plan makes the numbers hard to read. By the time someone is paying, they usually have at least one episode.

Podcasting turns out to be seasonal. January is a big month because people decide to finally start that podcast, the same way they join a gym.

Bringing in an SRE just before 2020

The SRE roles are relatively recent. Buzzsprout hired its first SRE in 2019. Rossi knows how to write Rails code but not how to handle server configuration and the tuning that scale requires. The team had relied heavily on a vendor for that work, then realized that "our whole business is built on this" and that without the skill in house they could go down.

The timing was fortunate. In 2020, with people locked in their homes during the pandemic, podcasting exploded, and Rossi says the team was "holding on by your fingernails" as signups poured in. The SRE scaled them up quickly, and Buzzsprout moved to a Kubernetes environment in 2020. It still runs on Kubernetes.

The SRE is named in the transcript as Brian Treywick. Rossi describes them as "an SRE plus" because they also do development. For the ClickHouse work, for example, they didn't just start the server; they worked out the Active Record configuration and how to run parallel tests. A second, more traditional SRE joined about a year to a year and a half ago. That person watches memory usage and scaling for job processing, and was hired partly so Brian could take real vacations. Rossi says Brian always brings a laptop when away, and Rossi would love them to go to Italy without one. Robby shared advice they once received: don't hire for a role until you can afford one and a half to two people in it, so there is redundancy. Rossi summed this up as the "bus factor."

What "as Rails as possible" means day to day

Buzzsprout's mission, in Rossi's words, is to help podcasters start podcasting and keep podcasting, and they will use whatever means best delivers that. Rails is good at this because it has no opinions about podcasting but has opinions about things that "probably don't matter," such as column types and table naming. Those are topics teams can spend a lot of time debating. Rossi also calls Rails agile: things are easy to change. As an example, Buzzsprout recently introduced annual plans. That was painful, but mostly because of how to communicate the change in English to customers, not because of the Rails implementation, which was much simpler.

Robby noted that naming conventions may sound like a small benefit, and recalled pre-Rails teams mapping out whole schemas and debating column prefixes. Rossi agreed it isn't the biggest lift and offered Hotwire as a better example.

Hotwire and the mobile apps

Rossi says Hotwire has saved the company "a ton." The team designed and built the Buzzsprout mobile app within six months. The Android app took about three more months. The team wanted both platforms to launch at the same time, and the design work was already done, so implementation wasn't too hard. Rossi's explanation is that they are web developers working in Rails and could reuse everything they already knew, connecting the app with Hotwire. Rossi says the app feels native because the parts that need to be native are connected in, and mentions Stimulus in that context.

Asked about payments, Rossi said the team avoided Apple's and Google's in-app payment systems by putting no payment information in the app at all, presenting it as a "companion app." That position still had to be defended. According to Rossi, the app had been approved during development, but when the team was ready to launch it was denied. They argued that it was a companion app for people already paying through the website, and waited without knowing how long the dispute would take while prepared launch marketing sat on hold. Rossi called it brutal. They know the rules have changed since then but still haven't added payment features because they don't want to risk going through that again. The app doesn't do recording, but Rossi says that when the team gets to features like that, Rails gives them a good toolbox to reach into.

From consumer to contributor

Higher Pixels is a member of the Rails Foundation, and Rossi ties that to the fact that everything the company builds is on Rails. They explained that using Rails in a business is very different from contributing to it. Over the past year to year and a half the team has started trying to contribute, and Rossi says it requires "a different brain" because you have to think so meta about the work. The company has had a couple of contributions merged.

The most recent was in Active Storage. Rossi thinks Buzzsprout can help the community here because the company stores so much data and cares so much about bandwidth, which is a major cost of goods sold. That PR took about a year to get merged. To explain why the team cared, Rossi went back to the migration from Paperclip.

The Paperclip to Active Storage migration, and what took down production

Rossi presents Active Storage as a case where the company's lack of participation hurt it. The team always tries to follow Rails' opinions, so as soon as Active Storage was generally released (they weren't on Rails edge), they decided to drop Paperclip. Rossi calls this "the wrong move": Active Storage wasn't built for Buzzsprout's use case, and the switch led to a long run of monkey patches. In hindsight, they think the team should have waited and helped Active Storage become what they needed.

This happened before the company had an SRE. The team was just Rails developers. They deployed the change and Buzzsprout ground to a halt. They rolled back, reviewed their code, redeployed, and the site went down again. They rolled back once more and asked their hosting vendor for clues. The vendor said request volume was rising exponentially beyond the available resources.

The cause turned out to be asset URLs. With Paperclip, public assets were linked directly (still on a Buzzsprout domain), so requests never reached the Rails server. With Active Storage, every asset request went through Rails. Each RSS feed contains hundreds of links to artwork, episodes, and MP3s, so all those URLs suddenly hit the application. The first monkey patch overrode this behavior to link directly to public assets in RSS feeds and a few other places.

The second round came when Buzzsprout started relying on a CDN. Rossi says you don't need to worry about CDNs when starting out, but eventually a lot of unchanging requests didn't need the server at all. The team had to patch Active Storage again to emit CDN links instead of redirecting to S3. Rossi thinks Active Storage was designed around a domain like Basecamp's, where assets sit behind a login and should be private, while most of Buzzsprout's assets are public and need to be served through a CDN with fine-grained control. As Rossi describes it, Active Storage's current CDN support downloads the asset to the web server so it can then be cached on the CDN, which doesn't fit Buzzsprout's needs. Rossi says the "next big push" will be improving how Active Storage works with CDNs, and there is still a lot of monkey patching in that area.

Immediate variants: the problem behind the PR

The merged PR came from image variants. When a podcaster uploads episode artwork, Buzzsprout resizes it into two or three sizes. Active Storage's behavior, as Rossi describes it, was to schedule a job to create those variants. But as soon as artwork is published, podcast apps polling for new content request it, possibly thousands of times depending on the show's popularity. Each of those requests triggered the job to create the variants. The PR's original name was along the lines of "immediate variants": the variants have to be generated right away, before the asset starts being served, because it will be requested immediately and heavily.

Audio processing and a 600 MB WAV file

Audio is handled by Buzzsprout's own tooling, built before Active Storage existed, even though Active Storage's documentation mentions processing audio and video. Podcasters can upload any format, and Buzzsprout converts it to MP3 using industry standards so users don't need to understand audio preparation. The processing uses FFmpeg.

Rossi recalled that within the first couple of weeks after launch, someone uploaded a 600 MB WAV file for about a 30-minute podcast. They hadn't anticipated it. The server struggled, and because they are "not the server guy," it took time to realize the huge file had used up the disk space. Rossi took it as proof that Buzzsprout had reached its intended audience of people who aren't geeks and don't know the difference between an MP3 and a WAV, which is why post-processing matters.

Uploads now go directly to S3 through Active Storage. Rossi says the team sometimes sees slowdowns, which they attribute more to Amazon, and there are edge cases with podcasters outside the US depending on where the bucket is located. Overall they call direct upload a really nice feature: the file doesn't touch Buzzsprout's server until it's in the bucket as a key, which makes processing easier. Robby and Rossi recalled the old days of adjusting web server limits for large uploads, moving files manually, and building Ajax progress bars that failed at unknown points. Rossi's summary: "so much of that we get for free now."

Few gems, and owning the payment stack

Asked about gems they depend on, Rossi said the list is short. Buzzsprout uses the Stripe gem to talk to Stripe, but Rossi wants to control credit card processing and be able to switch vendors. They have been burned before: Buzzsprout depended on an FFmpeg gem that stopped being maintained, and the reaction was "I'm just going to do it myself." Billing predates Stripe. Buzzsprout originally used Authorize.net, so logic for subscriptions, declined cards, and retries already lives in the company's own library. If they started a new project today, the gem Rossi would reach for is VCR, which records web interactions so tests can replay them without calling real APIs. It has been useful on both Tick and Buzzsprout.

Robby raised vendor-specific columns like stripe_id and the difficulty of adding processors such as PayPal for customers without credit cards. Rossi said the goal is to be able to switch. Stripe is hard to resist because its API and documentation are so good, but Rossi calls it expensive and says it "know[s] they own you." Rossi is glad subscriptions aren't managed there, while acknowledging that actually switching processors may never happen.

Donor Tools, which helps nonprofits track donations, was where Rossi deliberately resisted Stripe and used WePay. After Chase acquired WePay, the team expected good things, but Rossi says Chase shut it down about two years later, forcing a move to Stripe. Because Donor Tools was written around 2015, much later than Buzzsprout's 2008–2009 code, it was more abstract. Stripe-specific columns lived in a delegated type table for that processor, although, Rossi notes, delegated types didn't exist in Rails yet at the time. They call it a good case study in making merchants swappable. Buzzsprout doesn't have that structure, and a switch there "would be painful."

How the storage data migration was done

Returning to the Paperclip migration, Rossi recalled, without being fully sure of the details, that Buzzsprout had millions of assets. The team duplicated them into a new bucket for Active Storage so either system could run. The difficulty was reconciling uploads and deletions that happened after each copy run, which required a "true up" done with scripts from the console. It wasn't a gradual rollout to a percentage of customers; it worked or it didn't. Rossi admitted "it was broken for a while."

Rossi pointed out that many requests to Buzzsprout come from machines, not people, which gives the team some slack. If RSS feeds have issues for a while, users aren't directly affected, though feed readers may be. Rossi once received an email from Amazon, which ingests podcasts, asking whether Buzzsprout had changed something.

RSS polling, Apple, and two lines for ETags

According to Rossi, Apple hits Buzzsprout's RSS feeds an excessive amount, sometimes thousands of times an hour for a single feed that rarely changes. Rails supports conditional GET requests, but clients have to honor them, or they download and parse the whole feed every time. Rails lets you set ETags: the client sends the last ETag it saw, and the server returns the full feed only if it changed, otherwise a 304 Not Modified. Clients that respect this can just check the CDN. Rossi says Apple runs two ingestion engines, for reasons Rossi doesn't know, and only one uses ETags. They have raised the energy waste with Apple.

Rossi presents ETags as an example of benefits that come from staying vanilla. When bandwidth costs from feed polling were climbing, Brian told them it would take "two lines" to add an ETag. As soon as it was deployed, the team saw 304s. Rossi's point: "I'm not a genius for doing that. It just came with Rails." Robby added that another podcast host they use doesn't send ETags, which forces them to re-fetch every episode to rebuild a static site.

A decade of analytics data outgrowing summary tables

Rossi tells other SaaS founders, "don't solve problems you don't have," while noting they probably borrowed the phrase from David or Jason. Then 2020 brought all those problems at once, including scaling stats.

Buzzsprout has collected stats since its 2009 launch, originally a single download count per episode, which Rossi thinks was stored in a separate table. In 2014 the company launched advanced analytics: location from IP address, which app (Apple Podcasts, Spotify, and so on), and more. That meant storing every play in a table that never stops growing. Rossi says the usual hot/cold data distinction doesn't apply here. Nobody cares about server metrics from a month ago, but a podcaster does care how many downloads a 2015 episode got, so old data has to stay available.

Rossi went to RailsConf asking people how to solve this, hoping for a magic bullet, and didn't find one. That's why they want to pitch a Rails World talk on the subject. Their own solution was hourly summary tables: each hour a job summarized recent data into tables such as episodes by date, by location, and by user agent. It worked until those tables also grew too large. Adding CPU got them through 2020, and moving into Amazon's ecosystem with Kubernetes allowed some horizontal scaling. But at one point the hourly summary job took close to an hour, and a job that runs hourly can't take longer than that. The team was constantly fighting it.

ClickHouse and thinking in OLAP

Rossi says the real shift was understanding OLAP databases; ClickHouse is simply the one they chose. As a self-described relational database person, Rossi explained the difference. Relational databases favor separate tables, joins, and indexes. OLAP databases are about columns: you put data in and they are very efficient at retrieving and aggregating it (counting, max, sums). You have to learn to write queries in a way that performs well.

Buzzsprout ran ClickHouse alongside the summarizer, which was still running at the time of recording because some queries still used the summary tables. Rossi expected to move fully off it in the next six-week work cycle.

Robby described a pattern Planet Argon often sees: slow dashboards lead to summary tables, cached columns, background jobs, jobs limited to recently updated records, and even jobs triggered when someone visits the site, so data is precomputed for users who may never log in. Rossi said Buzzsprout reached the end of those techniques and needed a new tool, which is where OLAP came in. It was intimidating, but Rails' multiple-database support made it feel natural to reach into a second database and get Active Record objects from an OLAP store.

How data gets into ClickHouse

Much of the solution came together at Tropical Ruby, where Rossi set out to get an answer from people there. They kept hearing "OLAP," and many immediately suggested a Kafka-style streaming pipeline. Rossi says Buzzsprout may end up there eventually but chose something simpler.

Buzzsprout already processes logs continuously and writes play data to its relational database. That includes Ruby logic to make sure a stat is real: filtering bots, collapsing repeated requests from the same IP within a time window, and similar checks. The job that used to build summary tables now assembles a large batch and pushes it into ClickHouse, roughly 50,000 rows an hour according to Rossi. ClickHouse handles the inserts while continuing to serve reporting queries. Rossi says this works well for now. Robby mentioned that another guest described using BigQuery on top of a production database for a similar purpose.

Multiple databases: two lines of config, many rewritten queries

Rossi encouraged listeners not to be intimidated by multiple databases, and credited the community, which Robby identified as Shopify, with compressing the complexity so that connecting took about two lines of configuration. When Robby called the change almost "boring," Rossi said it isn't boring yet. Connecting was two lines, but every query had to be rewritten and rethought.

Rossi's example is denormalization. In a relational database you avoid repetition and join to other tables. In OLAP "it's one big table." The user agent column sits next to a podcast app column even though the app is derived from the user agent, and a location ID sits next to city, state, and country. Rossi says this enables queries they couldn't run on summary tables because those queries wouldn't perform.

Staying small on purpose

Rossi goes further than being comfortable staying small: they want to. They enjoy running a small business, and they credit Rails' productivity for making it possible. With AI added, they call it "a multiplier on top of a multiplier," something that feels like "cheating." Rossi says larger competitors with more people but fewer podcasts don't believe Buzzsprout's team size, and competitors of StreamCare, which has one developer, reportedly have hundreds of people. Rossi calls Rails "a superpower."

The downside Rossi identified is support coverage. They've learned that a great support team is "a massive feature" that involves no code: invest in good people and let them advocate for customers. Customers love Buzzsprout's support team. But Rossi doesn't want staff spread across the world, so matching something like Basecamp's around-the-clock support in local work hours is hard for a small team.

Build vs. buy, and AI inside Buzzsprout

Rossi says they don't have a formal ethos on building versus buying beyond disliking dependence on other companies for mission-critical things, such as recurring billing. They were clear that if they started a SaaS today, they would absolutely use Stripe and Stripe subscriptions, because you can always bring it in house later. Buzzsprout is now at a stage where it can.

The AI features are an example. When a podcaster uploads an episode, AI listens to it and drafts titles, descriptions, chapter markers, social posts, and a blog post, which helps podcasters with the last steps before publishing. Initially Buzzsprout sent the audio to a vendor that returned these assets. Once customers adopted the feature, Buzzsprout brought it in house. Rossi says that gave them control to improve it, removed the dependency, and allowed them to lower pricing.

Robby asked when episode descriptions could get longer. Rossi said that is driven by Apple, which still has a "stranglehold" on podcasting after long being its benevolent dictator. Rossi mentioned talk of extending the limit and letting Apple catch up. Robby wants richer descriptions mainly for websites, search, and AI indexing. Rossi said the value depends on the podcast, since some shows naturally come with supporting links and reading material.

Rails as the secret sauce

Asked to sum up, Rossi said everything discussed shows Buzzsprout moving faster and more nimbly by sticking to vanilla Rails and using new framework features as soon as they appear. They added caching as an example. Fragment caching wasn't available when they started with Rails and is now easy. With Solid Cache, Rossi says, Buzzsprout has very few evictions, so much of its content is served directly from cache, a benefit they say they got "for free" from smart people building it into the framework.

For reading, Rossi recommended Sandi Metz, including Practical Object-Oriented Design (which they have read multiple times) and Metz's talks, calling Metz the biggest influence on how they code. They also cited everything 37signals has published, including the Rework material. Rossi says Higher Pixels has settled into its own version of those ideas. Both hosts said they miss seeing Metz at conferences.

Five years out: AI and development

Asked what they believe now that more people will agree with in five years, Rossi said they think developers already broadly agree that AI makes coding better, as a partner for discussing ideas and strategies, and that it makes the work fun. They said it used to be controversial but doubted it still was. Robby, who describes themself as a former skeptic, said it depends on which bubbles you're in. Robby has seen people abandon open source projects over AI use, even when maintainers are transparent about it, and asked where the line would even be, for example whether editor autocomplete counts. Rossi said that binary "has to go away" and predicted that in five years nobody will talk like that. They compared it to requiring people to disclose that they used Google. Robby didn't commit to that prediction and suggested checking back in five years, and Rossi agreed.