diff --git a/crates/api_clients/grok_consumer_client/src/requests/image_websocket/grok_wrapped_websocket.rs b/crates/api_clients/grok_consumer_client/src/requests/image_websocket/grok_wrapped_websocket.rs
--- a/crates/api_clients/grok_consumer_client/src/requests/image_websocket/grok_wrapped_websocket.rs
+++ b/crates/api_clients/grok_consumer_client/src/requests/image_websocket/grok_wrapped_websocket.rs
@@ -5,36 +5,34 @@ use futures::stream::FusedStream;
 use futures::{Stream, TryStreamExt};
 use log::{info, warn};
 use serde::Serialize;
-use std::sync::{Arc, RwLock};
+use std::sync::Arc;
 use std::time::Duration;
+use tokio::sync::Mutex;
 use wreq::ws::message::Message;
 use wreq::ws::WebSocket;
 
 #[derive(Clone)]
 pub struct GrokWrappedWebsocket {
-  pub(crate) websocket: Arc<RwLock<WebSocket>>,
+  pub(crate) websocket: Arc<Mutex<WebSocket>>,
 }
 
 impl GrokWrappedWebsocket {
   pub fn new(websocket: WebSocket) -> Self {
     Self {
-      websocket: Arc::new(RwLock::new(websocket)),
+      websocket: Arc::new(Mutex::new(websocket)),
     }
   }
 
   pub async fn send(&self, message: String) -> Result<(), GrokError> {
-    match self.websocket.write() {
-      Err(err) => Err(GrokClientError::WebsocketLockError.into()),
-      Ok(mut websocket) => {
-        let message = Message::text(message);
-        websocket.send(message)
-            .await
-            .map_err(|err| {
-              GrokClientError::WreqClientError(err)
-            })?;
-        Ok(())
-      },
-    }
+    // NB: tokio's Mutex can be held across `.await`, unlike std's guards.
+    let mut websocket = self.websocket.lock().await;
+    let message = Message::text(message);
+    websocket.send(message)
+        .await
+        .map_err(|err| {
+          GrokClientError::WreqClientError(err)
+        })?;
+    Ok(())
   }
 
   pub async fn send_serializable<T: Serialize>(&self, message: T) -> Result<(), GrokError> {
@@ -47,53 +45,45 @@ impl GrokWrappedWebsocket {
   }
 
   pub async fn get_response_with_timeout(&self, duration: Duration) -> Result<Option<WebsocketServerMessage>, GrokError> {
-    match self.websocket.write() {
-      Err(_) => Err(GrokClientError::WebsocketLockError.into()),
-      Ok(mut websocket) => {
-        let result = tokio::time::timeout(duration, websocket.try_next()).await;
+    let mut websocket = self.websocket.lock().await;
+    let result = tokio::time::timeout(duration, websocket.try_next()).await;
 
-        match result {
-          Err(elapsed) => {
-            info!("Websocket try_next() elapsed without receiving a message: {:?}", elapsed);
-            Ok(None) // Timeout elapsed.
-          }
-          Ok(inner) => {
-            let maybe_message = inner.map_err(GrokClientError::WebsocketReadError)?;
+    match result {
+      Err(elapsed) => {
+        info!("Websocket try_next() elapsed without receiving a message: {:?}", elapsed);
+        Ok(None) // Timeout elapsed.
+      }
+      Ok(inner) => {
+        let maybe_message = inner.map_err(GrokClientError::WebsocketReadError)?;
 
-            match maybe_message {
-              None => Ok(None),
-              Some(Message::Text(text)) => {
-                let maybe_message = WebsocketServerMessage::from_json_str(&text)?;
-                Ok(Some(maybe_message))
-              },
-              Some(_) => {
-                warn!("Received non-text websocket message.");
-                Ok(None)
-              },
-            }
-          }
+        match maybe_message {
+          None => Ok(None),
+          Some(Message::Text(text)) => {
+            let maybe_message = WebsocketServerMessage::from_json_str(&text)?;
+            Ok(Some(maybe_message))
+          },
+          Some(_) => {
+            warn!("Received non-text websocket message.");
+            Ok(None)
+          },
         }
-      },
+      }
     }
   }
 
   pub async fn try_next_timeout(&self, duration: Duration) -> Result<Option<Message>, GrokClientError> {
-    match self.websocket.write() {
-      Err(_) => Err(GrokClientError::WebsocketLockError),
-      Ok(mut websocket) => {
-        let result = tokio::time::timeout(duration, websocket.try_next()).await;
+    let mut websocket = self.websocket.lock().await;
+    let result = tokio::time::timeout(duration, websocket.try_next()).await;
 
-        match result {
-          Err(elapsed) => {
-            info!("Websocket try_next() elapsed without receiving a message: {:?}", elapsed);
-            Ok(None) // Timeout elapsed.
-          }
-          Ok(inner) => {
-            let maybe_message = inner.map_err(GrokClientError::WebsocketReadError)?;
-            Ok(maybe_message)
-          }
-        }
-      },
+    match result {
+      Err(elapsed) => {
+        info!("Websocket try_next() elapsed without receiving a message: {:?}", elapsed);
+        Ok(None) // Timeout elapsed.
+      }
+      Ok(inner) => {
+        let maybe_message = inner.map_err(GrokClientError::WebsocketReadError)?;
+        Ok(maybe_message)
+      }
     }
   }
 
diff --git a/crates/desktop/artcraft/src/core/threads/third_party_task_polling_thread/handlers/fal/poll_fal_tasks.rs b/crates/desktop/artcraft/src/core/threads/third_party_task_polling_thread/handlers/fal/poll_fal_tasks.rs
--- a/crates/desktop/artcraft/src/core/threads/third_party_task_polling_thread/handlers/fal/poll_fal_tasks.rs
+++ b/crates/desktop/artcraft/src/core/threads/third_party_task_polling_thread/handlers/fal/poll_fal_tasks.rs
@@ -11,11 +11,15 @@ use enums::tauri::tasks::task_status::TaskStatus;
 use fal_client::creds::fal_api_key::FalApiKey;
 use fal_client::polling::poll_job_response::poll_job_response::{poll_job_response, PollJobResponseArgs};
 use fal_client::polling::poll_job_status::poll_job_status::{poll_job_status, FalJobStatus, PollJobStatusArgs};
+use futures::stream::{self, StreamExt};
 use log::{error, info, warn};
 use sqlite_tasks::queries::task::Task;
 use sqlite_tasks::queries::update_task_status::{update_task_status, UpdateTaskArgs};
 use tauri::AppHandle;
 
+/// Maximum number of FAL jobs polled concurrently in one polling pass.
+const MAX_CONCURRENT_POLLS: usize = 4;
+
 pub async fn poll_fal_tasks(
   app_handle: &AppHandle,
   app_env_configs: &AppEnvConfigs,
@@ -33,25 +37,31 @@ pub async fn poll_fal_tasks(
     }
   };
 
-  for task in fal_tasks {
-    let result = poll_single_fal_task(
-      app_handle,
-      app_env_configs,
-      app_data_root,
-      task_database,
-      storyteller_creds_manager,
-      &api_key,
-      task,
-    ).await;
-
-    if let Err(err) = result {
-      error!(
-        "[FalPolling] Error processing task {}: {:?}",
-        task.id.as_str(),
-        err,
-      );
-    }
-  }
+  let api_key = &api_key;
+
+  // Each task is independent, so poll them concurrently rather than paying
+  // the sum of every round trip.
+  stream::iter(fal_tasks.iter().copied())
+    .for_each_concurrent(MAX_CONCURRENT_POLLS, |task| async move {
+      let result = poll_single_fal_task(
+        app_handle,
+        app_env_configs,
+        app_data_root,
+        task_database,
+        storyteller_creds_manager,
+        api_key,
+        task,
+      ).await;
+
+      if let Err(err) = result {
+        error!(
+          "[FalPolling] Error processing task {}: {:?}",
+          task.id.as_str(),
+          err,
+        );
+      }
+    })
+    .await;
 }
 
 async fn poll_single_fal_task(
