diff --git a/lib/browserid/wsapi.js b/lib/browserid/wsapi.js
index 7934f85d3936029e4c90cfd17729354a9ad7c457..c46c514b117396acf36f74a53c5b425e7ece36ca 100644
--- a/lib/browserid/wsapi.js
+++ b/lib/browserid/wsapi.js
@@ -47,7 +47,7 @@ bcrypt = require('bcrypt'),
 crypto = require('crypto'),
 logger = require('logging.js').logger,
 ca = require('./ca.js'),
-configuration = require('configuration.js');
+config = require('configuration.js');
 
 function checkParams(params) {
   return function(req, resp, next) {
@@ -79,7 +79,6 @@ function clearAuthenticatedUser(session) {
   });
 }
 
-
 function setAuthenticatedUser(session, email) {
   session.authenticatedUser = email;
   session.authenticatedAt = new Date();
@@ -91,7 +90,7 @@ function isAuthed(req) {
     if (req.session.authenticatedUser) {
       if (!Date.parse(req.session.authenticatedAt) > 0) throw "bad timestamp";
       if (new Date() - new Date(req.session.authenticatedAt) >
-          configuration.get('authentication_duration_ms'))
+          config.get('authentication_duration_ms'))
       {
         throw "expired";
       }
@@ -177,27 +176,35 @@ function setup(app) {
     // staging a user logs you out.
     clearAuthenticatedUser(req.session);
 
-    try {
-      // upon success, stage_user returns a secret (that'll get baked into a url
-      // and given to the user), on failure it throws
-      db.stageUser(req.body.email, function(secret) {
-        // store the email being registered in the session data
-        if (!req.session) req.session = {};
+    db.lastStaged(req.body.email, function (last) {
+      if (last && (new Date() - last) < config.get('min_time_between_emails_ms')) {
+        logger.warn('throttling request to stage email address ' + req.body.email + ', only ' +
+                    ((new Date() - last) / 1000.0) + "s elapsed");
+        return httputils.forbidden(resp, "throttling.  try again later.");
+      }
 
-        // store the secret we're sending via email in the users session, as checking
-        // that it still exists in the database is the surest way to determine the
-        // status of the email verification.
-        req.session.pendingCreation = secret;
+      try {
+        // upon success, stage_user returns a secret (that'll get baked into a url
+        // and given to the user), on failure it throws
+        db.stageUser(req.body.email, function(secret) {
+          // store the email being registered in the session data
+          if (!req.session) req.session = {};
 
-        resp.json({ success: true });
+          // store the secret we're sending via email in the users session, as checking
+          // that it still exists in the database is the surest way to determine the
+          // status of the email verification.
+          req.session.pendingCreation = secret;
 
-        // let's now kick out a verification email!
-        email.sendNewUserEmail(req.body.email, req.body.site, secret);
-      });
-    } catch(e) {
-      // we should differentiate tween' 400 and 500 here.
-      httputils.badRequest(resp, e.toString());
-    }
+          resp.json({ success: true });
+
+          // let's now kick out a verification email!
+          email.sendNewUserEmail(req.body.email, req.body.site, secret);
+        });
+      } catch(e) {
+        // we should differentiate tween' 400 and 500 here.
+        httputils.badRequest(resp, e.toString());
+      }
+    });
   });
 
   app.get('/wsapi/user_creation_status', function(req, resp) {
@@ -233,7 +240,7 @@ function setup(app) {
   });
 
   function bcrypt_password(password, cb) {
-    var bcryptWorkFactor = configuration.get('bcrypt_work_factor');
+    var bcryptWorkFactor = config.get('bcrypt_work_factor');
 
     bcrypt.gen_salt(bcryptWorkFactor, function (err, salt) {
       if (err) {
@@ -293,22 +300,30 @@ function setup(app) {
   });
 
   app.post('/wsapi/stage_email', checkAuthed, checkParams(["email", "site"]), function (req, resp) {
-    try {
-      // on failure stageEmail may throw
-      db.stageEmail(req.session.authenticatedUser, req.body.email, function(secret) {
+    db.lastStaged(req.body.email, function (last) {
+      if (last && (new Date() - last) < config.get('min_time_between_emails_ms')) {
+        logger.warn('throttling request to stage email address ' + req.body.email + ', only ' +
+                    ((new Date() - last) / 1000.0) + "s elapsed");
+        return httputils.forbidden(resp, "throttling.  try again later.");
+      }
 
-        // store the email being added in session data
-        req.session.pendingAddition = secret;
+      try {
+        // on failure stageEmail may throw
+        db.stageEmail(req.session.authenticatedUser, req.body.email, function(secret) {
 
-        resp.json({ success: true });
+          // store the email being added in session data
+          req.session.pendingAddition = secret;
 
-        // let's now kick out a verification email!
-        email.sendAddAddressEmail(req.body.email, req.body.site, secret);
-      });
-    } catch(e) {
-      // we should differentiate tween' 400 and 500 here.
-      httputils.badRequest(resp, e.toString());
-    }
+          resp.json({ success: true });
+
+          // let's now kick out a verification email!
+          email.sendAddAddressEmail(req.body.email, req.body.site, secret);
+        });
+      } catch(e) {
+        // we should differentiate tween' 400 and 500 here.
+        httputils.badRequest(resp, e.toString());
+      }
+    });
   });
 
   app.get('/wsapi/email_for_token', checkParams(["token"]), function(req,resp) {
@@ -387,7 +402,7 @@ function setup(app) {
 
           // if the work factor has changed, update the hash here.  issue #204
           // NOTE: this runs asynchronously and will not delay the response
-          if (configuration.get('bcrypt_work_factor') != bcrypt.get_rounds(hash)) {
+          if (config.get('bcrypt_work_factor') != bcrypt.get_rounds(hash)) {
             logger.info("updating bcrypted password for email " + req.body.email);
             bcrypt_password(req.body.pass, function(err, hash) {
               db.updatePassword(req.body.email, hash, function(err) {
@@ -436,7 +451,7 @@ function setup(app) {
       // same account, we certify the key
       // we certify it for a day for now
       var expiration = new Date();
-      expiration.setTime(new Date().valueOf() + configuration.get('certificate_validity_ms'));
+      expiration.setTime(new Date().valueOf() + config.get('certificate_validity_ms'));
       var cert = ca.certify(req.body.email, pk, expiration);
 
       resp.writeHead(200, {'Content-Type': 'text/plain'});
diff --git a/lib/configuration.js b/lib/configuration.js
index 09fa82aa463925bdfd788a86fa8af5d83f69dc29..050f7020623cc18771ab1f7490a7adc983e9b923 100644
--- a/lib/configuration.js
+++ b/lib/configuration.js
@@ -84,7 +84,8 @@ g_configs.production = {
   },
   bcrypt_work_factor: 12,
   authentication_duration_ms: (7 * 24 * 60 * 60 * 1000),
-  certificate_validity_ms: (24 * 60 * 60 * 1000)
+  certificate_validity_ms: (24 * 60 * 60 * 1000),
+  min_time_between_emails_ms: (60 * 1000)
 };
 
 
@@ -97,7 +98,8 @@ g_configs.local =  {
   database: { driver: "json" },
   bcrypt_work_factor: g_configs.production.bcrypt_work_factor,
   authentication_duration_ms: g_configs.production.authentication_duration_ms,
-  certificate_validity_ms: g_configs.production.certificate_validity_ms
+  certificate_validity_ms: g_configs.production.certificate_validity_ms,
+  min_time_between_emails_ms: g_configs.production.min_time_between_emails_ms
 };
 
 if (undefined !== process.env['NODE_EXTRA_CONFIG']) {
@@ -105,16 +107,6 @@ if (undefined !== process.env['NODE_EXTRA_CONFIG']) {
   eval(fs.readFileSync(process.env['NODE_EXTRA_CONFIG']) + '');
 }
 
-Object.keys(g_configs).forEach(function(config) {
-  if (!g_configs[config].smtp) {
-    g_configs[config].smtp = {
-      host: process.env['SMTP_HOST'],
-      user: process.env['SMTP_USER'],
-      pass: process.env['SMTP_PASS']
-    };
-  }
-});
-
 // test environments are variations on local
 g_configs.test_json = JSON.parse(JSON.stringify(g_configs.local));
 g_configs.test_json.database = {
@@ -155,6 +147,15 @@ if (process.env['VERIFIER_URL']) {
   g_config.verifier_url = url;
 }
 
+// extract smtp params from the environment
+if (!g_config.smtp) {
+  g_config.smtp = {
+    host: process.env['SMTP_HOST'],
+    user: process.env['SMTP_USER'],
+    pass: process.env['SMTP_PASS']
+  };
+}
+
 // now handle ephemeral database configuration.  Used in testing.
 if (g_config.database.driver === 'mysql') {
   if (process.env['MYSQL_DATABASE_NAME']) {
diff --git a/lib/db.js b/lib/db.js
index 97232454d422467662c6bb28eea7965c1b1184fa..e3a4092039378bd409c8f9a0f42a50d1e092c3f9 100644
--- a/lib/db.js
+++ b/lib/db.js
@@ -104,7 +104,8 @@ exports.onReady = function(f) {
   'listEmails',
   'removeEmail',
   'cancelAccount',
-  'updatePassword'
+  'updatePassword',
+  'lastStaged'
 ].forEach(function(fn) {
   exports[fn] = function() {
     checkReady();
diff --git a/lib/db/json.js b/lib/db/json.js
index 2fa8409131bc98673b5c24d1cb34c80e8743196e..9b5b68095b810031fbc27e0921c49fab390af76f 100644
--- a/lib/db/json.js
+++ b/lib/db/json.js
@@ -129,6 +129,17 @@ exports.isStaged = function(email, cb) {
   }
 };
 
+exports.lastStaged = function(email, cb) {
+  if (cb) {
+    sync();
+    var d;
+    if (db.stagedEmails.hasOwnProperty(email)) {
+      d = new Date(db.staged[db.stagedEmails[email]].when);
+    }
+    setTimeout(function() { cb(d); }, 0);
+  }
+};
+
 exports.emailsBelongToSameAccount = function(lhs, rhs, cb) {
   sync();
   emailToUserID(lhs, function(lhs_uid) {
@@ -161,7 +172,8 @@ exports.stageUser = function(email, cb) {
   sync();
   db.staged[secret] = {
     type: "add_account",
-    email: email
+    email: email,
+    when: (new Date()).getTime()
   };
   db.stagedEmails[email] = secret;
   flush();
@@ -176,7 +188,8 @@ exports.stageEmail = function(existing_email, new_email, cb) {
   db.staged[secret] = {
     type: "add_email",
     existing_email: existing_email,
-    email: new_email
+    email: new_email,
+    when: (new Date()).getTime()
   };
   db.stagedEmails[new_email] = secret;
   flush();
diff --git a/lib/db/mysql.js b/lib/db/mysql.js
index b1209b091b4572e8b321a98b74a5c7aae04b6ce4..25416817488b5fcd0f683e58dcb0271c3516b4e7 100644
--- a/lib/db/mysql.js
+++ b/lib/db/mysql.js
@@ -78,7 +78,7 @@ const schemas = [
   "CREATE TABLE IF NOT EXISTS email (" +
     "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
     "user BIGINT NOT NULL," +
-    "address VARCHAR(255) UNIQUE NOT NULL," + 
+    "address VARCHAR(255) UNIQUE NOT NULL," +
     "FOREIGN KEY user_fkey (user) REFERENCES user(id)" +
     ") ENGINE=InnoDB;",
 
@@ -88,7 +88,7 @@ const schemas = [
     "new_acct BOOL NOT NULL," +
     "existing VARCHAR(255)," +
     "email VARCHAR(255) UNIQUE NOT NULL," +
-    "ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL" + 
+    "ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL" +
     ") ENGINE=InnoDB;",
 ];
 
@@ -204,6 +204,17 @@ exports.isStaged = function(email, cb) {
   );
 }
 
+exports.lastStaged = function(email, cb) {
+  client.query(
+    "SELECT UNIX_TIMESTAMP(ts) as ts FROM staged WHERE email = ?", [ email ],
+    function(err, rows) {
+      if (err) logUnexpectedError(err);
+      if (!rows || rows.length === 0) cb();
+      else cb(new Date(rows[0].ts * 1000));
+    }
+  );
+}
+
 exports.stageUser = function(email, cb) {
   var secret = secrets.generate(48);
   // overwrite previously staged users
diff --git a/lib/httputils.js b/lib/httputils.js
index f88539a9b06dfc40c448fdef9e5cac1c5032bc2e..6b389251d2d807da6b86106a53b0e33b45df4810 100644
--- a/lib/httputils.js
+++ b/lib/httputils.js
@@ -63,6 +63,16 @@ exports.badRequest = function(resp, reason)
   resp.end();
 };
 
+exports.forbidden = function(resp, reason)
+{
+  resp.writeHead(403, {"Content-Type": "text/plain"});
+  resp.write("Forbidden");
+  if (reason) {
+    resp.write(": " + reason);
+  }
+  resp.end();
+};
+
 exports.jsonResponse = function(resp, obj)
 {
   resp.writeHead(200, {"Content-Type": "application/json"});
diff --git a/lib/wsapi_client.js b/lib/wsapi_client.js
index 1db1af746b15c8f808bd4090fc341517c137750e..29eeabec6db9d852dcba220ead0e3990a06f8833 100644
--- a/lib/wsapi_client.js
+++ b/lib/wsapi_client.js
@@ -54,7 +54,7 @@ function injectCookies(ctx, headers) {
       headers['Cookie'] += k + "=" + ctx.cookieJar[k];
     }
   }
-} 
+}
 
 function extractCookies(ctx, res) {
   if (ctx.cookieJar === undefined) ctx.cookieJar = {};