Rewriting cBioPortal's Backend in Go with AI Agents: Prototype and Prompt Analysis

A lot of code is being rewritten by AI agents this year. GitHub ported the Copilot agent runtime from TypeScript to about 800,000 lines of Rust in a few months, mostly with Copilot itself (write-up). Anthropic’s guide to AI-driven code modernization says projects that used to take years “can now finish in months (or weeks).”

Some are controversial. ArtCraft, an AI-built “Adobe-compatible” suite in Rust, launched with the line “I just one-shotted Photoshop.” People who tried it found that text editing and brushes didn’t work, and the Hacker News thread turned into an argument about overclaiming. Getting an agent to write a rewrite is quick now. Showing that it behaves like the original is the slow part.

Bioinformatics has its own wave, mostly into Rust. Heng Li lists many of them in The AI Rewrite Dilemma, from fgbio to fgumi and FastQC to RastQC, and argues that the people side is harder than the technical side: “Empathy matters more than technical benefit or legal correctness.” Anthropic’s guide says much the same: the slow part is getting everyone to agree on what the rewrite should do and how to check it. Phil Ewels at Seqera set out principles for doing it well at rewrites.bio: match the original’s output exactly, credit its authors, and say what you did and didn’t validate.

My own first attempt, in February, was vibe-vep, a Go rewrite of VEP in a single binary. VEP is one of the more expensive parts of keeping cBioPortal’s variant annotations up to date. vibe-vep worked well, but then we learned that Kuan-lin Huang’s lab at Mount Sinai was building a Rust rewrite, fastVEP. So we paused vibe-vep and benchmarked theirs instead. Meanwhile the models kept getting better at this kind of work, so this summer we tried something bigger as a proof of concept: cBioPortal’s own backend.

This post covers what came out of it and how the work went, measured from the agents’ session logs. Whether to roll it out is up for discussion in RFC 106.

Contents: The rewrite · Rewrite compared · How many prompts? · Prompts categorized · Prompts: the new source code? · Lessons

The rewrite

cbioportal-go replaces cBioPortal’s Java REST API and its importer, about 130,000 lines of Java and configuration built up over more than ten years, with Go. Everything else stays: the website, the ClickHouse database and the session service. The diagram shows what changes. It’s an experiment for now, not an official cBioPortal release. In the terms of Anthropic’s modernization guide, it’s a transform: new code in a new language, same behavior.

Java backend (today) React website: cBioPortal/cbioportal-frontend React website unchanged REST API Java API: cBioPortal/cbioportal Java API Spring Boot cached? Redis cache: redis.io Redis cache SQL Importer (Java + Python): cBioPortal/cbioportal-core Importer Java + Python loads Session service: cBioPortal/session-service Session service ClickHouse: clickhouse.com ClickHouse study data MongoDB: mongodb.com MongoDB saved sessions cbioportal-go React website: cBioPortal/cbioportal-frontend React website unchanged REST API Go API: cBioPortal/cbioportal-go Go API in-memory metadata cache SQL Importer (Go): cBioPortal/cbioportal-go Importer Go loads Session service: cBioPortal/session-service Session service ClickHouse: clickhouse.com ClickHouse study data MongoDB: mongodb.com MongoDB saved sessions

Hover over a box to see what it does and what it uses on the public portal. Click it to open its repository.

React website · cBioPortal/cbioportal-frontend

The website people use. It gets all its data from the REST API.

Unchanged: both backends serve the same website build.

Java API · cBioPortal/cbioportal

The Spring Boot REST API that answers the website, scripts and AI agents. About 80,000 lines of Java.

About 1.8 GB of memory per server on average and 0.34 s of CPU per request. MSK runs 20 Java backend servers across its portals, about $1,280 a month.

Redis cache · redis.io

Keeps the Java API's answers for 60 minutes, so repeated requests skip the database.

Answers 65 to 75% of Java's lookups. Its servers cost about $215 a month. The Go version has no response cache.

Importer (Java + Python) · cBioPortal/cbioportal-core

Checks study files and loads them into the database. About 33,000 lines of Java and Python.

Loading MSK-IMPACT (50,000 samples) takes 289 s and 3.9 GB of memory.

Go API · cBioPortal/cbioportal-go

Answers the same REST API as Java, field by field. Keeps study metadata in memory.

About 48 MB of memory per server on average (131 MB at most) and 0.11 s of CPU per request. The servers for all portals would cost about $160 a month.

Importer (Go) · cBioPortal/cbioportal-go

Loads the same study files into ClickHouse. It lives in the same repository and program as the Go API.

Loading MSK-IMPACT takes 80 s and 0.2 GB of memory.

Session service · cBioPortal/session-service

Stores users' saved pages, virtual studies and custom data.

Shared by both backends and not part of the rewrite.

ClickHouse · clickhouse.com

The database that holds all study data, on ClickHouse Cloud.

Both backends use the same data. On the API portal's traffic, Go used 0.92 times Java's ClickHouse CPU per request.

MongoDB · mongodb.com

The database behind the session service.

Unchanged.

What changes: the API and the importer (blue) move to Go, and the Redis cache goes away. The website, ClickHouse and the session service stay the same.

We picked Go because a Go server needs a fraction of a Java server’s memory, builds in seconds, needs few outside libraries, and is plain enough for others to review. I had actually started in Rust. After a day I asked Claude whether Go would fit better, and it said yes, even with part of the Rust version already written. The API mostly turns HTTP requests into ClickHouse queries, so the time goes to the database and Rust’s speed buys little. Rust rebuilds took 10 to 20 seconds. And cBioPortal’s contributors mostly write Java, which makes Go easier to pick up. When I asked again in October, it gave the same answer and added that most of the work had been matching Java in many small ways, where a fast compile loop helps. RFC 106 has the full reasoning.

The API has no complete written spec, so the Java code serves as one: “correct” means “gives the same answer as Java.” We check that in layers, from fast to slow:

  • unit tests for statistics, binning and query building
  • contract tests for every endpoint’s status codes, fields, order and errors
  • the same requests sent to the latest Java version, loaded with the same test studies
  • checks that both importers store the same data
  • cBioPortal’s own website test suite (about 365 Playwright test files), running in a real browser against Go
  • every night, about 5,000 of the previous day’s production requests sent to both backends and compared
  • a share of live traffic copied to Go to measure speed, memory, errors and database load

Rewrite compared

The numbers come from the public portal, cbioportal.org. Its API answers about 36 million requests a month, and on weekdays about a third of them come from scripts and AI agents (see the usage page).

Hover over or tap a tile for what it measures.

Lines of code: The web API and the importer together. Java's count includes configuration. Go's code lives in one repository and has three outside dependencies.
Memory reserved: What each production server sets aside. Java can grow to 12 GB and Go to 2 GB.
Memory used (avg): Average over one day of copied traffic. Go peaked at 131 MiB, while carrying a tenth of Java's load.
Median request: Half of all requests are answered faster than this. Java answers many small repeated requests from its Redis cache; Go has no response cache.
Slow requests (p99): The slowest 1% of requests. Go's p99 is lower on every busy route, but rests on fewer requests than Java's.
Server errors (one day): Java answered all of the day's requests, Go a 10% copy. At Java's rate Go would have had about 6 errors, so zero is encouraging but not proof.
Monthly server cost: An estimate for the 20 Java backend servers MSK runs across its portals, about $1,280, plus about $215 for Redis. Go would fit on two servers, but hasn't been tested at full traffic yet.
MSK-IMPACT import: Loading the largest public study (50,000 samples) from its files. Java's importer used 3.9 GB of memory, Go's 0.2 GB.
Container image: The download size of the server, with the website included.

Speed, memory and errors come from one day in October when Go got a copy of 10% of the public portal’s API requests. Speed compares fairly. Memory, errors and cost are less certain until Go has run at full traffic.

Java’s Redis cache mattered less than we expected. It answers 65 to 75% of Java’s lookups, which makes Java faster on small requests that repeat often. Go has no response cache, yet it matches Java’s typical response time and beats it on slow requests. On the API portal’s traffic it even put slightly less load on ClickHouse per request. Leaving out Redis saves another $215 a month.

In the nightly check of October 12, 4,531 of 4,959 replayed production requests gave identical answers. The other 428 all had known causes, for example a Java bug that Go fixes, or an answer Java itself gives differently each time. None were unexplained. RFC 106 asks for seven clean nights in a row before Go serves real traffic.

The AI tools ran on Claude subscriptions, a few hundred dollars over three months. At Claude API list prices the same work would have cost about $2,000 ($550 in July and $1,400 in October), less than two months of the estimated server savings. 98% of what the agents read was served from the prompt cache. Without it, the bill would have been about $22,500.

How many prompts?

The work took a few weeks of supervised agent time, spread over three months next to my other work. It ran mostly as one long Claude Code session in a tmux window on a server (the setup is in my async software engineering post). That main session handed single tasks, such as one group of endpoints or one speed fix, to helper agents, each working in its own copy of the repository. A change went in only after the tests passed and its measurements were checked.

The work came in two stretches, July and October, with a pause in between. Git agrees: there are no commits from late July until October 2. The chart shows my messages, the agents’ tool calls, commits and the size of the code per day.

Claude Opus 4.8 Claude Opus 5.5 Messages I typed per day peak 94 (Oct 2) 7-day average Agent tool calls per day peak 5,046 (Oct 2) Commits per day peak 100 (Oct 8) Lines of Go on main (tests included) first commit: 10k 77k 11k Jun 30 Jul 6 Oct 1 Oct 5 Oct 12

Hover over or tap a day to see its numbers.

Jul 8 to Sep 30: low activity, 11 messages and 5 commits in total.
Tue Jun 30: 1 messages from me, 7 agent tool calls, 0 commits, Go code not committed yet, Claude Opus 4.6.
Wed Jul 1: 0 messages from me, 0 agent tool calls, 0 commits, Go code not committed yet.
Thu Jul 2: 5 messages from me, 175 agent tool calls, 0 commits, Go code not committed yet, Claude Opus 4.8.
Fri Jul 3: 32 messages from me, 1,297 agent tool calls, 0 commits, Go code not committed yet, Claude Opus 4.8.
Sat Jul 4: 2 messages from me, 412 agent tool calls, 0 commits, Go code not committed yet, Claude Opus 4.8.
Sun Jul 5: 4 messages from me, 76 agent tool calls, 2 commits, 10,402 lines of Go, Claude Opus 4.8.
Mon Jul 6: 0 messages from me, 23 agent tool calls, 2 commits, 10,528 lines of Go, Claude Opus 4.8.
Tue Jul 7: 0 messages from me, 106 agent tool calls, 5 commits, 10,605 lines of Go, Claude Opus 4.8.
Thu Oct 1: 2 messages from me, 824 agent tool calls, 0 commits, 11,206 lines of Go, Claude Opus 5.5.
Fri Oct 2: 94 messages from me, 5,046 agent tool calls, 46 commits, 40,953 lines of Go, Claude Opus 5.5.
Sat Oct 3: 21 messages from me, 806 agent tool calls, 21 commits, 43,828 lines of Go, Claude Opus 5.5.
Sun Oct 4: 12 messages from me, 551 agent tool calls, 9 commits, 46,007 lines of Go, Claude Opus 5.5.
Mon Oct 5: 72 messages from me, 2,529 agent tool calls, 33 commits, 52,991 lines of Go, Claude Opus 5.5.
Tue Oct 6: 19 messages from me, 928 agent tool calls, 29 commits, 54,158 lines of Go, Claude Opus 5.5.
Wed Oct 7: 49 messages from me, 898 agent tool calls, 12 commits, 55,302 lines of Go, Claude Opus 5.5.
Thu Oct 8: 63 messages from me, 4,618 agent tool calls, 100 commits, 71,413 lines of Go, Claude Opus 5.5.
Fri Oct 9: 16 messages from me, 449 agent tool calls, 6 commits, 71,917 lines of Go, Claude Opus 5.5.
Sat Oct 10: 7 messages from me, 25 agent tool calls, 2 commits, 71,984 lines of Go, Claude Opus 5.5.
Sun Oct 11: 4 messages from me, 561 agent tool calls, 17 commits, 72,894 lines of Go, Claude Opus 5.5.
Mon Oct 12: 47 messages from me, 1,054 agent tool calls, 25 commits, 75,961 lines of Go, Claude Opus 5.5.
Tue Oct 13: 34 messages from me, 131 agent tool calls, 3 commits, 76,067 lines of Go, Claude Opus 5.5.
Wed Oct 14: 13 messages from me, 239 agent tool calls, 7 commits, 76,566 lines of Go, Claude Opus 5.5.
Thu Oct 15: 0 messages from me, 60 agent tool calls, 3 commits, 76,626 lines of Go, Claude Opus 5.5.
My messages, the agents' tool calls, commits and code size over time, and the model the agents ran on. Lines show the 7-day average in October. The zigzag skips July 8 to September 30, a stretch of low activity.

My messages fall on 24 days. If messages less than 30 minutes apart count as one sitting, that’s about 38 hours of typing to agents, much of it from my phone. Reading results or clicking through the website without typing isn’t counted, so my real time was higher.

Here is what the session logs show:

Hover over or tap a tile for what was counted. The counts cover the main sessions and their helper agents in July and October.

508 messages I typed: Everything I typed to the agents, including messages sent while an agent was busy. Slash commands, automatic notices and scheduled prompts aren't counted.
18,501 agent messages: Replies and steps from the main sessions and their helper agents. One message can hold text and several tool calls.
20,883 tool calls: Every action an agent took: running a shell command, reading or editing a file, starting a helper. 17,174 of them were shell commands.
147 helper agents: Tasks the main session handed off, each working in its own copy of the repository.
1,266 build commands: Runs of go build, go vet and make build.
1,544 test commands: 1,103 unit test runs and 441 end-to-end runs, including cBioPortal's website test suite in a browser.
354 commits: Commit commands the agents ran. Git ends up with fewer, because some commits were amended or squashed before merging.
187 pushes: Pushes to GitHub, mostly to branches for pull requests. The agents opened 31 pull requests.
62 rebases: Rebases to bring a branch up to date with main before merging.
12 context compactions: Times a session filled its context and Claude Code summarized the history so it could keep going.

For every message I typed, the agents sent about 36 of their own.

Prompts categorized

GitHub’s Copilot write-up sorted its engineers’ messages to the agents by main purpose. I sorted my 508 messages into the same categories. An LLM did the labeling and I checked a sample by hand.

Status or explanation request: 89 messages (17.5%) Status or explanation request 18% (89) Approval or short answer: 76 messages (15.0%) Approval or short answer 15% (76) Technical or design challenge: 72 messages (14.2%) Technical or design challenge 14% (72) Scope clarification: 60 messages (11.8%) Scope clarification 12% (60) Git, PR and session logistics: 42 messages (8.3%) Git, PR and session logistics 8% (42) Kickoff: 40 messages (7.9%) Kickoff 8% (40) Resource and fleet coordination: 33 messages (6.5%) Resource and fleet coordination 6% (33) Review, testing and CI: 33 messages (6.5%) Review, testing and CI 6% (33) Forward pressure and completeness: 24 messages (4.7%) Forward pressure and completeness 5% (24) Course correction: 20 messages (3.9%) Course correction 4% (20) Other: 19 messages (3.7%) Other 4% (19)

Hover over or tap a bar to see what the category means and an example message.

Status or explanation request: Asking what happened or why.

For example: "What's still missing before the API portal can switch?"

Approval or short answer: Replying to the agent's question.

For example: "Sounds good, go ahead."

Technical or design challenge: Questioning or proposing an approach.

For example: "I am considering doing this in Go instead of Rust. What do you think?"

Scope clarification: What's in or out, and what it should look like.

For example: "Keep the benchmark short and add a table of contents."

Git, PR and session logistics: Branches, commits, pull requests, crashed sessions.

For example: "Commit your work and open a pull request."

Kickoff: Starting a new task.

For example: "Add CI tests to the repository."

Resource and fleet coordination: Servers, secrets, DNS, helper agents.

For example: "I added the Docker Hub secrets."

Review, testing and CI: Reporting what's broken or reviewing results.

For example: "Several study view tables don't load."

Forward pressure and completeness: Pushing to keep going or finish.

For example: "Port the rest of the endpoints."

Course correction: Telling the agent it's on the wrong track.

For example: "It works in production, so it must be a backend bug."

Other: Anything else.

For example: "One-off commands and IDs."

What my 508 messages to the agents were mainly about. Examples are paraphrased.

A third of my messages were check-ins: asking how a task was going (18%) or answering yes or ok (15%). Another quarter steered the work, by questioning a technical choice (14%) or setting the scope (12%). Just 4% told an agent it was on the wrong track.

The mix is quite different from GitHub’s. Their three biggest categories, review and testing (31%), technical challenges (17%) and pushing for completeness (15%), make up 63% of their messages and a quarter of mine. Only technical challenges come out about the same. My agents ran the tests themselves, and my own checking happened in the browser.

Prompts: the new source code?

When an agent writes most of the code, people want to see your prompts as much as your code. Did you let it run wild, or did you question the language choice, the data model, the shortcuts it proposed? When I present this work, people ask the same questions I asked the agent along the way.

Tools are starting to keep this record next to the code. Claude Code links the session in the pull requests it opens, so a reviewer can read the conversation behind a change. Entire is a company built on this idea: its open-source CLI stores every agent session in the repository, paired with the commit it produced and with secrets redacted.

Sessions also hold things you can’t publish, like credentials that showed up in tool output and internal hostnames. Sharing them inside your team is fine. In public, share summaries and short excerpts instead. The numbers from my logs in this post are all counts.

Lessons

  • A mature codebase with lots of tests is easy to rewrite today. cBioPortal’s test suites, website tests and production traffic gave the agents a way to check every change against the original. In 2025 I wrote that AI tools struggled with large, mature codebases. With the latest models, that maturity is what made this rewrite possible.
  • You still need to know how the original works. A rewrite is full of small decisions. Should the API reproduce Java’s row order, even where Java never sorts and returns rows in whatever order the database left them? (Many differences came down to that.) Is Java’s answer correct, or a bug worth fixing? The agent will decide for you if you let it. It’s better to tell it, and that’s much easier if you know the original code and how the program is used.
  • Store your prompts somewhere safe! Those small decisions are exactly what others will ask about later, so keep a record of them. Claude Code deletes sessions after 30 days by default, so raise cleanupPeriodDays in your settings before you start.

🔧inodb🧬
Written by@inodb
building tools for cancer genomics

LinkedInGitHubCVGoogle ScholarPresentations