diff --git a/src/librustc/lint/context.rs b/src/librustc/lint/context.rs
index 81d3d440b566f..cf4115e37cd98 100644
--- a/src/librustc/lint/context.rs
+++ b/src/librustc/lint/context.rs
@@ -452,8 +452,7 @@ pub fn raw_struct_lint<'a>(sess: &'a Session,
     }
 
     if let Some(span) = def {
-        let explanation = "lint level defined here";
-        err.span_note(span, &explanation);
+        sess.diag_span_note_once(&mut err, lint, span, "lint level defined here");
     }
 
     err
diff --git a/src/librustc/session/mod.rs b/src/librustc/session/mod.rs
index d002aba595bca..ce3f2be54b2d5 100644
--- a/src/librustc/session/mod.rs
+++ b/src/librustc/session/mod.rs
@@ -17,7 +17,7 @@ use middle::dependency_format;
 use session::search_paths::PathKind;
 use session::config::DebugInfoLevel;
 use ty::tls;
-use util::nodemap::{NodeMap, FnvHashMap};
+use util::nodemap::{NodeMap, FnvHashMap, FnvHashSet};
 use util::common::duration_to_secs_str;
 use mir::transform as mir_pass;
 
@@ -75,6 +75,10 @@ pub struct Session {
     pub working_dir: PathBuf,
     pub lint_store: RefCell<lint::LintStore>,
     pub lints: RefCell<NodeMap<Vec<(lint::LintId, Span, String)>>>,
+    /// Set of (LintId, span, message) tuples tracking lint (sub)diagnostics
+    /// that have been set once, but should not be set again, in order to avoid
+    /// redundantly verbose output (Issue #24690).
+    pub one_time_diagnostics: RefCell<FnvHashSet<(lint::LintId, Span, String)>>,
     pub plugin_llvm_passes: RefCell<Vec<String>>,
     pub mir_passes: RefCell<mir_pass::Passes>,
     pub plugin_attributes: RefCell<Vec<(String, AttributeType)>>,
@@ -288,6 +292,35 @@ impl Session {
     pub fn diagnostic<'a>(&'a self) -> &'a errors::Handler {
         &self.parse_sess.span_diagnostic
     }
+
+    /// Analogous to calling `.span_note` on the given DiagnosticBuilder, but
+    /// deduplicates on lint ID, span, and message for this `Session` if we're
+    /// not outputting in JSON mode.
+    //
+    // FIXME: if the need arises for one-time diagnostics other than
+    // `span_note`, we almost certainly want to generalize this
+    // "check/insert-into the one-time diagnostics map, then set message if
+    // it's not already there" code to accomodate all of them
+    pub fn diag_span_note_once<'a, 'b>(&'a self,
+                                       diag_builder: &'b mut DiagnosticBuilder<'a>,
+                                       lint: &'static lint::Lint, span: Span, message: &str) {
+        match self.opts.error_format {
+            // when outputting JSON for tool consumption, the tool might want
+            // the duplicates
+            config::ErrorOutputType::Json => {
+                diag_builder.span_note(span, &message);
+            },
+            _ => {
+                let lint_id = lint::LintId::of(lint);
+                let id_span_message = (lint_id, span, message.to_owned());
+                let fresh = self.one_time_diagnostics.borrow_mut().insert(id_span_message);
+                if fresh {
+                    diag_builder.span_note(span, &message);
+                }
+            }
+        }
+    }
+
     pub fn codemap<'a>(&'a self) -> &'a codemap::CodeMap {
         self.parse_sess.codemap()
     }
@@ -561,6 +594,7 @@ pub fn build_session_(sopts: config::Options,
         working_dir: env::current_dir().unwrap(),
         lint_store: RefCell::new(lint::LintStore::new()),
         lints: RefCell::new(NodeMap()),
+        one_time_diagnostics: RefCell::new(FnvHashSet()),
         plugin_llvm_passes: RefCell::new(Vec::new()),
         mir_passes: RefCell::new(mir_pass::Passes::new()),
         plugin_attributes: RefCell::new(Vec::new()),
diff --git a/src/test/ui/span/issue-24690.rs b/src/test/ui/span/issue-24690.rs
new file mode 100644
index 0000000000000..def0d9aced3c3
--- /dev/null
+++ b/src/test/ui/span/issue-24690.rs
@@ -0,0 +1,22 @@
+// Copyright 2016 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! A test to ensure that helpful `note` messages aren't emitted more often
+//! than necessary.
+
+// Although there are three errors, we should only get two "lint level defined
+// here" notes pointing at the `warnings` span, one for each error type.
+#![deny(warnings)]
+
+fn main() {
+    let theTwo = 2;
+    let theOtherTwo = 2;
+    println!("{}", theTwo);
+}
diff --git a/src/test/ui/span/issue-24690.stderr b/src/test/ui/span/issue-24690.stderr
new file mode 100644
index 0000000000000..0d2a2ef751666
--- /dev/null
+++ b/src/test/ui/span/issue-24690.stderr
@@ -0,0 +1,32 @@
+error: unused variable: `theOtherTwo`
+  --> $DIR/issue-24690.rs:20:9
+   |
+20 |     let theOtherTwo = 2;
+   |         ^^^^^^^^^^^
+   |
+note: lint level defined here
+  --> $DIR/issue-24690.rs:16:9
+   |
+16 | #![deny(warnings)]
+   |         ^^^^^^^^
+
+error: variable `theTwo` should have a snake case name such as `the_two`
+  --> $DIR/issue-24690.rs:19:9
+   |
+19 |     let theTwo = 2;
+   |         ^^^^^^
+   |
+note: lint level defined here
+  --> $DIR/issue-24690.rs:16:9
+   |
+16 | #![deny(warnings)]
+   |         ^^^^^^^^
+
+error: variable `theOtherTwo` should have a snake case name such as `the_other_two`
+  --> $DIR/issue-24690.rs:20:9
+   |
+20 |     let theOtherTwo = 2;
+   |         ^^^^^^^^^^^
+
+error: aborting due to 3 previous errors
+