Scaling Ingestion Without Breaking Customers: How Ascend Runs a Rails Monolith for Insurance Money Movement

Open on YouTube ↗
Overview

In this episode of On Rails, host Robby Russell talks with Eddie Galindo, an engineering lead at Ascend, and Kagen Hearn, a senior software engineer there. Ascend builds software that helps insurance agencies move and reconcile money. Five years in, the whole platform still runs as one Rails monolith. Eddie has been there since the start. Kagen joined a few months before the recording, just as one product line began onboarding much larger customers.

30 min read

The conversation keeps returning to one question: what happens to a startup-speed Rails app when enterprise-scale customers arrive, and how does a small team find and handle the real limits? The guests describe a pragmatic approach. They stay on Rails conventions, keep infrastructure minimal, and add complexity only once there is a concrete reason for it. The most detailed part covers how they found the limits of their data ingestion pipeline, and why the final limit turned out to be their customers' own APIs.

What Keeps Them on Rails

Eddie started with Rails in the last year of college and just after graduating. He then spent several years away from it. At LinkedIn he did mostly front-end work, and he lived through that company's move from YUI to some jQuery, then to Ember.js, with discussions about React starting as he left. After that he joined a startup that used only Node.js. He said it was nice to come back and find Rails essentially the same. He had bought into the Ruby and Rails philosophy, and its principles still resonate with him.

Kagen came to Rails more recently, at their previous job at Midesk. Before that they had worked with Node.js and Python/Flask on the back end, and with Python, Scala and SQL during a data engineering phase. What Kagen values is that Ruby and Rails "lean into their own design principles rather than trying to hedge against them." In their view, Ruby is a dynamic, object-oriented language that doesn't try to be anything else, and that commitment gives it expressiveness and allows elegant designs.

What Ascend Does and the Scale Behind It

Eddie described Ascend as a company founded about five years ago to build a financial platform for the insurance industry. It focuses on money-related problems. For small and medium-sized agencies, it helps collect and disburse funds. For enterprise customers, it supports workflows for operating bank accounts and reconciling money such as incoming commissions. Asked about the business model, Eddie named a few channels: SaaS subscriptions, a financing product that helps people finance the policies they buy, and revenue tied to money transactions. The engineering team is about 20 people, the largest it has been.

Kagen works on the SaaS product for direct bill reconciliation, which serves accountants at insurance agencies. In direct billing, the carrier that underwrites a policy collects the premium and later passes the commission to the agency. The agency receives the commission deposit asynchronously, and the statement describing it can arrive before or after the money. Accountants then have to match deposits against statement entries to work out where every dollar came from. Ascend's product helps with that matching.

The guests would not discuss dollar figures but did share volumes. According to Kagen, Ascend has recently processed tens of thousands of statements over a few months, and a single statement can contain tens of thousands of entries. The number of reconciled statement entries has more than doubled in the past few months as new customers came on. Kagen expects it to grow by many more multiples, because many of those customers are just starting to ramp up and more are in the pipeline. The main scaling factor is the number of statement entries ingested and processed, which Kagen said is "growing by the millions."

Why Rails, and Whether You Need Rails Developers

Eddie gave a plain answer on why Rails was chosen. One co-founder had used Rails at Instacart, and it was the easiest way to get started, so they bootstrapped the app with it. What mattered early on was speed to a working product. The team didn't want to make "a hundred technical decisions" that would be irrelevant if the company didn't survive. They wanted to focus on the product and on whether they were building the right thing.

Robby asked whether a new startup needs to hire experienced Rails developers. Eddie said Ascend didn't. At the start, perhaps 30% of the team knew Rails. The team formed because people had worked together at a previous startup and wanted to keep doing so. For those new to Rails, the framework was easy to pick up. Eddie attributed this partly to a deliberate choice not to deviate from a typical Rails app and to rely heavily on the official guides and other public material.

Eddie estimated that 80–85% of the work can be done by looking up how the guides do something. Things like validations or returning a JSON response are settled patterns the team no longer has to decide on, so the interesting problems are on the product side. When Rails doesn't cover something, a gem usually does.

Kagen confirmed this from the newcomer side. They were "pretty impressed" by how closely the codebase followed Rails conventions. Things were generally where they expected them to be, which made onboarding easy, especially for someone who already knew Rails.

Why the Platform Is Still One Monolith

Ascend runs one monolith for what it calls its core platform. Eddie explained that the product is meant to be a platform, with customers using different parts of it, and that a monolith has been much easier to manage than multiple microservices.

The different products share a lot at the foundation. Ascend integrates with external systems and normalizes the data it pulls in, and every product benefits from that normalization. User management and organization management are also shared. Eddie also stressed infrastructure and compliance. For a small team handling money, not having "a hundred applications" to monitor closely matters. His stance is to push the monolith as far as possible and revisit the decision once it becomes hard to manage.

Ascend has five teams, one for each of its five products, and they are roughly evenly sized, though newer products have fewer people. Each team owns its product. Shared platform code is a collective responsibility that everyone is expected to keep clean. Eddie acknowledged that this arrangement may break down as the company grows, but said it has worked so far.

Robby asked Kagen whether the shared monolith feels like a superpower or a constraint. Kagen said it currently gives more benefits than constraints. In their view, many benefits of microservices are organizational: teams can deploy their own domain independently and isolate dependencies. Ascend is not at that point, because many models are common across the insurance domain, whether the work is agency bill, direct bill or another accounting task. Policies, lines of business and clients are shared everywhere, and the teams benefit from extending those models in one place instead of updating them across services.

Eddie did name one downside later. Direct bill reconciliation is the product pushing the platform's scalability and performance limits. Because everything lives in one app, the team has to watch infrastructure concerns that only one part of the system really needs.

When Bigger Customers Arrived, Ingestion Assumptions Broke

Kagen said that until recently, direct bill had two relatively large organizations as customers, on a product that had been "fairly hastily put together" a year or two earlier. They described the tension as "trying to ship at startup speeds for large enterprise customers." Once many new customers began onboarding, earlier assumptions started to fail, especially around ingestion.

To serve a direct bill customer, Ascend has to ingest their policies, lines of business, clients and other data from the customer's agency management system (AMS). Kagen said most of these systems have been around a long time, some old enough to use XML and SOAP services. When the wave of new customers arrived, Ascend could process roughly 500,000 to one million new policies per day. The limit was latency. Ingestion means many slow external requests, and each one holds a Sidekiq thread for the whole wait. The number of Sidekiq workers therefore capped how much volume could be processed per day.

Finding the Real Limit: Postgres Connections and PgBouncer

Robby asked why they didn't just add more workers. Kagen explained that the team didn't know the actual scaling boundaries, because it had never needed to ingest more. The first step was to find out. Outside business hours, they gradually increased the number of workers and watched what happened.

The first limit was database connections. According to Kagen, Sidekiq takes a database connection for each thread the first time that thread does database work, and all these workers do database work. As they added Sidekiq workers, they quickly approached Heroku Postgres's connection limit.

The fix was transaction pooling. The team added PgBouncer on the client side as a sidecar, but only for the Sidekiq workers doing ingestion. Kagen explained that transaction pooling rules out some Postgres features, and checking the whole application for code that depends on them would be a heavy lift. Limiting PgBouncer to ingestion workers helped a lot without requiring that audit.

Inside the Ingestion Pipeline: Change Events on a Cron

Kagen then described how ingestion works. A cron job runs every 10 minutes and polls each customer's API for change events, meaning which policies changed within a configurable lookback window. For each change found, Ascend writes a change-event record to Postgres.

A second scheduled Sidekiq worker picks up those change events and pushes them onto processing queues, mainly the policy sync workers. This middle step gives the team one lever for concurrency: they can control how many change events this worker picks up at a time. A backlog can build up and be worked through as fast as needed or possible. Policy sync workers can also trigger secondary work, such as syncing transactions related to a policy. All customers run on the same shared resources simultaneously.

Robby asked what happens if a policy changes just after a poll. Kagen said it's fine for processing to happen later. The lookback window is deliberately longer than the 10-minute interval so nothing is missed. There is a daily sync covering the past day, and the 10-minute polls look back further than 10 minutes. Kagen wasn't sure of the current setting but thought it was about the past hour.

When the Customer's API Becomes the Bottleneck

After transaction pooling fixed the connection problem, a new limit appeared: customers' own APIs could only take so much ingestion traffic. Kagen said that too many concurrent policy requests led to 504s and other server errors. In the worst case, Ascend could disrupt a customer's own service if it wasn't careful. Simple retries were not acceptable, because the goal was to avoid overloading the customer.

A Concurrency Rate Limiter Inspired by Stripe

The solution was a concurrency rate limiter based on a 2017 Stripe blog post about rate limiting, specifically its section on concurrency limiting and the sample implementation Stripe published as a GitHub gist.

Kagen described how it works. Each integration with outgoing requests has a set in Redis. Before a worker makes an outbound request, for example to fetch a policy, it must acquire a slot, which means adding an item to that set. Each organization has a configured concurrency limit, the number of simultaneous requests its system can handle. If the set is at or above the limit, the worker can't get a slot. It retries about every 100 milliseconds, eventually times out, and re-enqueues the job.

This design has consequences for how jobs are written. A job can fail to get a slot at any point, so every enqueued job must be idempotent. In rate-limited tasks, the team tries to put one request in each job so that each job has one side effect. Kagen noted these are general Sidekiq principles but not always easy to follow. A policy sync tries to acquire a slot at the start. If it fails, the job goes to the back of the queue, so other organizations keep moving while one is throttled. The transaction-sync jobs it triggers are rate-limited as well.

The limiter is written as a Ruby class used with a context block, so it is opt-in. Requests from user-facing paths, such as a user clicking a button that triggers an external API call, are not limited and don't use slots. Kagen's reasoning was that user-driven traffic doesn't put customer APIs under the stress that bulk ingestion does.

Eddie added that none of this was designed upfront. The pipeline began as "just a worker that's going to make an API call" to ingest data, and the team kept running it until it hit a limit, then dealt with each limit as it appeared.

Robby asked how the limits are set. Kagen said new customers start with a safe, conservative value. If more ingestion volume is needed, the team estimates what the customer can handle based on its size and whether it runs on-premise or on shared infrastructure. Some customers can only take very little traffic, so their limits have to be lowered. Robby joked about asking customers to just buy more hardware. The guests laughed it off, and the topic ended there.

Feature Flags, Organization Configuration, and the God Object

Kagen confirmed that feature flag and configuration complexity has grown a lot in recent months as large customers arrived. They see this as natural in enterprise B2B software. Each customer wants its own version of the product, with its own accounting workflows and ideas about reconciliation. Some have acquired other agencies, which changes their processes too. The team tries not to keep adding conditionals, but Kagen said a lot of them are unavoidable.

The team uses two mechanisms. Feature flags, managed in LaunchDarkly, gate actual product functionality: which views and UI buttons a customer sees. Organization configurations live in the database and adjust behavior of features every customer uses. Kagen's example is statement matching in direct bill reconciliation. Each statement entry has to be matched to the policy it belongs to, and every organization has its own matching rules, which can differ by context. Organization configuration lets each customer express those rules.

Robby asked whether this becomes a growing god object. Kagen said yes, it currently is one. Each setting is a real column on a database model that belongs to the organization, not a JSON blob. Kagen said "thankfully" to that, and noted that needing a migration for every new setting adds friction that somewhat discourages adding more. Eddie added that the team is considering splitting configuration by domain, for example payment configuration and reconciliation configuration, so it isn't one huge object. The application is multi-tenant, with organizations' data separated but stored in a single database.

Eddie explained the rule for choosing between the two. Feature flags are for short-lived things. With enterprise customers, a feature may need customer approval or a slower rollout, but the goal is for everyone to get it eventually, after which the flag and its branching are removed. Organization configuration is for things that permanently change business logic, such as how granular reconciliation should be. Those settings are part of how the product works and will stay.

When Customer Flexibility Becomes Technical Debt

Robby asked Eddie how to tell when customer flexibility turns into technical debt. Eddie said everyone struggles with this. The team tries to push back and think about the product in general terms instead of building one-off features. It tries to see the request from the customer's side and from Ascend's side: does this belong in the product long-term, or can the customer adopt a different process? He admitted they don't have all the answers. Sometimes a request makes sense and they build it. It is decided case by case, with attention to what makes sense for all customers and why a customer is asking.

This works partly because engineers and product people often join customer calls. That lets them work through the actual need instead of receiving a promise already made by sales. Kagen said the same was true at previous SaaS companies. The largest contracts got more custom work, but before agreeing to anything custom, the team asked whether it would generalize to others. They usually pushed back on very bespoke features that would never apply to anyone else, though there were exceptions.

Convention Over Configuration and Duck Typing

Robby asked whether Rails gives enough tools for this kind of per-customer customization. Kagen disagreed with the common criticism that convention over configuration makes custom work harder. They think that criticism applies more to technical choices than to product work. Because conventions make it quick to build something new, it is also quick to build something bespoke.

Duck typing helps too. In Kagen's explanation, duck typing means an object's type is its interface, the methods it responds to, not declared type information. When a customer needs something odd built on shared functionality, the team can write an adapter or a class that matches the interface an existing module expects, or override a method, and reuse most of the existing logic. Kagen said this helps "with hacky things like that in particular."

Robby raised the concern that this can feel too magical and be hard for newcomers to debug. Kagen agreed it is a real trade-off. You can write such code in a way that hides what's going on, especially if you ignore Rails conventions. Conventions help you find things and reason about how they work. When you drop them, the "magic" can take over. Kagen sees this as part of the cost of Ruby and Rails fully committing to duck-typed object orientation. Eddie had nothing to add.

Heroku's Uncertain Future and Tuning Puma

Ascend runs on Heroku. On Heroku's long-term plans, Eddie said the team is as confused as everyone else. He mentioned seeing on Reddit that Heroku had posted about going into maintenance mode while still releasing features. The team has started looking at options and will move if it has to. It is hitting limits in some areas, but hasn't yet done the work of evaluating alternatives. Eddie mentioned that their autoscaling provider said it would evaluate vendors and publish recommendations for Heroku-like platforms. He didn't know if that had come out yet and said the team will do its own evaluation when the time comes.

Eddie also described a Puma tuning change. Requests were being accepted and then sitting idle, so the team looked at using more of Puma's concurrency. It turned out to be an easy configuration change: raising concurrency improved requests per second. Higher concurrency also used more memory on their Heroku dynos, so they switched to jemalloc for Ruby memory management. Eddie said both changes have worked well so far.

They tested these changes in a Heroku sandbox configured much like production. Eddie said they had learned the hard way: an earlier attempt to turn up concurrency made the application stop working.

Eddie credits sticking close to a standard Rails app with keeping Ruby and Rails upgrades easy. With coding agents, he said, you can ask for a version bump, have the agent run the tests, and then do the team's own verification.

AI Tooling, Testing at Volume, and Local Seeding

The team hasn't standardized on AI tools. Eddie said most engineers use Claude Code, but nothing is required. Robby noted the recording took place in the second half of June, and that the tools change quickly.

On simulating realistic data volumes, Kagen said the team has seeding rake tasks. As far as they know, it hasn't invested heavily in AI-specific guardrails such as setups that let an AI fully develop and verify its own work. Kagen personally uses AI a lot for that purpose, though. Their example: customers can export any list view to CSV, and at current volumes, building the CSV in memory became very slow or even crashed. Kagen rewrote the builder to stream. To test it, they asked Claude to open a Rails console, create a statement with a million transactions, and generate a CSV, to check that memory use stayed stable and that latency dropped reasonably.

Eddie said the sandbox contains only mock data, and the team relies much more on its automated tests than on clicking through a sandbox. The Rails app mainly serves a JSON API. For data-scale concerns, the team writes benchmark scripts and measures memory pressure in different places. Eddie said a test-first approach is what mainly keeps bugs out.

A JSON API, a Next.js Front End, and Over a Billion Sidekiq Jobs

The JSON API is used both internally and by customers through a public API. Responses are built with Active Model Serializers. The customer dashboard, where users do most of their work, is a Next.js application. Eddie said that decision was made early by the engineer who owned the front end. He didn't remember whether Hotwire existed yet, but at the time building dynamic UI in Rails seemed harder, and so did hiring engineers for it. He agreed with Robby that things have improved since.

Sidekiq has processed about 1.3 to 1.4 billion jobs. Eddie called it a very important part of their infrastructure, and the team is on the Enterprise plan.

How the Team Vets Gems and Dependencies

Eddie said the team's approach from the start has been that any gem it brings in is code it owns, even though it lives outside the repo. The team has to understand it during upgrades and debug it when something breaks. So they avoid pulling in a complex gem to solve a small problem. They are comfortable with established, widely used gems. PaperTrail, used for model audit logs, is one he named.

Robby asked about abandoned or blocking gems. Eddie said they have been lucky: when they needed a fix, a PR usually already existed upstream, so they would use their own fork until it was released. He thinks they are down to maybe two forked gems. The team rarely adds new gems now. PgBouncer was the most recent new dependency he could think of, and the application's problems tend to be similar enough that a small set of gems covers them. Keeping existing dependencies from being compromised is something he said is on their minds, especially given the industry they are in.

Running Elasticsearch Alongside Postgres

Ascend has two main data stores, Postgres and Elasticsearch. Kagen explained why. Customers have millions of policies, statements can have tens of thousands of entries, and users eventually want to filter or sort by almost any field on a transaction. Elasticsearch handles that well on large data sets, and the number of supplier statements will keep growing without bound. Postgres serves detail views. Elasticsearch serves list views with filtering, sorting and search.

Keeping the two in sync is an ongoing challenge. Documents are serialized into Elasticsearch with Active Model Serializers, which causes two problems. First, when a document includes values from related models, updating a related model has to trigger reindexing of the parent document, or list views and filters will be wrong. Second, serializers often have N+1 queries, which can load the database heavily and slow the queues.

Kagen said inconsistencies can reach users if they are bad enough. Because list views come from Elasticsearch, a stale document shows an old value, and a new record might not appear at all, while the detail view shows current data. The UI then disagrees with itself. This can happen when indexing queues back up or when someone forgets to reindex from a related model. Kagen said users tend to notice quickly and report it. Model callbacks on save and update that run the related serializers help, but dependencies between models are still hard to track.

Sidekiq Batches, State Machines, and ParadeDB

Some flows are designed for eventual consistency, and the UI is blocked while indexing finishes. Kagen used supplier statement extraction as an example. A PDF statement arrives, AI parses it, and each entry becomes a new record in the database. During this, the statement is marked as extracting or matching. Sidekiq batches coordinate the follow-up work. For example, matching each entry to its policy runs automatically after extraction. When all jobs in the batch are done, the batch callback unblocks the statement for users. Extraction states are managed with a state machine using AASM. Eddie said these patterns are used throughout the application because the team relies so heavily on Elasticsearch.

Robby asked about alternatives to Elasticsearch. Kagen said the team has recently started looking at ParadeDB, a Postgres extension, but hasn't decided anything, and that it is a broader organizational decision. The appeal is that indexing would happen automatically when records are saved to Postgres. Kagen also mentioned that Modern Treasury, which does similar money and accounting work, uses ParadeDB for Elasticsearch-style work.

Do LLMs Work Better in Typed Languages?

Asked about using LLMs with Ruby, Kagen described a growing consensus that LLMs work best with strongly typed languages, because types provide guardrails and automatic checks. Kagen offered a counterpoint. Rails conventions and Ruby's expressiveness also suit AI well, because they give LLMs a ready-made set of rules that keep them "on the rails" at low token cost. In Kagen's experience, LLMs don't create abstractions upfront, plan for scale, or set conventions on their own. They do exactly what they are asked. Existing conventions fill that gap, and fewer tokens spent means better reasoning. Robby said he wants to see studies on this and that for now these are anecdotes, not science.

The Cost of Writing Code Versus the Cost of Good Software

Eddie said AI has made the team somewhat more efficient. One benefit he likes is that people spend more time explaining what they want to do and why, so intent is clearer upfront. He treats AI output as if it came from a colleague he is working with. He opposes committing code and hoping someone else reviews it. Whether you wrote it or a tool did, you are expected to understand it. He compared it to copying from Stack Overflow: you still had to understand the code.

Kagen cited a recent post from Ashby's engineering team on AI and the future of engineering. Its thesis is that the cost of writing code is approaching zero, but the cost of producing meaningful software is not. The skills that set good engineers apart, such as judgment, taste and understanding customers, matter more now. As Kagen summarized it, LLMs make it easy to skip the thinking, and because they produce plausible but slightly wrong code or wrong patterns, engineers need to think harder than before.

Robby said teams vary widely. He has talked to people who barely review AI-generated code, which he finds surprising, and others who are very cautious. Kagen said the answer depends on context. If being wrong is cheap, fully vibe-coding and shipping with little review can make sense, because you can fix it and ship again. If failure is costly and you need it right the first time, it doesn't. The Ashby post describes a spectrum: full vibe coding for low-stakes work like throwaway scripts and internal tools, which Ascend does use for internal tooling, and human-driven work with AI as an assistant when stakes are high. Kagen said the stakes at Ascend are often high because the software handles money for accountants who care about details. So the acceptable level of AI use varies even within one company.

Technical Decisions They're Glad They Made

Asked which decisions besides Rails have served the team well, Eddie pointed to minimal infrastructure. From the start, the team wanted to spend its time on the product. That doesn't mean ignoring infrastructure, but dealing with it when it matters. He summarized it as not doing "engineering for the sake of engineering." Managed services may cost more than running everything yourself, but without a dedicated infrastructure or DevOps person, simplicity is what lets the team keep moving. His example of complexity that is justified: the work on data ingestion and Elasticsearch now makes sense because customer volume grew, but before that it would have been unnecessary.

His second choice was enforcing standards and a testing culture from early on, meaning tests for everything, especially back-end code that handles money. He said this has prevented many production bugs while still letting the team move fast. The test framework is RSpec. Eddie called that choice "probably debatable," but said they have too much invested to switch. Robby, who has long used RSpec and recently tried Minitest on small projects, agreed there's no need for a religious war over it.

Robby, speaking from consulting, said he has seen apps with three developers, five repositories, Terraform, and no customers. Eddie agreed that every piece of complexity needs a reason.

For book recommendations, Kagen chose Structure and Interpretation of Computer Programs, the MIT introductory programming book that uses Scheme, and called it "a real nerds book." Eddie recommended John Ousterhout's A Philosophy of Software Design, a short book he said can be read in a day or a week and which he finds valuable for its ideas on software design.

Ascend doesn't have an engineering blog yet. The guests said they have discussed one but never started it, and Robby encouraged them to begin writing about the work they described.