I absolutely love when I attend a conference and have an interaction with a customer where the problem they are having eats away at me. Recently I attended Cloud Next 2026 in Las Vegas and had a customer meeting where the customer expressed frustration with Firebase Storage egress costs and I stayed up pretty late exploring the solution. This is my solution to their issue.
Egress on media from Google Cloud Storage
Customer states that depending on network usage of their services that their egress would cost them quite a bit each month. The setup is that the customer has a series of media files that they are storing and delivering to their customers.
The media files do not change often and are only for paying customers meaning that they cannot serve the media to anyone online, just those logged in. There are two things that make this interesting. The first was did Cloud CDN support signed URLs for content and the second was what is the break even point for Cloud CDN vs Google Cloud Storage for egress costs only?
Signed URLs
The first experiment I did was to determine whether I could sign URLs. I was able to find this documentation on generating signed URLs from Google Cloud. It seems that it’s likely I can generate a signed URL for a Cloud CDN as long as I grab the base64 encoded key bytes from the cloud console on key creation. I then went off to experiment creating a CDN.
Cloud CDN configuration
In the Cloud CDN menu, I selected to Add an Origin at the top of the page. I was then asked to pick out an Origin Type, I chose Backend Bucket, Define my backend bucket, I choose an Existing Backend Bucket which was the folder with all of my media content in it, and then select an origin name. Once I had done that, I needed to add a Load Balancer. This was the most stressful part for me because I was unaware that I needed a Load Balancer and knew there was a minimum monthly charge for load balancing. For one forwarding rule which just describes how to route requests to your load balancer to reroute to your CDN, it costs 18.25 a month. You can include four more forwarding rules without exceeding this price from my read of the documentation. This is where I selected to create a new load balancer and continued onward. Now for the Cache Performance menu. In this menu, I chose the default Cache static Content (recommended) for the Cache mode. Since the customer is only serving static media content, I think we can set a TTL of 1 year for the Client, Default, and Maximum settings. This means that clients will likely never need to redownload content and additionally, they likely not need to fill the cache as often with a lower TTL. I then left all the other options set to the default and moved down to the Restricted Content section. In this section I chose to Restrict access using signed URLs and signed cookies and added a new URL signing key. I filed in the name I wanted for the key and selected the option to Automatically generate signing keys. Here I needed to grab the Signing key value from the dialog box and store it safely before deploying the app to my server, perhaps in a Google Cloud Secret. Anyone with this string can sign requests to my CDN and get data which would not be helpful if I am attempting to restrict access using signed urls. Finally, I click Done and wait for the Cloud CDN to be ready.
Generating signed URLs
Since the customer is a Flutter developer, I wanted to make sure that I provided them with a code sample for Signed URLs in their preferred backend language, Dart! For this I leaned on Gemini to help me convert the existing code samples from the docs to Dart and came up with the following code snippet:
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:dotenv/dotenv.dart';
final env = DotEnv(includePlatformEnvironment: true);
String generateSignedUrl(String imageName, {int? expirationTime}) {
if (File('.env').existsSync()) {
env.load(['.env']);
}
final address = Platform.environment['CDN_ADDRESS'] ?? env['CDN_ADDRESS'];
final baseUrl = 'http://$address';
final url = '$baseUrl/$imageName';
final keyName = env['CDN_KEY_NAME'] ?? Platform.environment['CDN_KEY_NAME'];
final base64Key =
env['CDN_BASE64_KEY'] ?? Platform.environment['CDN_BASE64_KEY'];
if (keyName == null ||
keyName.isEmpty ||
base64Key == null ||
base64Key.isEmpty) {
throw StateError(
'CDN_KEY_NAME or CDN_BASE64_KEY environment variable is missing',
);
}
final resolvedExpirationTime =
expirationTime ??
(DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000 + 3600);
// Decode the base64url-encoded key, adding padding if needed
String padBase64(String input) {
final rem = input.length % 4;
if (rem == 0) return input;
return input + '=' * (4 - rem);
}
final List<int> keyBytes = base64Url.decode(padBase64(base64Key));
// Determine URL separator
final separator = url.contains('?') ? '&' : '?';
// Construct URL string to sign
final urlToSign =
'$url${separator}Expires=$resolvedExpirationTime&KeyName=$keyName';
// Create HMAC-SHA1 signature
final hmacSha1 = Hmac(sha1, keyBytes);
final digest = hmacSha1.convert(utf8.encode(urlToSign));
// Encode signature using base64url (keep padding)
final signature = base64Url.encode(digest.bytes);
// Construct final signed URL
return '$urlToSign&Signature=$signature';
}
You can see that I am using a dotenv file to store a CDN_KEY_NAME,
CDN_BASE64_KEY, and a CDN_ADDRESS. The CDN_KEY_NAME value comes from the
key name we specified in the previous step. The CDN_BASE64_KEY is the Signing
Key Value that we also specified in a previous step. Finally, to get the
CDN_ADDRESS, I needed to know the address of my load balancer which I can
retrieve from the following gcloud command.
gcloud compute forwarding-rules list --global
In the output I looked for the entry ${LOAD_BALANCER_NAME}-forwarding-rule-ipv4
and noted its IP_ADDRESS. Once I had that, I used a shelf server to handle
the signing of URLs with some Firebase middleware to confirm the users custom
claims from their Firebase Authentication Token and I had a working CDN.
Middleware authMiddleware() {
return (Handler innerHandler) {
return (Request request) async {
if (env['ENABLE_AUTH_VERIFICATION'] != 'false' && Platform.environment['ENABLE_AUTH_VERIFICATION'] != 'false') {
final authHeader = request.headers['Authorization'];
if (authHeader == null || authHeader.isEmpty || !authHeader.startsWith('Bearer ')) {
return Response.forbidden(jsonEncode({'error': 'Missing or invalid Authorization header'}));
}
final idToken = authHeader.substring(7);
FirebaseApp getFirebaseApp() {
try {
return FirebaseApp.instance;
} catch (_) {
return FirebaseApp.initializeApp();
}
}
final app = getFirebaseApp();
final auth = app.auth();
if (Platform.environment['TEST_ENV'] != 'true') {
try {
await auth.verifyIdToken(idToken);
} catch (e) {
return Response.forbidden(jsonEncode({'error': 'Token verification failed', 'details': e.toString()}));
}
}
final decodedJWT = dart_jsonwebtoken.JWT.decode(idToken);
final payload = decodedJWT.payload as Map;
if (payload['paidUser'] != true) {
return Response.forbidden(jsonEncode({'error': 'User does not have paidUser privilege'}));
}
}
return await innerHandler(request);
};
};
}
Future<Response> _imageHandler(Request request) async {
try {
final imageName = request.params['image_name'];
if (imageName == null || imageName.isEmpty) {
return Response.badRequest(
body: jsonEncode({'error': 'Missing image name'}),
);
}
final signedUrl = generateSignedUrl(imageName);
return Response.ok(
jsonEncode({'signed_url': signedUrl}),
headers: {'Content-Type': 'application/json'},
);
} catch (e) {
return Response.internalServerError(
body: jsonEncode({'error': 'Failed to generate signed URL', 'details': e.toString()}),
);
}
}
Break even point
This got me wondering what the minimum break even point is for egress costs on Cloud CDN are and just based on back of napkin math where Fixed Price/(Normal price - Discount Price) = break even price. Using this formula:
(Load balancer fixed price)/(Storage egress costs - CDN egress costs)
otherwise written as 18.25/(0.12-0.8), I got a break even point of 456.25 gigabytes of network egress which equates to a break even price of $54.75 USD per month. Therefore, if you want to save on network egress costs just from storage, if you send out about 450+ gigabytes a month, you are better off using Cloud CDN.
This price does ignore other costs like cache fill requests but since the cache fill we count as a yearly expense due to our long cache TTL, I felt like we could exclude it from our calculations.
Are there other options?
Based on the limited information I had when speaking to the customer this seemed like a good solution. One other solution I thought of after we spoke was that we could ask the customer to explore an LRU cache in Flutter that could hold onto media content for sometime as the media may be frequently visited media but I didn’t have time to explore it. Maybe my friend Rody has some ideas here?