diff --git a/textcaptcha.install b/textcaptcha.install
new file mode 100644
index 0000000..9dee928
--- /dev/null
+++ b/textcaptcha.install
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * Implements hook_install().
+ */
+function textcaptcha_install() {
+  $text = t('To set up textCAPTCHA get an API key !here. Then go to the admin
+  configuration page !admin to save your key and retrieve some challenge
+  questions.', 
+    array(
+      '!here' => l('here', 'http://textcaptcha.com/'), 
+      '!admin' => l('here', 'admin/config/people/captcha/textcaptcha'),
+    )
+  );
+  drupal_set_message($text, 'warning');
+}
+
+/**
+ * Implements hook_update_N().
+ */
+function textcaptcha_update_7001() {
+  // Update captcha limit. There was a bug, restricting cache limit to 0, 1, 2,
+  // 3, or 4 (when users were selecting 0, 10, 20, 30, 40).
+  $limit = variable_get('textcaptcha_cache_limit', 0);
+  switch ($limit) {
+    case 0:
+    case 1:
+      variable_set('textcaptcha_cache_limit', 10);
+      break;
+    case 2:
+      variable_set('textcaptcha_cache_limit', 20);
+      break;
+    case 3:
+      variable_set('textcaptcha_cache_limit', 30);
+      break;
+    case 4:
+      variable_set('textcaptcha_cache_limit', 40);
+      break;
+  }
+
+  // Populate cache with some challenges.
+  for ($i = 0; $i < 10; $i++) {
+    if ($challenge = textcaptcha_get_new_challenge()) {
+      textcaptcha_cache_new_challenge($challenge);
+    }
+  }
+}
diff --git a/textcaptcha.module b/textcaptcha.module
index d9273a7..a272a00 100644
--- a/textcaptcha.module
+++ b/textcaptcha.module
@@ -1,4 +1,4 @@
-<?php // $Id: textcaptcha.module,v 1.1.2.2.2.1 2010/12/08 21:34:33 kevee Exp $
+<?php
 
 /**
 *  @file
@@ -43,27 +43,73 @@ function textcaptcha_settings_form() {
   );
 
   $form['textcaptcha_cache_limit'] = array(
-    '#type' => 'select',
-    '#title' => t('Number of challenges to keep as backup'),
-    '#description' => t('If you cannot contact textcaptcha.com or the service is unavailable, some challenges are kept in the site cache as backup. Select zero to turn this feature off.'),
-    '#options' => array(
-              0,
-              10,
-              20,
-              30,
-              40
-            ),
-    '#default_value' => variable_get('textcaptcha_cache_limit', 10),
+    '#type' => 'textfield',
+    '#title' => t('Number of challenges to keep cached locally'),
+    '#description' => t('TextCAPTCHA calls a web service on cron runs to retrieve new challenge questions. This number determines how many challenges to keep cached at a time.'),
+    '#default_value' => variable_get('textcaptcha_cache_limit', 30),
+  );
+
+  $form['textcaptcha_cron'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Fetch questions on cron'),
+    '#description' => t('Enable or disable fetching and processing questions on cron.'),
+    '#default_value' => variable_get('textcaptcha_cron', 1),
+  );
+
+  $form['actions']['text_captcha'] = array(
+    '#type' => 'submit',
+    '#value' => t('Retrieve new captcha challenges'),
+    '#submit' => array('textcaptcha_get_new_challenges'),
   );
 
   return system_settings_form($form);
 }
 
 /**
+ * Get new challenge questions and cache them.
+ */
+function textcaptcha_get_new_challenges() {
+  $success = 0;
+  for ($i = 0; $i < 10; $i++) {
+    if ($challenge = textcaptcha_get_new_challenge()) {
+      textcaptcha_cache_new_challenge($challenge);
+      $success++;
+    }
+  }
+
+  if ($success < 1) {
+    $text = t('Sorry. After trying !i times, I was unable to retrieve any new
+    challenge questions. Either your API key is bad, you are not online, or the
+    web service you are trying to reach is not responding.', array('!i' => $i));
+    drupal_set_message($text, 'warning');
+  }
+  else {
+    $text = t('Success! You have !success new challenge questions. (!success/!i
+    attempts to retrieve new challenge questions were successful.)',
+      array(
+        '!success' => $success,
+        '!i' => $i,
+      )
+    );
+    drupal_set_message($text, 'success');
+  }
+
+  $count = textcaptcha_cache_count();
+  $text = t('You have !count cached challenge questions.', array('!count' => $count));
+  if ($count < 10) {
+    $status = 'warning';
+  }
+  else {
+    $status = 'notice';
+  }
+  drupal_set_message($text, $status);
+}
+
+/**
 *  Form validation for admin settings. Here we check that the API key works.
 */
 function textcaptcha_settings_form_validate($form, $form_state) {
-  $result = drupal_http_request(TEXTCAPTCHA_URL_PREFIX . trim($form_state['values']['textcaptcha_api_key']));
+  $result = textcaptcha_http_request();
   if ($result->code !== '200') {
     form_set_error('textcaptcha_api_key', t('The API Key did not work or the server cannot contact textcaptcha.com'));
   }
@@ -80,10 +126,10 @@ function textcaptcha_captcha($op, $captcha_type='') {
     case 'generate' :
       $result = array();
       if ($captcha_type == 'Text Captcha') {
-        $textcaptcha = textcaptcha_get_captcha();
+        $challenge = textcaptcha_get_challenge();
         $result['form']['captcha_response'] = array(
           '#type' => 'textfield',
-          '#title' => $textcaptcha['question'],
+          '#title' => $challenge['question'],
           '#description' => t('Question text provided by !link',
                               array('!link' => l('textcaptcha.com', 'http://textcaptcha.com'))
                              ),
@@ -91,7 +137,7 @@ function textcaptcha_captcha($op, $captcha_type='') {
           '#maxlength' => 50,
           '#required' => TRUE,
         );
-        $result['solution'] = serialize($textcaptcha['answers']);
+        $result['solution'] = serialize($challenge['answers']);
         $result['captcha_validate'] = 'textcaptcha_captcha_validate';
 
         return $result;
@@ -101,43 +147,42 @@ function textcaptcha_captcha($op, $captcha_type='') {
 }
 
 /**
-*  Retrieves CAPTCHA and possible answer list from the textcaptcha.com service
-*/
-function textcaptcha_get_captcha($use_cache = TRUE) {
-  if (variable_get('textcaptcha_cache_limit', 10) == 0) {
-    $use_cache = FALSE;
-  }
-  if (variable_get('textcaptcha_api_key', FALSE)) {
-    $cached_challenges = cache_get('textcaptcha_cached_challenges');
-    $captcha = drupal_http_request(TEXTCAPTCHA_URL_PREFIX . trim(variable_get('textcaptcha_api_key', '')));
-    if ($captcha->code == '200' && $captcha->data) {
-      $parser = drupal_xml_parser_create($captcha->data);
-      $captcha_xml = array();
-      xml_parse_into_struct($parser, $captcha->data, $captcha_xml);
-      $answers = array();
-      foreach ($captcha_xml as $element) {
-        if ($element['tag'] == 'QUESTION' && $element['type'] == 'complete') {
-          $question = $element['value'];
-        }
-        if ($element['tag'] == 'ANSWER' && $element['type'] == 'complete') {
-          $answers[] = $element['value'];
-        }
-      }
-      $cached_challenges->data[] = array('question' => $question,
-                         'answers' => $answers);
-      if (count($cached_challenges->data) > variable_get('textcaptcha_cache_limit', 10)) {
-        array_shift($cached_challenges->data);
-      }
-      if ($use_cache) {
-        cache_set('textcaptcha_cached_challenges', $cached_challenges->data);
-      }
-      return array('question' => $question, 'answers' => $answers);
-    }
-    elseif ($cached_challenges && $use_cache) {
-      return array_rand($cached_challenges->data);
+ * Retrieves CAPTCHA and possible answer list from the textcaptcha.com service
+ *
+ * @return array $challenge
+ *   @see textcaptcha_parse_data().
+ */
+function textcaptcha_get_challenge() {
+  $challenge = array();
+
+  if (!$key = variable_get('textcaptcha_api_key', FALSE)) {
+    return FALSE;
+  }
+
+  // Try to retreive a cached captcha challenge question.
+  if ($cache = cache_get('textcaptcha_challenges')) {
+    $challenges = array_filter($cache->data);
+    $key = array_rand($challenges);
+    $challenge = $challenges[$key];
+  }
+  else {
+    // If no cached challenges are available try and get one on the fly
+    // from web service.
+    $challenge = textcaptcha_get_new_challenge();
+    if ($challenge) {
+      textcaptcha_cache_new_challenge($challenge);
     }
   }
-  return FALSE;
+
+  // Handle empty response.
+  if (!$challenge) {
+    $text = t("Sorry. Something went wrong. This CAPTCHA won't work. Please reload your page.");
+    drupal_set_message($text, 'warning');
+    $challenge['question'] = $text;
+    $challenge['answers'] = array();
+  }
+
+  return $challenge;
 }
 
 /**
@@ -176,4 +221,126 @@ function textcaptcha_requirements($phase) {
     }
   }
   return $requirements;
-}
\ No newline at end of file
+}
+
+/**
+ * @param array $data
+ *  Data from a successful drupal_http_request() call to textcaptcha.com API
+ *
+ * @return array $challenge
+ *  Return array including challenge and md5 hash of valid answers.
+ *    e.g. $challenge(
+ *            'question' => 'What color is the sky?',
+ *            'answers' => array('c9f0f895f', '24d27c169'),
+ *    );
+ */
+function textcaptcha_parse_data($data) {
+  $parser = drupal_xml_parser_create($data);
+  $captcha_xml = array();
+  xml_parse_into_struct($parser, $data, $captcha_xml);
+
+  $challenge = array();
+  foreach ($captcha_xml as $element) {
+    if ($element['tag'] == 'QUESTION' && $element['type'] == 'complete') {
+      $challenge['question'] = $element['value'];
+    }
+    if ($element['tag'] == 'ANSWER' && $element['type'] == 'complete') {
+      $challenge['answers'][] = $element['value'];
+    }
+  }
+
+  return $challenge;
+}
+
+/**
+ * Implements hook_cron().
+ *
+ * Every time cron runs it checks for a new challenge question to add to the
+ * cache. If there 100 cached challenges, drop the oldest.
+ *
+ */
+function textcaptcha_cron() {
+  if (variable_get('textcaptcha_cron', 1)) {
+    $challenge = textcaptcha_get_new_challenge();
+    textcaptcha_cache_new_challenge($challenge);
+  }
+}
+
+/**
+ * Cache new challenge question.
+ *
+ * @param array $challenge
+ *   @see textcaptcha_parse_data().
+ */
+function textcaptcha_cache_new_challenge($challenge) {
+  // Store challenge with other cached challenges.
+  if ($cache = cache_get('textcaptcha_challenges')) {
+    $data = $cache->data;
+    // If the cache is full, remove the oldest, to be replaced with the new one.
+    $limit = variable_get('textcaptcha_cache_limit', 50);
+    $limit = (is_int($limit)) ? $limit : 50;
+    if (count($data) > $limit) {
+      array_shift($data);
+    }
+    $data[] = $challenge;
+  }
+  else {
+    $data = array($challenge);
+  }
+  // Use array_filter in case there are empty elements in $data.
+  $data = array_filter($data);
+  cache_set('textcaptcha_challenges', $data);
+}
+
+/**
+ * Return count of cached challenge questions.
+ */
+function textcaptcha_cache_count() {
+  if ($cache = cache_get('textcaptcha_challenges')) {
+    $data = $cache->data;
+    $count = count($data);
+  }
+  else {
+    $count = 0;
+  }
+  return $count;
+}
+
+/**
+ * Retrieve a new challenge from textcaptcha web service and cache it.
+ *
+ * @return
+ *  $challenge array or FALSE
+ */
+function textcaptcha_get_new_challenge() {
+  // Attempt to retrieve a new challenge from the web service.
+  $success = FALSE;
+  for ($i = 1; $i <= 10; $i++) {
+    $response = textcaptcha_http_request();
+    if (!$response->code == 200) {
+      // We didn't get a successful response. Log failed attempt.
+      $text = t('Failed attempt !n to get textcaptcha question from !api.', array('!n' => $i, '!api' => TEXTCAPTCHA_URL_PREFIX));
+      watchdog('textcaptcha', $text);
+    }
+    else {
+      // Log success.
+      $success = TRUE;
+      $text = t('Successfully retrieved new textcaptcha challenge question on attempt !n', array('!n' => $i));
+      watchdog('textcaptcha', $text);
+      break;
+    }
+  }
+
+  return ($success) ? textcaptcha_parse_data($response->data) : FALSE;
+}
+
+/**
+ * Wrapper around textcaptcha API call.
+ *
+ * @return array
+ *  Response from drupal_http_request() to textcaptcha web service.
+ */
+function textcaptcha_http_request() {
+  $response = drupal_http_request(TEXTCAPTCHA_URL_PREFIX . trim(variable_get('textcaptcha_api_key', '')));
+  return $response;
+}
