When I started contributing to open source, I had the same vague advice everyone has: "just contribute to open source." Nobody tells you the actual mechanics — how to pick an issue that won't evaporate, how to talk to maintainers, what to do when your first attempt goes nowhere. This is the write-up I wish I'd had.
Over the past few months I landed merged contributions in two production Rust projects: ReductStore, a time-series database for robotics and industrial IoT, and pyrefly, Meta's Python type checker written in Rust. Here is how it actually went, including the parts that didn't work.
The first lesson: a contribution can evaporate before you finish
My first attempt taught me the most. I claimed a crash issue in pyrefly that looked clean and well-scoped. I set up the environment, started digging in, and then the reporter commented that they could no longer reproduce it. The maintainers' other fixes had resolved it incidentally. I never wrote a line of fix code, and the issue closed.
That felt like wasted effort at the time. It wasn't. It taught me the single most important habit I now follow: reproduce the bug yourself, on the current code, before you claim anything.
A crash that the reporter themselves can't trigger anymore is a ghost. An issue without reproduction steps is a maybe. Before I commit to an issue now, I get the failure to happen on my own machine, with my own steps, on the latest main branch. If I can't, I don't claim it, or I ask the reporter for specifics rather than guessing.
Picking the right issue matters more than picking a hard one
After that, I got selective. Not every "good first issue" is actually good for a first contribution. The categories I learned to weigh:
- Crashes and deterministic bugs are easier to land than features. A crash has a correct answer: it stops crashing, you add a regression test, done. A new feature or a new diagnostic rule has many defensible implementations, and the maintainers have to arbitrate which one they want. That arbitration means rounds of back-and-forth. For a first contribution, you want unambiguous success criteria.
- Avoid issues that might get fixed out from under you. Flaky, environment-dependent crashes can vanish as a side effect of unrelated work, which is exactly what happened to my first attempt. A deterministic logic bug — something that is wrong every single time for a clear reason — won't disappear on you.
- A maintainer-confirmed fix direction removes the riskiest unknown. The pyrefly issue I eventually landed was a false-positive diagnostic: the type checker told users to install stub packages for libraries that already shipped their own type information. A maintainer had already commented agreeing with the fix approach. That meant I wasn't guessing at what they wanted — I was implementing something they'd already blessed. That is the lowest-risk kind of issue to take.
Reproduce, then investigate, then code, in that order
The workflow that consistently worked for me is boring, and that's the point:
- Reproduce the issue locally on current main.
- Investigate read-only — find exactly where the bug lives and what the fix needs to touch, before writing anything.
- Align with the maintainer on approach and any genuine design questions.
- Implement the minimal change plus a regression test.
- Verify end-to-end yourself — don't trust that "tests pass" means it works.
For the pyrefly fix, reproduction alone took real work. The diagnostic only fired under a specific configuration, with a real config file present and the package installed in the resolved environment. A naive run showed nothing. I had to understand the tool's preset and severity system before I could even see the bug. By the time I'd surfaced it, I had also located the exact function responsible and understood why it triggered. The investigation was most of the job. The fix itself was small.
Talk to maintainers like a colleague, not a petitioner
Early on I overthought every message. What I learned is that maintainers mostly want two things: evidence you've done your homework, and clarity about what you're asking.
A claim comment that says "I'd like to take this" is fine. A claim comment that says "I reproduced this, here's the trigger condition, here's the function responsible, and here's the fix direction the maintainer already suggested — assigning to me?" gets a fast yes, because it shows you've already done the investigation.
When there's a genuine design decision, surface it before you build, with your own recommendation attached. "Should this go at the call sites or inside the helper? I lean toward the call sites because the resolved path is available there — open to your preference" is far better than either guessing silently or asking an open-ended "how should I do this?" You're making it a one-line decision for a busy person, while showing you understand the tradeoff.
And when a maintainer pushes back on your approach in review, concede cleanly. On one of my ReductStore contributions, a maintainer pointed out I'd put some state in the configuration struct when it belonged as a runtime component. He was right. "Good point, I took a shortcut there, reworking to follow the existing pattern" is the correct response. Defending a worse design to save face is how you lose a maintainer's trust.
What I actually built
ReductStore (Rust, time-series storage for robotics/IIoT): I contributed to the system-events and observability layer. The first contribution (PR #1417) added replication diagnostics, emitting structured events into a system bucket so operators can query them like any other data. The second (PR #1431) built usage-statistics reporting: each node emits its own metrics — traffic, record counts, storage size — as queryable records on an interval, with replicas forwarding to the primary for a consolidated view. Both shipped and were credited by the maintainer, who thanked me publicly by name.
pyrefly (Rust, Meta's Python type checker): I fixed a false-positive untyped-import diagnostic (PR #3840). The checker recommended installing stub packages for libraries that already ship their own type markers (a py.typed file, per PEP 561), like modern versions of requests. The fix checks for that marker before warning, and crucially handles submodules correctly, since the marker lives at the package root, not in each submodule directory. It merged into Meta's repository.
I want to be precise about scope, because it matters: this work sits in the observability and diagnostics layers of these systems. I did not design the storage engine's durability core or the type checker's inference engine. I contributed real, shipped features against well-defined parts of large codebases. That's a true description, and it's enough.
One detail I'll admit felt good: the ReductStore maintainer announced the first contribution publicly, on his own channels, thanking me by name. I mention it not to boast but because it's the clearest signal that contributing well — rather than asking for recognition — is what earns it. You don't get credited for showing up. You get credited for being useful.
The habits that actually moved the needle
If I compress everything into the few things that made the difference:
- Reproduce before you claim. A bug you haven't seen fire is a bug you might not be able to fix.
- Investigate before you code. Reading code you didn't write, until you understand where the change goes and why, taught me more than any tutorial. The implementation is usually the small part.
- Verify before you trust. "The tests pass" is not the same as "the fix works." Run the original reproduction against your change and watch the bug disappear.
- Be accurate about what you did. Overstating your contributions is the one thing that backfires with exactly the technical people you want to impress. The honest version of good work is impressive on its own.
None of this is clever. It's just the discipline of treating someone else's codebase with the care it deserves, and treating the maintainers as people whose time is worth not wasting. That's the whole thing.
I'm a full-stack and Rust engineer focused on systems programming and developer tooling. See more of my work, the open-source contributions behind this post, or what I do in Rust.